Fix migration workflow: upload as artifact instead of scp to practice EC2

This commit is contained in:
2026-08-13 11:14:25 +07:00
parent 7ebbe1f309
commit a4819b8653
51 changed files with 6830 additions and 8 deletions
+82
View File
@@ -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.
+20
View File
@@ -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.