# Documentation Reverse-engineered from the code in this repository. Every claim here traces to a file, a command, or an artifact on disk — see [DOCUMENTATION_PLAN.md](DOCUMENTATION_PLAN.md) for the method and for what was not verified. ## What this system is A Vietnamese-language question-answering system over the **Dược thư Quốc gia Việt Nam 2018** (Vietnamese National Drug Formulary), for doctors and pharmacists. A user asks a drug question in Vietnamese; the system resolves what was asked, retrieves the exact monograph section from a vector store, has an LLM restate it, verifies that restatement against the retrieved text, and returns it with printed-page citations — or refuses. Two things distinguish it from a generic RAG app, and both are enforced in code: - **Retrieval decides what is true; generation only decides how it reads.** A generated answer is discarded unless every number in it appears verbatim in the specific evidence block it cites (`rag/grounding.py`) *and* a second LLM pass confirms the cited block actually says it (`rag/answer.py`). - **Tables and formulas are quarantined, not linearised.** Content whose numbers could not be reliably reconstructed from the PDF is never embedded as prose and never restated; it is surfaced as "check the source page". Scope boundary: the corpus is **Part 2 monographs only** (printed pages 99–1496). Part 1 general chapters and Part 3 appendices are not ingested. ## Architecture at a glance ```mermaid flowchart LR U[Clinician
browser] CADDY[Caddy 2
TLS + reverse proxy] WEB["web — Next.js 14
chat UI + BFF routes
+ in-memory rate limit"] AI["ai-service — FastAPI
RagAgent orchestrator"] QD[("Qdrant
duocthu_v1
15,100 points")] PG[("PostgreSQL 16
traces · turns · feedback")] BR["AWS Bedrock
Cohere embed-v4 · Cohere rerank
Converse generation"] ING["ingestion — offline batch
PDF → chunks → vectors"] PDF[/"duoc-thu-quoc-gia-viet-nam-2018.pdf"/] U --> CADDY --> WEB --> AI AI --> QD AI --> PG AI --> BR PDF --> ING --> QD ING --> BR ``` The `api-gateway`, `auth-service`, `user-service` and `chat-service` directories in `apps/` contain **only** a `README.md` and a `package.json`. There is no gateway, no authentication and no chat-service in the request path; `web` calls `ai-service` directly. See [02-system-architecture.md](02-system-architecture.md). ## Main technology stack | Layer | Technology | Evidence | |---|---|---| | Frontend | Next.js 14 (App Router), React 18, Tailwind, framer-motion | `apps/web/package.json` | | Backend | Python 3.12, FastAPI, Pydantic Settings, uvicorn | `apps/ai-service/pyproject.toml`, `Dockerfile` | | Vector store | Qdrant (cosine, 1024-d) | `adapters/qdrant.py`, `ingestion/load/` | | Relational | PostgreSQL 16 (`psycopg` 3) | `adapters/postgres.py`, `migrations/` | | Embedding | `cohere.embed-v4:0` on AWS Bedrock | `adapters/embedding.py`, `ingestion/embed/bedrock_cohere.py` | | Generation | Bedrock Converse API (model id is config) | `adapters/bedrock_converse.py` | | Rerank | `cohere.rerank-v3-5:0` on Bedrock | `adapters/bedrock_converse.py` | | PDF parsing | PyMuPDF (`fitz`), pdfplumber for tables only | `ingestion/extract/`, `ingestion/tables/` | | Observability | Prometheus, OpenTelemetry → OTel Collector → Tempo, Grafana | `rag/telemetry.py`, `infra/docker/` | | Runtime | Docker Compose on a single EC2 host, Caddy for TLS | `infra/docker/docker-compose.prod.yml` | | Monorepo | pnpm workspaces + Turborepo (JS side only) | `pnpm-workspace.yaml`, `turbo.json` | No RAG framework is used. There is no LangChain and no LlamaIndex anywhere in the dependency set — the orchestration is hand-written in `rag/agent.py`. ## Core runtime services | Service | Language | Entrypoint | Port | |---|---|---|---| | `ai-service` | Python | `apps/ai-service/main.py` → `app` | 8000 | | `web` | TypeScript | `apps/web/app/` (Next.js) | 3000 | | `caddy` | — | `infra/docker/Caddyfile` | 80/443 | | `ingestion` | Python | `python -m ingestion.cli`, `python -m ingestion.load.run` | offline, no port | ## Main data stores | Store | Holds | Live-path role | |---|---|---| | Qdrant `duocthu_v1` | 15,100 chunk points + payload | Every retrieval | | Qdrant `duocthu_v1__manifest` | One point: corpus sha, model id, dimensions | Startup gate (`bootstrap.py`) | | PostgreSQL | `rag_retrieval_trace`, `rag_conversation_turn`, `rag_answer_feedback` | Traces + multi-turn history; both fail-open | | Local disk | `chunks.jsonl`, `monographs.jsonl`, embedding cache | Offline pipeline only | Redis appears in `infra/docker/docker-compose.yml` (local dev) and in the pre-existing architecture document. **Nothing in the codebase imports a Redis client.** It is not deployed in production and not read or written by any code. ## Main pipelines 1. **Ingestion (offline)** — PDF → spans → monographs → chunks → embeddings → Qdrant. Seven CLI subcommands plus a separate embed/load entrypoint. Has already been run; re-running the embed step costs real Bedrock spend. → [04-ingestion-pipeline.md](04-ingestion-pipeline.md) 2. **Query (live)** — HTTP → understanding LLM call → deterministic route → Qdrant retrieval → generation LLM call → deterministic grounding → entailment LLM call → citations → response. → [10-rag-orchestration.md](10-rag-orchestration.md) ## Documentation map **Start here, in order:** 1. [00-project-overview.md](00-project-overview.md) — problem, users, boundaries 2. [02-system-architecture.md](02-system-architecture.md) — components and what is *not* built 3. [03-data-flow.md](03-data-flow.md) — the two end-to-end flows in one page **For AI/RAG engineers:** [08-query-understanding.md](08-query-understanding.md) → [09-retrieval-pipeline.md](09-retrieval-pipeline.md) → [10-rag-orchestration.md](10-rag-orchestration.md) → [11-generation-and-grounding.md](11-generation-and-grounding.md) → [19-rag-evaluation.md](19-rag-evaluation.md). For the corpus itself: [04](04-ingestion-pipeline.md) → [05](05-document-parsing.md) → [06](06-document-model-and-chunking.md) → [07](07-indexing-and-storage.md). **For backend engineers:** [12-api-architecture.md](12-api-architecture.md) → [14-data-stores.md](14-data-stores.md) → [15-configuration.md](15-configuration.md) → [18-testing.md](18-testing.md) → [23-local-development.md](23-local-development.md). **For frontend engineers:** [13-frontend-architecture.md](13-frontend-architecture.md) → [12-api-architecture.md](12-api-architecture.md) (the response contract) → [16-security.md](16-security.md) (rate limiting lives in the frontend today). **For DevOps/SRE:** [20-deployment.md](20-deployment.md) → [22-ci-cd.md](22-ci-cd.md) → [17-observability.md](17-observability.md) → [24-production-operations.md](24-production-operations.md) → [25-troubleshooting.md](25-troubleshooting.md) → [21-kubernetes-and-argocd.md](21-kubernetes-and-argocd.md) (unapplied target state). **For QA:** [18-testing.md](18-testing.md) → [19-rag-evaluation.md](19-rag-evaluation.md) → [26-known-limitations.md](26-known-limitations.md). **Before planning work:** [26-known-limitations.md](26-known-limitations.md) → [27-technical-debt.md](27-technical-debt.md) → [28-roadmap-from-code.md](28-roadmap-from-code.md). Terms: [29-glossary.md](29-glossary.md). ## Pre-existing documents in this directory `architecture.md`, `progress-log.md`, `pdf-parsing-outlier-catalog.md`, `document-profile.md`, `verification-strategy.md`, the dated plan/audit files, and `adr/0001`–`adr/0008` predate this set. They are kept for their reasoning and their empirical measurements. Where they describe current behaviour, they have drifted in places — the drift is listed in [26-known-limitations.md](26-known-limitations.md#documentationcode-discrepancies).