# 00 — Project overview ## Problem domain Clinicians in Vietnam consult the **Dược thư Quốc gia Việt Nam 2018** (Vietnamese National Drug Formulary), a ~1,668-page reference book. Part 2 of that book is 684 drug monographs, each split into up to 19 fixed sections (indications, contraindications, precautions, dosage, interactions, ADRs, …). Looking something up in the paper book is slow and the answer is section-shaped: "what is the paediatric dose of paracetamol" is answered by one specific subsection of one monograph, not by a summary of the drug. This system makes that lookup conversational while keeping the answer bound to the book's own text. ## Who the users are Doctors and pharmacists. The prompts explicitly instruct the model to keep the book's professional terminology and *not* simplify for a lay reader (`apps/ai-service/rag/prompt.py`, rule 6). The UI is Vietnamese-only. There is no authentication, so in the deployed system "user" means anyone who can reach the public URL. See [16-security.md](16-security.md). ## What the system does | Capability | Where | |---|---| | Understand a Vietnamese turn (possibly misspelled, abbreviated, multi-turn) into a structured frame | `rag/understanding.py` | | Resolve drug identity against a 684-drug / 10,164-alias catalog, bounded before the LLM runs | `rag/routing.py` + `rag/understanding.py` | | Retrieve a whole named monograph section deterministically by payload filter | `adapters/qdrant.py::find_by_section` | | Reverse lookup: a condition/indication → drugs whose `chi_dinh` names it | `adapters/qdrant.py::find_by_indication` / `search_indication` | | Two-drug interaction lookup across both monographs | `rag/agent.py::_interaction` | | Ask a clarifying question instead of dumping every dose band | `rag/agent.py`, `rag/prompt.py` rule 7 | | Restate retrieved evidence as structured, individually-cited claims | `rag/prompt.py` `ANSWER_SCHEMA` | | Refuse a generation whose numbers or citations do not trace to the evidence | `rag/grounding.py` | | Refuse a generation a second LLM pass judges unsupported by its cited block | `rag/answer.py::_verify_entailment` | | Return printed-page + physical-page + bbox provenance per citation | `rag/answer.py::_indexed_citations` | | Persist a retrieval trace and per-answer thumbs feedback | `adapters/postgres.py`, `migrations/` | | As-you-type drug-name autocomplete with no model call | `rag/routing.py::complete` | ## What the system deliberately does not do - **Does not answer from Part 1 or Part 3 of the book.** Only printed pages 99–1496 are ingested (`ingestion/segment/detector.py`, `MONOGRAPH_PRINTED_PAGE_START/END`). Questions about the BSA appendix, IV preparation tables, ATC index or the general chapters abstain. - **Does not read numbers out of quarantined tables or 2-D formulas.** A `VERIFY_PDF` decision returns a notice and the source page instead (`rag/answer.py`, `rag/service.py::_decide`). - **Does not rank or recommend.** `prompt.py` rule 10 forbids first-line / treatment-of-choice framing; a condition→drug answer is a factual list. - **Does not answer for non-human subjects.** A keyword scope check abstains on veterinary phrasing (`rag/policy.py`). - **Does not reverse-look-up "which drug *causes* X" or "which drug is contraindicated in X".** Both are explicitly routed to an abstain (`rag/agent.py`, `turn_type == "condition_relation"`). - **Does not fall back to a raw source dump when a configured generator fails.** It abstains with the specific failure reason. - **Does not compute doses.** `rag/calculators.py` implements the book's DuBois BSA formula but **no runtime code calls it** — see [27-technical-debt.md](27-technical-debt.md). ## System boundary ```mermaid flowchart TB CLIN["Doctor / pharmacist
Vietnamese, professional, no account"] SYS["Dược Thư RAG
Grounded Q&A over the 2018 formulary
web + ai-service + ingestion"] BR["AWS Bedrock
Cohere embed-v4 · rerank-v3.5 · Converse"] LE["Let's Encrypt
ACME via Caddy"] GH["GitHub Actions
SSH deploy to EC2"] PDF[/"duoc-thu-quoc-gia-viet-nam-2018.pdf
37 MB, committed in-repo"/] CLIN -->|HTTPS chat| SYS SYS -->|InvokeModel / Converse| BR SYS <-->|certificate issuance| LE GH -->|git reset + compose up --build| SYS PDF -->|offline ingestion, already run| SYS ``` ## Runtime components | Component | State | Notes | |---|---|---| | `apps/ai-service` | **Implemented** | The whole RAG engine. ~9.2k lines Python. | | `apps/web` | **Implemented** | Chat UI + BFF + rate limiting. | | `ingestion` | **Implemented, already run** | ~8.4k lines. Corpus is loaded. | | `packages/ui`, `shared-types`, `api-client`, `config` | **Implemented** | Shared React/TS. `api-client` is not imported by `web`'s live path (see [13](13-frontend-architecture.md)). | | `apps/api-gateway`, `auth-service`, `user-service`, `chat-service` | **Not found** | `README.md` + a 4-line `package.json` each. No source. | | `apps/mobile` | **Not found** | `README.md` + `.gitkeep`. | ## External dependencies | Dependency | Required for | Failure behaviour | |---|---|---| | Qdrant | Every retrieval | Startup fails if the manifest cannot be read; a query-time failure propagates | | AWS Bedrock — embed | Dense/indication fallback search only | `QueryEmbeddingUnavailable` → abstain (`rag/ports.py`) | | AWS Bedrock — Converse | Understanding, generation, entailment | `AnswerGenerationUnavailable` → abstain with a specific reason | | AWS Bedrock — rerank | Ordering on the similarity fallback | `RerankUnavailable` → original order kept (fail-open) | | PostgreSQL | Traces, multi-turn history, feedback | Fail-open: answer still returned, trace id becomes an unpersisted UUID | | Prometheus / Tempo / Grafana | Observability only | Absent = no metrics/traces; service answers unchanged | Credentials for Bedrock come from the EC2 instance's IAM role — no AWS access keys appear in any committed file (`infra/docker/docker-compose.prod.yml` header comment; IAM policy documents in `infra/aws/iam/`). ## Deployment target **Current:** a single EC2 host running Docker Compose behind Caddy at `https://realvuxbaro.me`, deployed by `.github/workflows/deploy.yml` over SSH on push to `master`. **Target (written, never applied):** Helm chart + ArgoCD `Application` manifests under `infra/helm/` and `infra/argocd/`, with three placeholder `TODO`s per environment. See [21-kubernetes-and-argocd.md](21-kubernetes-and-argocd.md).