Fix migration workflow: upload as artifact instead of scp to practice EC2

This commit is contained in:
2026-08-13 11:14:25 +07:00
parent 7ebbe1f309
commit a4819b8653
51 changed files with 6830 additions and 8 deletions
+138
View File
@@ -0,0 +1,138 @@
# Claude ownership claim — 2026-08-12
Scope respected per `WORK_SPLIT_2026-08-10.md`: Claude owns `infra/**`,
Dockerfiles, deployment config and `.github/**`; Codex owns
`apps/ai-service/rag/**` and the retrieval adapters. **Nothing under `rag/`,
`adapters/`, or `ingestion/` was modified.**
## Context
The owner asked for a full reverse-engineered documentation rewrite, then for
the highest-value findings to be fixed. `docs/00``29` + `docs/README.md` +
`docs/DOCUMENTATION_PLAN.md` + `docs/adr/README.md` + ADR 0009/0010 are new and
are now the current-state reference; the pre-existing documents in `docs/` were
deliberately kept, not deleted or edited.
Everything below was verified on local before being written up. **Nothing was
pushed or deployed.**
## Files changed
| File | Status | Why |
|---|---|---|
| `.github/workflows/ci.yml` | **new** | No test gate existed at all — `deploy.yml` went from `push: master` straight to SSH + rebuild. 555 tests had never run in CI. |
| `apps/ai-service/tests/conftest.py` | **new** | `pytest tests -q` failed at *collection*, not at a test, on any machine without Qdrant. |
| `apps/ai-service/.env.example` | **new** | No env template existed anywhere; `config.py` was the only record of ~22 settings. |
| `apps/web/middleware.ts` | edited | `/api/pdf` (37 MB per request) and `/api/feedback` had **no** rate limit — `matchRules` only had entries for `/api/chat` and `/api/suggest`, so everything else fell through to `NextResponse.next()`. |
| `.gitignore` | edited | ~25 untracked `.codex-*.log`/`.png` scratch files at the repo root and in `apps/ai-service/`. |
### `tests/conftest.py` — the detail that matters to Codex
`main.py` calls `build_runtime(get_settings())` at module scope and
`tests/test_api.py` imports `main`, so with `EMBEDDING_PROVIDER=cohere-v4` (the
code default, and what a developer `.env` sets) pytest opens a `QdrantClient`
during collection. The conftest does one thing:
```python
os.environ.setdefault("EMBEDDING_PROVIDER", "disabled")
```
`setdefault`, not assignment — an explicit
`EMBEDDING_PROVIDER=cohere-v4 pytest ...` against a local Qdrant still behaves
exactly as before. Verified both ways locally.
## Local verification (nothing pushed)
| Check | Before | After |
|---|---|---|
| `cd apps/ai-service && pytest tests -q` (no env var) | **collection ERROR**`ResponseHandlingException`, Qdrant refused | **278 passed, 6 skipped** in 2.6 s |
| `EMBEDDING_PROVIDER=cohere-v4 pytest tests/test_api.py -q` | collection error | collection error — override still wins, as intended |
| `cd apps/ai-service && ruff check .` | passed | passed |
| `cd ingestion && pytest tests -q` | 277 passed / 12 skipped | **277 passed, 12 skipped** (unchanged) |
| `next lint` (apps/web) | clean | clean |
| `next build` (apps/web) | exit 0 | exit 0 |
| `GET /api/pdf` headers | no rate-limit header at all | `x-ratelimit-limit: 30` |
| 64 × `POST /api/feedback` | all reached the handler | 60 × 400 (reached handler), then **4 × 429 with `Retry-After: 60`** |
| `GET /api/chat`, `/api/suggest` headers | 12 / 120 | 12 / 120 — unchanged |
## Deliberately NOT done — production risk
**`infra/docker/docker-compose.prod.yml` was left alone.** The committed
default credential (`POSTGRES_USER: duoc_thu` / `POSTGRES_PASSWORD: duoc_thu`,
mirrored as `secret.postgresPassword` in the Helm values) is a real finding, but
parameterising it as `${POSTGRES_PASSWORD:-duoc_thu}` would be a half-measure
with non-zero risk: the default still sits in Git, and the deploy runs
`sudo -E docker compose`, so an unrelated `POSTGRES_PASSWORD` in the host's
environment would give the postgres container a password that `.env.prod`'s DSN
does not match. Fixing this properly is a coordinated rotation (new password →
`.env.prod` DSN → `ALTER ROLE`), which is the owner's call, not a drive-by edit.
**`ci.yml` does not gate `deploy.yml`.** They are independent workflows, so a
red CI currently does not stop a deploy. Wiring `deploy` to `needs:` the CI jobs
is the actual fix for the gap, but it changes deploy behaviour — a flaky job
would block a production deploy — so it is left for the owner to approve as a
one-line follow-up.
**`ruff` is not run over `ingestion/`.** `ingestion/pyproject.toml` declares no
`[tool.ruff]` section, so ruff applies its full default rule set and reports
~426 pre-existing findings. Adding the same lint config `apps/ai-service` uses
is follow-up work; nothing was silenced or auto-fixed.
## Handoff to Codex — findings inside `rag/**`, not touched
All verified by reading the code, several by import-graph grep. Full write-ups
with file references are in `docs/26-known-limitations.md` and
`docs/27-technical-debt.md`.
1. **`rag/fusion.py`, `rag/expansion.py`, `rag/calculators.py` have zero runtime
callers** — referenced only by their own tests. `calculators.py`
(`body_surface_area_m2`, the book's DuBois formula) was written specifically
so a BSA dose would be *computed* rather than read off a quarantined table;
that wiring never happened. (docs/27 D-12)
2. **`search_lexical` issues Qdrant `MatchText` against the `text` payload
field, which is not in `INDEXED_PAYLOAD_FIELDS`**
(`ingestion/load/models.py`). Qdrant needs an explicit full-text index for
`MatchText`. If the deployed collection has no such index, the neighbour
pooling and the patient-safety facet routes are effectively relying on the
Python re-scoring of whatever the scroll returned. **Worth checking the live
collection's index list before changing anything.** (docs/27 D-08)
3. **Four Prometheus metrics are registered but never incremented**
`duocthu_loop_retrieval_rounds_total`, `duocthu_loop_refined_total`,
`duocthu_loop_repaired_total`, `duocthu_followup_inherited_total`. Leftovers
of the ADR 0007 loop that ADR 0008 replaced. They will always read 0, which
on a dashboard reads as "this never happens" rather than "this is not
measured". (docs/27 D-13)
4. **`SECTION_ORDER` in `rag/sections.py` has 18 entries; `ten_thuong_mai` is
missing** while `SECTION_KEYS` and the corpus both have 19. In
`find_by_drug`, that chunk sorts to the end via the
`order.get(..., len(order))` default instead of into its book position.
(docs/27 D-25)
5. **`RagAgent._last_frame` and `_clarify_streak` are still in-process dicts.**
Only `_history` got `PostgresConversationStore`. Running more than one
`ai-service` replica silently degrades multi-turn quality — the prior-frame
merge and the clarify circuit breaker both become per-replica — and nothing
detects it. (docs/27 D-04)
6. **No evaluation runner exists.** `Golden Dataset/*.csv` (209 labelled rows)
is read by no code, and `rag/condition_evaluation.py` +
`rag/evaluation.py` implement complete metric summaries with no production
caller. `rag/run_eval.py` measures the in-memory retriever, not Qdrant.
Wiring `evals/condition_to_drug_v1.jsonl` through the live service into
`summarize_condition_outcomes` is existing tested code, not new design.
(docs/19, docs/27 D-10)
7. **Optional retriever capabilities are discovered with `getattr`, not
declared in `ports.py`** — `find_by_indication`, `search_indication`,
`search_lexical`, `find_by_drug`. A retriever missing one silently disables a
whole route. (docs/27 D-14)
## Not modified
`apps/ai-service/rag/**`, `apps/ai-service/adapters/**`,
`apps/ai-service/routers/**`, `apps/ai-service/main.py`, `bootstrap.py`,
`config.py`, `ingestion/**`, `infra/**`, both Dockerfiles, `packages/**`, and
every pre-existing file in `docs/`.
@@ -0,0 +1,604 @@
# Codex handoff — Condition / Disease → Medication Q&A — 2026-08-11
Đây là memory/handoff bền vững cho phần mở rộng chatbot Dược thư từ tra cứu
theo thuốc sang tra cứu bệnh/condition → các thuốc có bằng chứng chỉ định, kèm
đánh giá an toàn theo dữ kiện người bệnh. Đọc file này trước khi tiếp tục task.
## Trạng thái ngắn gọn
- Feature đã được audit, thiết kế, implement, test local và deploy lên production
AWS cá nhân.
- Production đang chạy commit merge `f4b84fb` và GitHub Actions run
`31471486789` đã thành công.
- Public URL: `https://realvuxbaro.me`.
- Production battery đã xác nhận **20/20 case unique đầu tiên pass sau fix**.
Còn **40 case chưa chạy** vì chủ dự án yêu cầu tạm ngưng để chuyển task.
- Feedback người dùng đã deploy và đã lưu thành công một feedback production.
- Không đụng vào Gitea, ArgoCD, k3s hay hạ tầng của team. Chỉ dùng GitHub cá
nhân và EC2/Docker Compose cá nhân hiện hữu.
- Từ thời điểm handoff này: không sửa/commit/deploy thêm cho feature cho tới khi
chủ dự án yêu cầu tiếp tục.
## Production topology và đường deploy thực tế
Production path đã audit từ code, không suy đoán:
```text
GitHub master push
-> .github/workflows/deploy.yml
-> appleboy SSH action
-> EC2 ~/app
-> git reset --hard origin/master
-> Docker Compose build/restart
-> migration
-> health/ready/web/condition smoke
-> Prometheus/Tempo/Grafana checks
```
Các file production chính:
- `.github/workflows/deploy.yml`
- `infra/docker/docker-compose.prod.yml`
- `infra/docker/docker-compose.observability.yml`
- `infra/docker/Caddyfile`
Production request path đã xác nhận:
```text
Caddy
-> Next.js POST /api/chat
-> FastAPI POST /v1/rag/query
-> RagAgent / query understanding
-> RetrievalService / Qdrant
-> grounded generation + entailment
-> citation/provenance
-> PostgreSQL trace
```
## Những gì đã làm hôm nay
### 1. Audit hiện trạng trước khi sửa
Audit chi tiết nằm tại:
- `docs/condition-to-drug-audit-and-design.md`
Các phát hiện quan trọng:
- Chatbot cũ chủ yếu drug-centric.
- Có primitive reverse-indication retrieval nhưng query condition thực tế thường
bị route sang clarification và không tạo danh sách thuốc.
- Qdrant collection local `duocthu_v1` có 15.100 points, vector cosine 1.024
chiều.
- Chunk có `drug_id`, `drug_name`, `section_key`, section display name, text,
physical/printed page ranges, attachment/quarantine metadata.
- Ingestion hiện không phát `parent_id`; parent hydration có trong AI service
nhưng không phải hierarchy đang hoạt động của corpus hiện tại.
- Provenance hiện tới chunk/page/attachment region, chưa có character span.
- Dense, lexical và reranker primitives tồn tại; RRF/hybrid module chưa nằm trên
live reverse-indication path.
- Grounding cũ đã có structured claims, citation verification, numeric grounding
và entailment check; thiếu candidate-set guard deterministic cho drug list.
- Raw conversation history bền trong PostgreSQL; normalized frame vẫn in-memory
theo process/worker.
### 2. Structured clinical query/context
Đã tạo `apps/ai-service/rag/clinical.py` với các contract nhỏ, không chứa map
bệnh → thuốc:
- `ConditionQuery`
- `PatientContext`
- renal/hepatic contexts
- condition relation
- case context action
- `MedicationCandidateAssessment`
- candidate status
Normalizer chỉ canonicalize alias chắc chắn như THA/cao huyết áp/tăng huyết áp
và gout/gút. Các abbreviation mơ hồ không được tự mở rộng.
Patient context giữ structured fields khi có:
- tuổi, giới, cân nặng;
- bệnh chính và bệnh nền;
- dị ứng, ADR trước đó;
- thuốc đang dùng;
- thai kỳ/cho con bú;
- CKD/eGFR/CrCl/creatinine;
- suy gan/Child-Pugh/AST/ALT/bilirubin;
- labs và điều trị trước đó.
Không invent field thiếu và không ép general query qua full patient pipeline.
### 3. Intent/routing và ambiguity/relation guard
Đã mở rộng query understanding/routing để phân biệt:
- drug information/overview;
- drug → condition;
- condition → drug;
- dosage;
- contraindication;
- interaction;
- reverse relation khác indication;
- ambiguous/out of scope.
Guard deterministic đã thêm cho:
- `THA dùng thuốc nào?`, `cao huyết áp...`, `gout...`;
- bare broad conditions: viêm gan, ung thư, nhiễm trùng/nhiễm khuẩn;
- relation confusion như `thuốc nào gây tăng huyết áp?`;
- `thuốc nào chống chỉ định ở bệnh nhân gout?`;
- named-drug queries như `Paracetamol có tác dụng gì?`
`probenecid có dùng được không?`.
Broad conditions chỉ clarify khi subtype thực sự làm thay đổi đáng kể câu trả
lời. Tăng huyết áp general không bị hỏi tuổi/cân nặng/labs vô ích.
### 4. Indication-only reverse retrieval
Đã sửa reverse lookup theo đúng semantics:
```text
condition
-> chỉ search section_key=chi_dinh
-> lexical phrase first
-> dense fallback trong chi_dinh nếu lexical không match
-> group chunk hits theo drug_id
-> drug-level rank/cap
-> tối đa 2 evidence chunk/drug
```
Không tạo candidate từ chống chỉ định, ADR, thận trọng hay tương tác. Số chunk
không được dùng làm số phiếu để rank thuốc. General response hiện cap 8
candidates để không trả danh sách 30 thuốc.
### 5. Patient-specific second stage
Khi có dữ kiện bệnh nhân, stage 2 chỉ chạy cho top candidates từ indication:
- interaction với current medications;
- contraindication/precaution có match bệnh nền/dị ứng/labs;
- renal/hepatic dose context;
- pregnancy/breastfeeding sections;
- age considerations.
Hiện cap 2 patient candidates để kiểm soát latency/evidence explosion. Interaction
evidence chỉ được chọn nếu chunk thực sự nhắc thuốc đang dùng; điều này đã sửa
false-positive interaction trong quá trình manual testing.
Status hiện dùng các mức tương đương:
- supported;
- supported with caution;
- requires additional information;
- insufficient evidence.
Code không tự kết luận `CONTRAINDICATED` chỉ từ một lexical hit và không tự tạo
dose adjustment nếu corpus không support.
### 6. Grounding và hallucination guard
Đã thêm candidate-set constraint deterministic:
- list-mode claim phải có `drug_id`;
- `drug_id` phải thuộc candidate set từ retriever;
- citation của claim phải trỏ tới evidence của đúng drug đó;
- drug ngoài candidate set bị reject;
- mọi drug final phải có supporting evidence/citation.
Prompt đã khóa distinction:
- Dược thư chứng minh thuốc có chỉ định;
- không được tự suy thành first-line, preferred, treatment of choice hay standard
regimen;
- không fallback ngầm sang kiến thức parametric nếu corpus không đủ bằng chứng.
Trusted metadata label (`drug_id`, drug name, section) được đưa vào evidence
prompt để monograph tự xưng bằng class name vẫn entail đúng tên thuốc nguồn.
### 7. Citation/provenance contract
Citation API/frontend hiện carry trực tiếp:
- drug id/name;
- section key/title;
- source document;
- chunk id;
- printed/physical pages;
- attachment/source crop nếu có.
Frontend không còn phải suy toàn bộ provenance chỉ bằng cách split chunk id.
### 8. End-user feedback
Đã thêm:
- migration `apps/ai-service/migrations/004_rag_answer_feedback.sql`;
- PostgreSQL upsert feedback theo trace;
- `POST /v1/rag/feedback`;
- Next BFF `POST /api/feedback`;
- UI component thumbs up/down và optional comment dưới assistant answer;
- validation trace id/rating/comment/conversation id.
Production feedback smoke đã lưu thành công:
- trace id: `3f7687d0-6857-4eb9-9954-ac629b0ec611`
- feedback id: `d2b9777e-8e9f-458a-870a-dc3f01cf740c`
- status: `saved`
### 9. Production deploy smoke cho feature
Workflow GitHub đã thêm condition smoke thật sau health/ready:
```text
Đợt gout cấp có thuốc nào được Dược thư ghi chỉ định?
```
Deploy chỉ xanh nếu response:
- `decision=answerable`;
- có citation `section_key=chi_dinh`.
Nếu request fail, workflow in 200 dòng log gần nhất của AI service để debug.
## Lỗi phát hiện hôm nay và cách xử lý
### A. Fixture integration cũ không còn đúng semantics
Biểu hiện:
- integration test gửi bare drug nhưng fake frame là `drug_attribute` với
`attribute=None`;
- router mới đúng ra hỏi người dùng muốn tra mục nào;
- test vẫn đòi `answerable`.
Fix:
- đổi fake frame sang `drug_overview` để test tiếp tục kiểm tra đúng mục tiêu
end-to-end retrieval/Qdrant/Postgres, không nới lỏng production router.
### B. Frontend typecheck và Next build chạy song song tranh chấp `.next`
Biểu hiện:
- `tsc` báo mất `.next/types/...` khi `next build` đồng thời tạo/xóa generated
directory.
Kết luận/fix:
- lỗi orchestration test, không phải source code;
- chạy tuần tự: shared tsc → Next build → web tsc;
- cả ba đều pass.
### C. Production-only 500 cho gout cấp/gout mạn
Biểu hiện:
- production case G09/G10 trả frontend fallback `upstream_error` sau 57 giây;
- local cùng Bedrock/Qdrant pass;
- generic gout query vẫn pass;
- subtype query rơi vào dense indication fallback.
Quá trình chẩn đoán:
1. Retry production lặp lại lỗi, nên không coi là provider transient.
2. Thêm condition smoke vào personal-AWS deploy workflow.
3. Workflow run `31471207908` cố ý fail và in stack trace thật.
4. Stack trace xác nhận:
```text
AttributeError: 'QdrantClient' object has no attribute 'search'
```
Root cause:
- production Docker cài qdrant-client 1.x mới, đã bỏ `QdrantClient.search`;
- local đang dùng 1.x cũ còn method này;
- constraint project `qdrant-client>=1.7,<2` cho phép cả hai;
- lexical-hit queries không đi qua code lỗi nên lỗi chỉ lộ ở dense fallback.
Fix:
- thêm compatibility helper trong `adapters/qdrant.py`;
- ưu tiên API mới `query_points(query=vector, ...)`;
- fallback sang legacy `search(query_vector=vector, ...)` cho local/older client;
- áp dụng cho cả drug-scoped dense search và indication dense fallback;
- thêm fake production client chỉ có `query_points` để regression test đúng lỗi.
Kết quả:
- deploy run `31471486789` pass;
- in-container condition smoke pass;
- public production retry G09/G10: 2/2 pass.
### D. False interaction evidence khi current medication không có trong chunk
Biểu hiện trong local manual testing:
- candidate có thể nhận interaction evidence chỉ vì CKD/condition terms match,
dù chunk không nhắc current medication.
Fix:
- tách interaction query khỏi warning/dose facets;
- interaction chunk chỉ được nhận nếu thật sự match current medication text.
### E. Patient evidence match quá rộng
Biểu hiện:
- từ generic như `chức năng` làm methyldopa bị gắn warning sai.
Fix:
- `_patient_context_matches` yêu cầu clinical anchor thật: renal/hepatic term,
raw disease/allergy/lab, thay vì generic token overlap.
### F. Single-monograph entailment không ổn định
Biểu hiện:
- Warfarin/Colchicin evidence có thể chỉ nói class, không lặp tên monograph;
- entailment judge đôi khi reject claim tên thuốc.
Fix:
- gắn trusted drug/section metadata label vào mọi prompt evidence block.
### G. Patient prompt invent/echo số từ user context
Biểu hiện:
- age/eGFR/G4 từ query có thể bị model biến thành unsupported numeric claim.
Fix:
- patient generation query được sanitize;
- non-dose condition list cấm số khi câu hỏi không yêu cầu số liệu;
- number grounding vẫn fail closed.
## PDF source đã mở và kiểm tra trực quan
Source:
- `ingestion/data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf`
- 1.668 PDF pages.
Do máy không có Poppler, các trang được render bằng PyMuPDF rồi mở ảnh để kiểm
tra khách quan. Các trang đã xem:
- physical 162 / printed 163 — Alopurinol;
- physical 375 / printed 376 — Cefuroxim;
- physical 460 / printed 461 — Colchicin;
- physical 589 / printed 590 — Entecavir;
- physical 719 / printed 720 — Gemifloxacin;
- physical 876 / printed 877 — Lamivudin;
- physical 967 / printed 968 — Methyldopa;
- physical 1181 / printed 1182 — Probenecid;
- physical 12211223 / printed 12221224 — Quinapril.
Đã đối chiếu trực quan:
- colchicin: đợt gout cấp; chống chỉ định suy thận/suy gan nặng;
- allopurinol: gout mạn, không phải điều trị cơn cấp;
- probenecid: gout mạn; chống chỉ định khi CrCl thấp theo sách;
- entecavir/lamivudine: viêm gan B mạn;
- gemifloxacin: viêm phổi mắc phải cộng đồng mức nhẹ-vừa;
- methyldopa: tăng huyết áp và thai kỳ; có lưu ý thận và interaction text;
- quinapril: tăng huyết áp, cảnh báo/điều chỉnh liên quan chức năng thận.
## Test/evaluation đã chạy
### Local gates trước deploy feature
```text
python -m pytest -q
277 passed, 6 skipped
RUN_INTEGRATION=1 python -m pytest -q tests/test_live_datastores.py
6 passed
python -m ruff check .
All checks passed
corepack pnpm --filter @duoc-thu/shared-types exec tsc --noEmit
passed
corepack pnpm --filter @duoc-thu/web build
passed; /api/feedback included in production routes
corepack pnpm --filter @duoc-thu/web exec tsc --noEmit
passed
```
### Local gates sau Qdrant production hotfix
```text
python -m pytest -q
278 passed, 6 skipped
RUN_INTEGRATION=1 python -m pytest -q tests/test_live_datastores.py
6 passed
python -m ruff check .
All checks passed
```
### Production runs
- `31470380713` — commit `1342571` — success; initial feature + feedback deploy.
- `31471207908` — commit `7db6289` — failure by newly-added condition smoke;
exposed Qdrant API incompatibility. Đây là diagnostic failure có chủ đích,
không phải trạng thái cuối.
- `31471486789` — commit `f4b84fb` — success; Qdrant hotfix, condition smoke,
health/ready, web, migration, Prometheus, Tempo và Grafana đều pass.
## Production manual battery: trạng thái thật
Fixture:
- `apps/ai-service/evals/production_manual_60.jsonl`
- runner: `apps/ai-service/scripts/run_manual_battery.py`
- runner ghi raw response, deterministic checks và elapsed time; không dùng một
overall LLM judge.
Artifacts hiện có:
- `tmp/prod-manual-01-10.jsonl`
- 10 cases;
- initial 8 pass, G09/G10 fail do production Qdrant 500.
- `tmp/prod-retry-fixed-g09-g10.jsonl`
- G09/G10 retry sau hotfix: 2/2 pass.
- `tmp/prod-manual-11-20.jsonl`
- 10/10 pass;
- IDs: G12, G13, G14, G15, A01, A02, A03, A04, A05, A06.
- `tmp/prod-feedback-smoke.jsonl`
- G01 smoke: pass.
Kết luận production battery tới lúc pause:
- unique case 120: **20/20 pass sau fix/retry**;
- còn case 2160: **chưa chạy**;
- không được báo feature là đã hoàn tất full 60/60 cho tới khi chạy nốt;
- khi resume, bắt đầu từ `--start 21`, dùng run id mới;
- giữ conversation cases 5560 trong cùng một run/chunk để history không bị
tách.
Command tiếp tục gợi ý:
```powershell
cd D:\VSF-DUOCTHU\apps\ai-service
python scripts/run_manual_battery.py `
--base-url https://realvuxbaro.me `
--target web `
--output ../../tmp/prod-manual-21-30.jsonl `
--start 21 --limit 10 `
--run-id prod-resume-<timestamp>
```
Sau đó chạy 3140, 4150, và 5160. Retry provider transient riêng nhưng phải
giữ cả initial result và retry result; lỗi logic phải fix, redeploy và chạy lại
case liên quan.
## Relevant GitHub PRs/commits
- PR #1 — grounded condition medication Q&A + feedback.
- feature commit `62a76a9`
- merge commit `1342571`
- PR #2 — production condition retrieval smoke.
- commit `a30598b`
- merge commit `7db6289`
- PR #3 — modern Qdrant vector query compatibility.
- commit `22d86fe`
- merge commit `f4b84fb`
Không dùng Gitea/ArgoCD cho bất kỳ PR/deploy nào.
## Files chính đã tạo/sửa
Core AI:
- `apps/ai-service/rag/clinical.py`
- `apps/ai-service/rag/condition_evaluation.py`
- `apps/ai-service/rag/understanding.py`
- `apps/ai-service/rag/agent.py`
- `apps/ai-service/rag/service.py`
- `apps/ai-service/rag/answer.py`
- `apps/ai-service/rag/prompt.py`
- `apps/ai-service/rag/models.py`
- `apps/ai-service/rag/instrumentation.py`
- `apps/ai-service/adapters/qdrant.py`
- `apps/ai-service/adapters/postgres.py`
- `apps/ai-service/routers/rag.py`
- `apps/ai-service/main.py`
Feedback/UI/contracts:
- `apps/ai-service/migrations/004_rag_answer_feedback.sql`
- `apps/web/app/api/feedback/route.ts`
- `apps/web/app/_components/AnswerFeedback.tsx`
- `apps/web/app/_components/ChatPanel.tsx`
- `apps/web/app/api/chat/route.ts`
- `packages/shared-types/src/dto/chat.ts`
Tests/evals:
- `apps/ai-service/tests/test_clinical_condition_flow.py`
- `apps/ai-service/tests/test_condition_evaluation.py`
- updates to API/citation/Qdrant/retrieval/live datastore tests
- `apps/ai-service/evals/condition_to_drug_v1.jsonl`
- `apps/ai-service/evals/production_manual_60.jsonl`
- `apps/ai-service/scripts/run_manual_battery.py`
Docs/deploy:
- `docs/condition-to-drug-audit-and-design.md`
- `.github/workflows/deploy.yml`
## Các hạn chế/rủi ro còn lại
Đây là các điểm chưa hoàn tất hoặc cố ý nằm ngoài scope, không được quên ở phiên
sau:
1. **Production battery mới 20/60 unique cases.** 40 case gồm patient-specific,
allergy, pregnancy, renal, drug-centric regression và conversation history
vẫn phải chạy.
2. **Dược thư không phải guideline.** Hệ thống chỉ được nói có indication, không
được coi là bằng chứng first-line/preferred/standard regimen.
3. **Patient candidate cap hiện là 2.** Đây là giới hạn latency/evidence, không
phải clinical ranking đầy đủ.
4. **Condition normalization cố ý bảo thủ.** Chưa phải terminology service/ICD
normalizer toàn diện; không thêm disease→drug dictionary.
5. **Normalized conversation state còn in-memory.** Raw history bền ở Postgres,
nhưng worker restart hoặc multi-worker có thể làm mất normalized last frame và
phải reconstruct từ raw history.
6. **Không có active parent-child hierarchy trong corpus hiện tại.** Code có
hydration compatibility nhưng ingestion không phát `parent_id`.
7. **Provenance chưa có character span.** Hiện trace tới chunk/page/attachment
region.
8. **True hybrid/RRF chưa live.** Condition retrieval hiện lexical-first + dense
fallback, rerank/group ở drug level; không rewrite stack nếu chưa có eval chứng
minh cần.
9. **BFF che upstream non-2xx thành generic `upstream_error`.** Điều này làm chẩn
đoán lỗi Qdrant khó. Deploy smoke hiện in backend logs khi condition request
fail, nhưng một exception khác ngoài smoke vẫn có thể cần Grafana/Tempo hoặc
EC2 logs để tìm root cause.
10. **Local workspace có nhiều file untracked không thuộc feature**: `.codex-*`,
`.codex/`, local uvicorn logs, screenshots, `tmp/`, và
`docs/answer-experience-implementation-plan.md`. Không `git add -A`, không xóa
chúng nếu chưa có xác nhận của chủ dự án.
11. Có thể còn local uvicorn dev processes ở các port 80808093 từ manual testing.
Chúng không phải production. Chỉ cleanup khi được yêu cầu và phải xác định
đúng PID/command trước khi dừng.
## Local git state tại handoff
- Local branch: `agent/qdrant-query-points`.
- Remote production `master`: `f4b84fb`.
- Feature/hotfix đã merge; local branch không cần push thêm.
- File handoff này được tạo theo yêu cầu lưu memory sau khi chủ dự án yêu cầu
pause. Không commit/deploy file này trong turn hiện tại.
- Working tree còn untracked artifacts của nhiều phiên; giữ nguyên.
## Nguyên tắc khi resume
1. Đọc file này và `docs/condition-to-drug-audit-and-design.md` trước.
2. Xác nhận production vẫn ở commit mong muốn và health public còn 200.
3. Không chạm Gitea/ArgoCD/team infrastructure.
4. Chạy tiếp production battery từ case 21, không chạy lại từ đầu trừ khi có
code/deploy mới ảnh hưởng toàn pipeline.
5. Mọi drug trong answer phải có indication evidence và same-drug citation.
6. Không biến indication thành lời khuyên first-line/best treatment.
7. Nếu case fail:
- phân biệt provider transient với deterministic logic failure;
- giữ artifact initial failure;
- tái hiện local;
- lấy production trace/log;
- fix nhỏ nhất;
- chạy full local gates;
- deploy qua GitHub personal AWS workflow;
- rerun failed case và relevant regressions.
8. Chỉ kết luận Definition of Done sau khi đủ 60 production cases và report exact
commands/results/remaining limitations.