← всі звіти · 03-architecture.md

sparc_phase: A project: med-detective created: 2026-05-10 last_updated: 2026-05-10 author: Claude (post-hoc документація існуючої системи) status: approved approved_date: 2026-05-10 legacy_baseline: true spec_link: ./01-specification.md pseudocode_link: ./02-pseudocode.md

Architecture — Архітектура: стек, файли, інтерфейси — Med Detective (umbrella)

Стек

Компонент Технологія Чому
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

Компоненти і відповідальності

Ключові інтерфейси

HTTP API

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

Безпека

Деплой і інфраструктура

Спостережуваність

Відкриті питання

  1. Чи переходимо на async Redis client (aioredis) для unified asyncio? — performance impact мінімальний при поточному навантаженні (<1 RPS)
  2. Чи підтримуємо стрімінг відповіді Patient через Server-Sent Events? — UX покращиться, але парситимемо partial JSON для intent recognition; складність зросте

Sign-off