Files
duocthu/README.md
T

205 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dược Thư RAG — Medical Chatbot Platform
Medical chatbot grounded in the Vietnamese National Drug Formulary
(Dược thư quốc gia Việt Nam 2018), built as a microservices monorepo.
Start with the [canonical documentation set](docs/README.md). It is a compact,
code-verified set covering architecture, PDF ingestion, RAG/chat, local development,
operations, API, configuration, evaluation and documentation governance.
The former numbered `0029` material and historical plans are retained in
[`docs-legacy/`](docs-legacy/) as raw input only. Architecture decisions also remain
there until reviewed. See the canonical
[documentation policy](docs/documentation-policy.md) for source precedence.
> **Status** (2026-08-11): **live in production at
> [realvuxbaro.me](https://realvuxbaro.me)** — a real RAG chatbot over the
> whole formulary, not a scaffold. What exists and what does not:
>
> | Part | State |
> |---|---|
> | `ingestion/` | Done — 15,100 chunks embedded and loaded into Qdrant `duocthu_v1` |
> | `apps/ai-service/` | Done — live grounded RAG (retrieval, generation, grounding, abstention, citations, traces) |
> | `apps/web/` | Done — chat UI with citation/evidence panel |
> | `apps/api-gateway`, `auth-service`, `user-service`, `chat-service` | **Not built** — `README.md` + `package.json` only |
> | `apps/mobile/` | **Not built** — reserved |
> | `infra/docker/` | Done — production runs Compose, including the Prometheus/Grafana/Tempo observability overlay |
> | `infra/helm/medical-chatbot/` | Built and validated as an offline migration kit; **not deployed** to Docker Desktop, k3s or ArgoCD |
> | `infra/k8s`, `terraform`, `argocd` | **Not built yet** — still the target (ADR 0002), not abandoned: the plan is the team's self-hosted Gitea + ArgoCD; the current EC2/Compose setup is an interim stopgap |
>
> Because the gateway and auth services do not exist, `apps/web` talks
> **directly** to `apps/ai-service`; there is no authentication layer. See
> [canonical architecture document](docs/architecture.md) for the implemented
> topology and the explicit status of current, scaffolded and target components.
## Directory map
```
apps/
web/ Next.js frontend (also hosts the BFF route the browser calls)
ai-service/ Python FastAPI — RAG orchestration + AWS Bedrock calls
api-gateway/ NestJS — public entry point, routes to internal services
auth-service/ NestJS — signup/login/JWT
user-service/ NestJS — profile/preferences
chat-service/ NestJS — chat session + message history
mobile/ reserved for a future mobile app
packages/
shared-types/ TS DTOs shared across Node services + web
api-client/ typed HTTP client for web
ui/ shared React components
config/ shared eslint/tsconfig presets
ingestion/ offline batch pipeline: PDF -> monographs -> chunks -> embeddings -> Qdrant
infra/ docker-compose, k8s/Helm, Terraform, CI
docs/ canonical project documentation
docs-legacy/ raw notes, historical plans and ADRs pending review
```
## Prerequisites
- Node.js + pnpm (JS workspace: `apps/web`, `packages/*`; the NestJS service
directories are unbuilt placeholders)
- Python 3.11+ (`apps/ai-service`, `ingestion`)
- Docker (local Postgres + Qdrant via `infra/docker/docker-compose.yml`)
- AWS credentials with Bedrock invoke permission, for anything that generates
an answer. Without them `ai-service` still starts, but every answer abstains
rather than falling back to raw source text.
## Running it locally
```powershell
docker compose -f infra\docker\docker-compose.yml up -d postgres qdrant
cd apps\ai-service
python -m migrate
python -m uvicorn main:app --port 8079 # NOT --reload, see below
```
```powershell
pnpm install
pnpm --filter web dev # http://localhost:3000
```
`ai-service` needs a populated Qdrant collection to serve answers: it verifies
a `duocthu_v1__manifest` sidecar at startup and refuses to run against a corpus
whose sha/model/dimensions do not match. A fresh machine either restores a
Qdrant snapshot or re-runs `ingestion/` (the latter costs real Bedrock spend).
> **Prefer a plain restart over `uvicorn --reload` on Windows here.** The
> reloader has been observed serving the previous code after an edit on this
> project, which makes it hard to tell whether a change took effect.
Tests: `cd apps/ai-service && python -m pytest -q` — 230 pass. `test_api.py` and
`test_live_datastores.py` need Postgres and Qdrant actually running; skip them
with `--ignore` when the stack is down. `apps/web` has **no test setup at all**,
so a green suite says nothing about the frontend — drive it in a browser.
## Observability: Prometheus, Grafana and Tempo
The observability stack is provisioned in the repository and has been deployed
to the production EC2 instance since 2026-08-11.
- **Prometheus** scrapes `/metrics` from `ai-service`. It records request rate
and latency, latency for each RAG stage, routing decisions and reasons,
provider failures, trace-write failures and the existing domain counters.
- **Grafana** is the user interface for dashboards and metric queries. Its
datasource and the Dược Thư dashboard are provisioned automatically.
- **Tempo** stores OpenTelemetry traces. A trace contains the receive,
understanding, routing, retrieval, rerank/evidence, generation,
grounding/entailment, persistence and response stages. Correlation and trace
IDs follow the request from the Next.js BFF into FastAPI.
- **OpenTelemetry Collector** receives spans from `ai-service` and exports them
to Tempo. Grafana exemplars link aggregate latency metrics to an individual
Tempo trace.
For answer lineage, use the three views together:
1. The web citation/evidence panel shows which source chunks, pages and exact
evidence text were selected for the answer.
2. **Grafana -> Explore -> Tempo** shows which pipeline stages ran, their
nesting and timing, the final decision/reason, provider failures and the
persisted trace ID.
3. PostgreSQL table `rag_retrieval_trace` is the durable audit record. It stores
the query, resolved drug, decision/reason, selected citations/evidence,
correlation ID and OpenTelemetry trace ID, so a returned `trace_id` can be
joined to its Tempo trace.
This is provenance and execution tracing, not model chain-of-thought logging.
Full prompts/responses, hidden reasoning, every rejected retrieval candidate
and every ranking score are deliberately not stored today. If deeper debugging
is needed, add bounded audit fields rather than putting sensitive prompt or
patient content into metric labels or span names.
Start the local stack from the repository root:
```powershell
docker compose -f infra\docker\docker-compose.yml up -d prometheus tempo otel-collector grafana
```
Local endpoints:
| Service | Address | Use |
|---|---|---|
| Grafana | `http://localhost:3002` | Dashboards and Explore |
| Prometheus | `http://localhost:9090` | Raw targets, PromQL and metrics |
| Tempo | `http://localhost:3200` | Trace backend; normally queried through Grafana |
| ai-service metrics | `http://localhost:8079/metrics` | Raw OpenMetrics output when ai-service runs on port 8079 |
For the existing EC2 Compose deployment, the optional overlay is
`infra/docker/docker-compose.observability.yml`. It leaves
`docker-compose.prod.yml` unchanged. A deployment, when explicitly approved,
uses both files:
```powershell
docker compose `
-f infra/docker/docker-compose.prod.yml `
-f infra/docker/docker-compose.observability.yml `
up -d
```
Grafana is available directly through Caddy and the existing production TLS
certificate at `https://realvuxbaro.me/grafana/`. Anonymous access is disabled;
sign in with the Grafana admin account. A dedicated
`grafana.realvuxbaro.me` hostname can replace this path after its Namecheap A
record exists.
Grafana and Prometheus are also bound to EC2 loopback only. This keeps both SSH
fallbacks available without exposing their native ports to the Internet:
```powershell
ssh `
-L 3002:127.0.0.1:3002 `
-L 9090:127.0.0.1:9090 `
<ssh-user>@52.0.158.61
```
Keep that session open and use `http://localhost:3002/grafana/` for Grafana or
`http://localhost:9090` for the raw Prometheus UI. The same Grafana account is
used through both the public HTTPS path and the SSH tunnel. The production
password lives in the GitHub Actions secret `GRAFANA_ADMIN_PASSWORD`; do not use
the Compose fallback password in production.
Prometheus intentionally has no public URL. Normally use
**Grafana -> Explore -> Prometheus**; use its SSH tunnel only for low-level
target or PromQL diagnostics. To investigate a slow request, open the
request-latency panel, follow its exemplar/trace link, or paste the returned
`X-Trace-ID` into **Explore -> Tempo**.
## Production
Live at [realvuxbaro.me](https://realvuxbaro.me): a single EC2 `t3.large`
running `infra/docker/docker-compose.prod.yml` (postgres, qdrant, ai-service,
web, Caddy for automatic Let's Encrypt TLS). Bedrock is reached through an IAM
instance role — there are no long-lived AWS keys on the box or in any env file.
Pushing to `master` deploys: `.github/workflows/deploy.yml` SSHes in, resets to
the pushed commit, rebuilds `ai-service`/`web`, reconciles the observability
containers, reloads Caddy, runs migrations and verifies health, metrics and an
exact request trace. Postgres and Qdrant data survive deploys because they live
in named volumes rather than the containers.
This is **interim infrastructure**, not the end state. The intended target is
still the team's self-hosted **Gitea** (company domain) plus their **ArgoCD**
instance, per `docs-legacy/adr/0002-argocd-gitops.md` — that work is *not started*,
not cancelled. Until it is deliberately started, the project stays on private
GitHub, and the team's existing `git.vinmec.tech/ai-team/gitops` repository is
reference-only: never push this project into it.