| Компонент | Технологія | Чому |
|---|---|---|
| Backend | Python 3.11 + FastAPI + uvicorn | Async I/O для паралельних LLM-викликів, low overhead |
| LLM Inference | Replicate API (meta/meta-llama-3-8b-instruct) |
SOTA latency 1.0-1.4с p95, billing through Replicate Deltamedical account |
| LLM Fallback (Schönen-main only) | Gemini 2.5 Flash через gemini-med-detective.key |
На випадок Replicate downtime |
| Session storage | Redis (single instance, DB per форма) | TTL 24h, low latency, простий key-value |
| Frontend | Vanilla HTML + Tailwind CDN | Без фреймворків, швидкий load, легка адаптація |
| Reverse proxy | Caddy host-systemd (не Docker) | Атомарні reload через systemctl reload caddy, drop-in /etc/caddy/conf.d/<form>.caddy |
| Hosting | systemd unit per форма на VPS 31.131.26.203 | Незалежність від Docker, простий deploy через systemctl restart |
| Validation | scripts/validate_case.py |
Offline check кейс-spec'и проти LAB_CATALOG |
/srv/projects/med-detective/ # workdir 8765 (test slot legacy назва)
/srv/projects/med-detective-test/ # workdir 8766 (PROD! назва legacy після swap'у 19.04)
/srv/projects/med-detective-menopace/ # workdir 8768 (Menopace)
/srv/projects/med-detective-fables/ # workdir 8769 (Fables)
└── backend/
├── main.py # FastAPI app, routes, CASE_REGISTRY
├── llm.py # Replicate + Gemini wrappers, paralleled calls
├── judge.py # Judge prompt + rubric scoring
├── lab_catalog.py # 39 тестів × 6 категорій + extra_tests support
├── cases/
│ ├── case_01_*.json # case spec
│ ├── case_02_*.json
│ └── ...
├── prompts/
│ ├── patient_case_01.md # role-play prompt for AI patient
│ ├── patient_case_02.md
│ └── ...
└── scripts/
├── validate_case.py
└── deploy.sh
/etc/caddy/conf.d/
├── med-detective.caddy # /med-detective/ → 127.0.0.1:8766
├── med-detective-test.caddy # /med-detective-test/ → 127.0.0.1:8765
├── med-detective-menopace.caddy # /med-detective-menopace/ → 127.0.0.1:8768
└── med-detective-fables.caddy # /med-detective-fables/ → 127.0.0.1:8769
/etc/systemd/system/
├── med-detective.service # uvicorn :8765
├── med-detective-test.service # uvicorn :8766 (prod насправді)
├── med-detective-menopace.service # uvicorn :8768
└── med-detective-fables.service # uvicorn :8769
⚠️ Назви workdir перевернуті щодо логічних URL через swap routes 2026-04-19. Перевіряти кожен раз через
grep reverse_proxy /etc/caddy/conf.d/med-detective*.caddy
main.py — HTTP-шар. Тільки routing + валідація вхідних даних + Redis session management. Ніякої LLM-логіки.llm.py — обгортка над Replicate + Gemini. Експортує call_patient(history, persona), call_intent(text), call_judge(session). Паралелізує Patient + Intent через asyncio.gather.judge.py — окремий модуль для Judge prompt + rubric (40+30+15+15). Парсить JSON відповідь, fallback на diagnosis-only при regex fail.lab_catalog.py — статичний список 39 тестів. Загальний для всіх форм (один LAB_CATALOG ділиться).cases/*.json — спеки кейсів. Один кейс = один файл. Структуру див. у 02-pseudocode.md § «Кейс (case JSON)».prompts/patient_*.md — рольові промпти для AI-пацієнта. Один на кожен кейс. Містить персону, скарги, anxiety-trigger, секретний діагноз.scripts/validate_case.py — offline валідація. Запускається перед інтеграцією нового кейса.GET /<form>/ → лендінг з list of cases
GET /<form>/case/<case_id> → game UI
POST /<form>/api/chat → {session_id, text} → patient_reply
GET /<form>/api/lab_catalog → 39 тестів
POST /<form>/api/order_tests → {session_id, test_ids[]} → results
POST /<form>/api/finish_case → {session_id, diagnosis, product} → judge result
GET /<form>/api/health → 200 OK з версією
# llm.py
async def call_patient(history: list, persona: str) -> str: ...
async def call_intent(text: str) -> dict: ... # {type: "question"|"exam_request"|"diagnosis"}
async def call_judge(session: dict, case: dict) -> dict: ... # {total, diagnosis, anamnesis, product, efficiency}
# main.py
@app.post("/api/chat")
async def chat(req: ChatRequest, session_id: str = Header()) -> ChatResponse:
# parallel: patient + intent
patient, intent = await asyncio.gather(...)
[Doctor browser] (HTTP)
│
▼
[Caddy host-systemd] /<form>/* → 127.0.0.1:<port>
│ (зворотний proxy + strip prefix)
▼
[FastAPI uvicorn] systemd unit
│
├──► [llm.py call_patient] ──► [Replicate API] ──► Llama-3-8B
├──► [llm.py call_intent] ──► [Replicate API] ──► Llama-3-8B (паралельно)
│ (ці два паралельно, ~1.0-1.4с p95)
│
├──► [Redis] DB per form, TTL 24h ──► session state
└──► [judge.py] (по завершенні) ──► [Replicate API] ──► Llama-3-8B
│
└──► rubric scoring 40+30+15+15
/srv/passepartout/replicate/token.txt (chmod 600, читається uvicorn під root)/srv/passepartout/google/gemini-med-detective.keycases/ тільки після legal_review() passmed-detective{,-test,-menopace,-fables}.service/etc/caddy/conf.d/, atomic systemctl reload caddy1. Case Builder Crew генерує case_XX.json + patient_XX.md
2. Validate: scripts/validate_case.py
3. Sergii apruvає у Telegram
4. cp у backend/cases/ + backend/prompts/
5. Update CASE_REGISTRY у main.py
6. systemctl restart med-detective-test # на test slot спочатку
7. Smoke-тест http://31.131.26.203/med-detective-test/case/XX
8. Promote: swap Caddy routes 8765↔8766 + reload caddy (3 секунди rollback)
curl http://127.0.0.1:<port>/api/healthjournalctl -u med-detective -f