Files
duocthu/docs
..

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 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 991496). Part 1 general chapters and Part 3 appendices are not ingested.

Architecture at a glance

flowchart LR
    U[Clinician<br/>browser]
    CADDY[Caddy 2<br/>TLS + reverse proxy]
    WEB["web — Next.js 14<br/>chat UI + BFF routes<br/>+ in-memory rate limit"]
    AI["ai-service — FastAPI<br/>RagAgent orchestrator"]
    QD[("Qdrant<br/>duocthu_v1<br/>15,100 points")]
    PG[("PostgreSQL 16<br/>traces · turns · feedback")]
    BR["AWS Bedrock<br/>Cohere embed-v4 · Cohere rerank<br/>Converse generation"]
    ING["ingestion — offline batch<br/>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.

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.pyapp 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
  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

Documentation map

Start here, in order:

  1. 00-project-overview.md — problem, users, boundaries
  2. 02-system-architecture.md — components and what is not built
  3. 03-data-flow.md — the two end-to-end flows in one page

For AI/RAG engineers: 08-query-understanding.md09-retrieval-pipeline.md10-rag-orchestration.md11-generation-and-grounding.md19-rag-evaluation.md. For the corpus itself: 04050607.

For backend engineers: 12-api-architecture.md14-data-stores.md15-configuration.md18-testing.md23-local-development.md.

For frontend engineers: 13-frontend-architecture.md12-api-architecture.md (the response contract) → 16-security.md (rate limiting lives in the frontend today).

For DevOps/SRE: 20-deployment.md22-ci-cd.md17-observability.md24-production-operations.md25-troubleshooting.md21-kubernetes-and-argocd.md (unapplied target state).

For QA: 18-testing.md19-rag-evaluation.md26-known-limitations.md.

Before planning work: 26-known-limitations.md27-technical-debt.md28-roadmap-from-code.md.

Terms: 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/0001adr/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.