Fix migration workflow: upload as artifact instead of scp to practice EC2
This commit is contained in:
@@ -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ì?` và
|
||||
`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 5–7 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 1221–1223 / printed 1222–1224 — 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 1–20: **20/20 pass sau fix/retry**;
|
||||
- còn case 21–60: **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 55–60 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 31–40, 41–50, và 51–60. 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 8080–8093 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.
|
||||
Reference in New Issue
Block a user