6.4 KiB
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.
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_PDFdecision returns a notice and the source page instead (rag/answer.py,rag/service.py::_decide). - Does not rank or recommend.
prompt.pyrule 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.pyimplements the book's DuBois BSA formula but no runtime code calls it — see 27-technical-debt.md.
System boundary
flowchart TB
CLIN["Doctor / pharmacist<br/><i>Vietnamese, professional, no account</i>"]
SYS["<b>Dược Thư RAG</b><br/>Grounded Q&A over the 2018 formulary<br/>web + ai-service + ingestion"]
BR["AWS Bedrock<br/><i>Cohere embed-v4 · rerank-v3.5 · Converse</i>"]
LE["Let's Encrypt<br/><i>ACME via Caddy</i>"]
GH["GitHub Actions<br/><i>SSH deploy to EC2</i>"]
PDF[/"duoc-thu-quoc-gia-viet-nam-2018.pdf<br/>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). |
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 TODOs per
environment. See 21-kubernetes-and-argocd.md.