Fix migration workflow: upload as artifact instead of scp to practice EC2
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# ADR 0009: No RAG framework — hand-written orchestration behind ports
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. **Recorded retrospectively** during the 2026-08-12 documentation pass:
|
||||
the decision is unambiguous in the implementation, but no ADR existed for it.
|
||||
|
||||
## Context
|
||||
|
||||
The system performs retrieval-augmented generation with query understanding,
|
||||
multiple retrieval strategies, reranking, prompt construction, structured output
|
||||
parsing, and post-generation verification — the exact feature set LangChain and
|
||||
LlamaIndex exist to provide.
|
||||
|
||||
## Decision
|
||||
|
||||
Neither framework is used. There is no RAG or agent library of any kind.
|
||||
|
||||
Verifiable from the repository:
|
||||
|
||||
- `apps/ai-service/pyproject.toml` declares six runtime dependencies:
|
||||
`fastapi`, `httpx`, `psycopg`, `pydantic-settings`, `qdrant-client`,
|
||||
`uvicorn`. Optional extras add `prometheus-client`, `anthropic` and three
|
||||
OpenTelemetry packages.
|
||||
- `apps/ai-service/Dockerfile` installs that set plus `boto3`.
|
||||
- No file imports `langchain`, `llama_index`, `haystack` or any equivalent.
|
||||
|
||||
Instead:
|
||||
|
||||
- Orchestration is a plain class with an explicit branch table
|
||||
(`rag/agent.py::_route`).
|
||||
- Prompts are module-level constants with JSON schemas (`rag/prompt.py`).
|
||||
- Providers are injected through `typing.Protocol`s (`rag/ports.py`) and
|
||||
implemented in `adapters/`, which is the only package importing an SDK — and
|
||||
always lazily, inside a method.
|
||||
- `bootstrap.py` is the single composition root.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Enabled by this choice**
|
||||
|
||||
- `rag/` imports no SDK, so the entire domain — including every safety check —
|
||||
is unit-testable offline with stub objects. All 278 ai-service tests run in
|
||||
2.6 s with no network.
|
||||
- Behaviour is inspectable: the retrieval route for a given turn is a readable
|
||||
`if` chain, not framework dispatch.
|
||||
- Failure semantics are chosen per call site. The fail-closed/fail-open
|
||||
asymmetry in [02-system-architecture.md](../02-system-architecture.md#failure-boundaries)
|
||||
is deliberate and would be hard to express through a framework's uniform
|
||||
error handling.
|
||||
- Prompt text is reviewable as domain policy in one file, and swapping providers
|
||||
cannot silently change what the model was told.
|
||||
|
||||
**Costs**
|
||||
|
||||
- Retrieval strategies, rank fusion, context packing and evaluation harnesses
|
||||
are all hand-written. Two of them (`fusion.py`, `expansion.py`) were written
|
||||
and never wired ([27-technical-debt.md](../27-technical-debt.md#d-12--dead-code-three-tested-modules-with-no-runtime-caller)).
|
||||
- Optional retriever capabilities are discovered with `getattr` rather than
|
||||
declared, so the real interface is wider than `ports.py` documents (D-14).
|
||||
- No community tooling for tracing, caching or evaluation applies; the
|
||||
observability layer is bespoke.
|
||||
|
||||
## Rationale
|
||||
|
||||
Partially recoverable. The code does not state "we chose not to use a
|
||||
framework", but the ports-and-adapters discipline is documented repeatedly in
|
||||
module docstrings, and one of them makes the intent explicit —
|
||||
`rag/understanding.py`:
|
||||
|
||||
> `rag/` imports no SDK: the LLM is injected as a `JsonLlm` protocol … and a
|
||||
> deterministic stub runs the whole path offline in tests.
|
||||
|
||||
`rag/prompt.py` gives the parallel reason for prompts:
|
||||
|
||||
> This is domain policy, not infrastructure … it lives here so it can be read,
|
||||
> reviewed and tested without an SDK, and so swapping the provider cannot
|
||||
> silently change what the model was told.
|
||||
|
||||
The consistent theme is testability and reviewability of the safety layer.
|
||||
Whether cost, lock-in or framework maturity also weighed in the decision is not
|
||||
recoverable from the repository.
|
||||
@@ -0,0 +1,85 @@
|
||||
# ADR 0010: Single-host Docker Compose as the interim deployment
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. **Recorded retrospectively** during the 2026-08-12 documentation pass.
|
||||
Does **not** supersede [ADR 0002](0002-argocd-gitops.md), whose own status line
|
||||
says it remains the target:
|
||||
|
||||
> **Accepted — still the target, not yet implemented.** Not superseded by the
|
||||
> current production setup.
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0002 chose GitOps on the team's ArgoCD instance. A complete Helm chart
|
||||
(`infra/helm/medical-chatbot/`) and three ArgoCD `Application` manifests exist.
|
||||
Neither has been applied: each `Application` carries three unresolved `TODO`s
|
||||
(project/RBAC scope, repo URL, target cluster), `infra/k8s/base|overlays/` hold
|
||||
only `.gitkeep`, and no image registry is configured anywhere.
|
||||
|
||||
Meanwhile the product is live at `https://realvuxbaro.me`.
|
||||
|
||||
## Decision
|
||||
|
||||
Run production as Docker Compose on a single EC2 host, with Caddy terminating
|
||||
TLS, and deploy by SSH from GitHub Actions.
|
||||
|
||||
Verifiable from the repository:
|
||||
|
||||
- `infra/docker/docker-compose.prod.yml` — postgres, qdrant, ai-service, web,
|
||||
caddy, with named volumes.
|
||||
- `infra/docker/docker-compose.observability.yml` — the OTel/Prometheus/Tempo/
|
||||
Grafana overlay, which also sets `OTEL_ENABLED=true`.
|
||||
- `infra/docker/Caddyfile` — `realvuxbaro.me` → `web:3000`, `/grafana/*` →
|
||||
`grafana:3000`.
|
||||
- `.github/workflows/deploy.yml` — `appleboy/ssh-action`, `git reset --hard`,
|
||||
`docker compose up -d --build`, `caddy reload`, `python -m migrate`, then ~18
|
||||
assertions.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Accepted trade-offs**
|
||||
|
||||
- Images are built on the production host and are untagged, so there is **no
|
||||
artifact to roll back to**; recovery is a revert commit plus a rebuild.
|
||||
- Deploys are in-place, with brief per-service downtime.
|
||||
- No horizontal scaling. That happens to align with the in-process agent state
|
||||
described in [02-system-architecture.md](../02-system-architecture.md#the-stateful-detail-that-constrains-scaling),
|
||||
but the alignment is coincidental, not enforced.
|
||||
- Configuration and secrets live in an uncommitted `.env.prod` on the host, so
|
||||
production configuration cannot be reviewed in Git.
|
||||
- `postgres` and `qdrant` are deliberately absent from the workflow's `up -d`
|
||||
list, so a code deploy never restarts the stateful services — and changes to
|
||||
their service definitions do not take effect until someone restarts them.
|
||||
|
||||
**Preserved despite the simpler runtime**
|
||||
|
||||
The deploy script asserts far more than a Compose deploy usually does: service
|
||||
health, a **real grounded answer** from the real corpus (`decision=answerable`
|
||||
with a `chi_dinh` citation), both Grafana datasources, the provisioned
|
||||
dashboard, public reachability of `/grafana/login`, and end-to-end trace
|
||||
propagation by asserting that a specific `X-Trace-ID` becomes retrievable from
|
||||
Tempo. That verification block is what makes the simpler runtime defensible.
|
||||
|
||||
**Migration path**
|
||||
|
||||
The Helm chart already maps every setting in `config.py` to a ConfigMap, mounts
|
||||
`POSTGRES_DSN` from a Secret, and configures readiness/liveness/startup probes
|
||||
against the same `/ready` and `/health` endpoints Compose uses. Moving to
|
||||
Kubernetes therefore needs: an image registry and tagging, a corpus-load or
|
||||
snapshot-restore step (the chart provisions an **empty** Qdrant, against which
|
||||
`ai-service`'s manifest check refuses to start), the three ArgoCD `TODO`s
|
||||
resolved, and the `bump-image-tag` workflow that
|
||||
`infra/ci/github-actions/README.md` describes but does not contain.
|
||||
|
||||
## Rationale
|
||||
|
||||
**Decision observed; rationale not fully recoverable from the repository.** The
|
||||
Compose header comment records one constraint —
|
||||
|
||||
> No GPU, no team k3s — Bedrock calls go out over the instance's IAM role … so
|
||||
> no AWS access keys live in this file or its env files.
|
||||
|
||||
— and ADR 0002 remaining un-superseded shows the Kubernetes target was not
|
||||
abandoned. Beyond that, whether the driver was cost, cluster access, or time to
|
||||
first deployment is not determinable from the code.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Architecture decision records
|
||||
|
||||
| ADR | Title | Status | Reflected in code? |
|
||||
|---|---|---|---|
|
||||
| [0001](0001-vector-db-qdrant.md) | Use Qdrant as the vector database | Accepted | **Yes** — `adapters/qdrant.py`, `ingestion/load/qdrant_repo.py` |
|
||||
| [0002](0002-argocd-gitops.md) | Use the team's existing ArgoCD instance for deployment (GitOps) | Accepted — target, **not yet implemented** | **No** — production is Docker Compose on EC2 ([20](../20-deployment.md)) |
|
||||
| [0003](0003-pdf-parsing-strategy.md) | PDF parsing strategy, validated empirically | Accepted | **Yes** — `ingestion/extract/`, `ingestion/segment/detector.py` |
|
||||
| [0004](0004-chunking-strategy.md) | Chunking strategy for drug monographs | Accepted (monograph range only) | **Yes** — `ingestion/chunk/chunker.py` |
|
||||
| [0005](0005-segment-output-contract-for-chunking.md) | `segment/` output contract needed by `chunk/` | Proposed; header says "contract only, no implementation" | **Yes, now implemented** — `segment/models.py` + `chunk/` follow it. The status line is stale |
|
||||
| [0006](0006-quarantined-block-references-in-chunks.md) | Chunks must carry references to lifted table/formula blocks | Accepted, implemented in schema v4 | **Yes** — `ChunkAttachment`, `has_quarantined_content`, the ADR-0006 gate set |
|
||||
| [0007](0007-conversational-reasoning-rag.md) | Conversational reasoning RAG (state + bounded loop) | **Superseded by 0008** | **No** — `rag/conversation.py` and `rag/reasoning.py` no longer exist |
|
||||
| [0008](0008-llm-understanding-one-shot-rag.md) | LLM query understanding + one-shot grounded RAG | Accepted, live since 2026-08-06 | **Yes** — `rag/understanding.py`, `rag/agent.py`, `rag/answer.py` |
|
||||
| [0009](0009-no-rag-framework.md) | No RAG framework — hand-written orchestration behind ports | Accepted (recorded retrospectively) | **Yes** |
|
||||
| [0010](0010-interim-single-host-compose-deployment.md) | Single-host Docker Compose as the interim deployment | Accepted (recorded retrospectively) | **Yes** |
|
||||
|
||||
ADRs 0009 and 0010 were written during the documentation pass described in
|
||||
[DOCUMENTATION_PLAN.md](../DOCUMENTATION_PLAN.md). They record decisions that are
|
||||
unambiguously visible in the implementation but had no ADR. Where the rationale
|
||||
could not be recovered from the repository, they say so rather than inventing
|
||||
one.
|
||||
Reference in New Issue
Block a user