Indonesian clinical-guideline retrieval agent. Anamnesa answers clinical questions written in Indonesian with inline citations to the public-domain Ministry of Health corpus. Every cited recommendation carries a currency flag (current / aging / superseded). When the corpus does not cover a question, Anamnesa refuses rather than hallucinates.
| Demo video | youtu.be/DJyzwe3ibCI |
| Live demo | anamnesa.kudaliar.id |
| Built for | Built-with-Opus-4.7 Claude Code hackathon · 21–27 April 2026 |
- Indonesia's national clinical guidelines, protocols, and BPJS formulary are published as long government PDFs.
- A primary-care clinician mid-consult often has only one option: open the PDF on a phone and Ctrl-F through hundreds of pages.
- Commercial CDS tools solve the interface problem but are English-only and skip Indonesian guidelines and the national formulary.
- Anamnesa is the missing retrieval layer — typed Indonesian question in, Indonesian answer out, citation to the exact page.
Every query produces one append-only QueryState that flows through four stages:
- Normalizer (Haiku 4.5) — one-shot. Reads the colloquial question, emits a structured query (intent · condition · population · setting). Refuses out-of-scope or patient-specific questions here.
- Retriever (MCP server, no LLM) — hybrid: BGE-M3 semantic vectors over 9,000+ chunks + rank-bm25 lexical, fused by reciprocal-rank fusion with metadata filters. Deterministic, fast, auditable. Agents reach it only through the
anamnesa-mcptool boundary. - Drafter (Opus 4.7, adaptive thinking,
higheffort) — composes an Indonesian answer with inline citations. Can request up to 2 retries with narrower filters. Cannot emit a claim without a supporting citation. - Verifier (Opus 4.7, 1M context,
higheffort) — re-fetches every cited chunk, classifies each claimsupported/partial/unsupported, attaches currency flags. Sends one revision back to the Drafter on failure; refuses outright if the retry also fails.
All four stages emit TraceEvents that stream to the browser via SSE. A budget layer caps retrieval attempts, agent calls, total tokens, and wall-clock — a runaway query cannot exhaust the budget.
Lightweight read-only endpoints handle queries that don't need the full agent loop:
| Mode | Endpoint | Cost | Latency | When to use |
|---|---|---|---|---|
| Fast search | GET /api/search |
$0 | ~50 ms | "Which guideline says X?" |
| Drug lookup | GET /api/drug-lookup |
$0 | ~30 ms | "Is amoxicillin in Fornas?" |
| Agent mode | POST /api/query → SSE |
~$0.40–0.80 | ~130 s live / ~0 s cached | Synthesize across guidelines |
- 24-hour SQLite answer cache keyed on canonical query text — repeat queries return in ~0 ms.
- Drug lookup queries the BPJS formulary by name, ATC code, or indication — no LLM ever invoked.
- Follow-ups like "and for pediatric patients?" would be incomprehensible to a stateless retriever.
- Normalizer receives the prior turn (query + answer) plus the terse follow-up and rewrites them into a fully-qualified standalone query before retrieval runs.
- Threads persist to browser localStorage with 24 h TTL and a 5-turn cap. A subtle banner restores in-progress conversations on reload.
- Inline numeric citations are clickable — scrolls to the matching reference card and flashes both ends.
- Reference cards open the exact PDF page in an in-app PDF.js viewer, so the user never leaves the thread.
- Currency flags sit beside every doc id — a 2015 recommendation superseded by a 2022 edition is flagged before the reader acts.
- Three exports per answer: clipboard, Markdown, WhatsApp. WhatsApp matches the channel Indonesian clinicians actually use.
- Thumbs-up / down writes to a SQLite feedback store;
/admin/feedbackauto-refreshes for triage. - Refusal explorer — when the system refuses with
corpus_silent, the UI also renders the three closest near-miss chunks so the user can see the boundary.
- No answer without inline citation — if retrieval is empty, the Drafter refuses.
- No fallback on training-set medical knowledge — when the corpus is silent, the answer is silent.
- No softened refusals — an unfounded answer is worse than a clear "no Indonesian guideline exists for this scenario."
- No patient-specific dosing — patient-specific questions redirect to guideline-level information.
- No translation of guideline content — the corpus is Indonesian; answers stay Indonesian.
| Documents in corpus | 81 |
| Structured chunks | 9,083 |
| Eval scenarios | 23 |
| Pass rate | 23 / 23 (100%) |
| Hallucinated citations | 0 |
| Wall-clock, agent mode | ~130 s live · ~0 s cached |
| Wall-clock, fast search & drug lookup | ~50 ms |
| Test suite | 165 passing · ruff clean |
Requirements: Python 3.12, Node 20+, an Anthropic API key. CUDA GPU is optional (CPU works, slower).
git clone https://github.com/0xNoramiya/anamnesa.git
cd anamnesa
uv venv --python 3.12
uv pip install -e ".[dev,embeddings]"
cp .env.example .env
# ANTHROPIC_API_KEY=sk-…, ANAMNESA_EMBEDDER=bge-m3
# Build the retrieval index (~3 min on a modern GPU)
.venv/bin/python -m scripts.reindex --embedder bge-m3 --yes
# Backend (terminal 1)
.venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000
# Frontend (terminal 2)
cd web && npm install && npm run dev
# open http://localhost:3000Crawled PDFs are not committed (light repo). Re-crawl via agents/prompts/crawler.md or copy the cache from a peer to enable the in-app PDF viewer.
Run the eval suite:
.venv/bin/python -m eval.run_eval --max-concurrent 2 \
--output-md eval/results/run.md --output-json eval/results/run.jsonRun the MCP server (Claude Desktop / Claude Code):
python -m mcp.anamnesa_mcp
# wire it via claude_desktop_config.json — see the /mcp page in the app┌─ web/ (Next.js 14 · Tailwind · shadcn/ui) ─────────────┐
│ Landing · Chat (multi-turn) · Drugs · Search │
│ Guideline · History · Favorites · Agent trace │
└─────────────────────────┬──────────────────────────────┘
│ fetch / SSE
┌─ server/ (FastAPI) ─────┴──────────────────────────────┐
│ Lifespan boots Orchestrator once (BGE-M3, LanceDB, │
│ agent clients). Per-query asyncio task → SSE queue. │
│ /api/health /api/meta /api/search /api/query │
│ /api/stream /api/drug-lookup /api/feedback /… │
└─────────────────────────┬──────────────────────────────┘
┌─ core/ ─────────────────┴──────────────────────────────┐
│ orchestrator · QueryState · HybridRetriever │
│ embeddings (BGE-M3) · LanceDB chunk store · manifest │
└────────────────────────────────────────────────────────┘
┌─ agents/ ──────────────────────────────────────────────┐
│ normalizer (Haiku 4.5) · drafter / verifier (Opus 4.7)│
│ prompts/ — system prompts encoding the trust contract │
└────────────────────────────────────────────────────────┘
┌─ mcp/ ─────────────────────────────────────────────────┐
│ FastMCP server: search_guidelines · get_full_section │
│ get_pdf_page_url · check_supersession │
└────────────────────────────────────────────────────────┘
- Backend lifespan loads BGE-M3, opens LanceDB, warms rank-bm25 — first request pays no warm-up cost.
- Streaming — every agent query runs as an asyncio background task writing into a queue the SSE handler drains.
- Tool boundary — agents only reach retrieval through the FastMCP server, so the same tools work in-process and from Claude Desktop / Claude Code.
For the full spec (refusal states, budget guardrails, trace event shapes, Bahasa conventions) see CLAUDE.md.
- Frontend — Next.js 14 (App Router), TypeScript, Tailwind CSS, shadcn/ui, PDF.js, native SSE.
- Backend — Python 3.12 · FastAPI · Uvicorn · Pydantic v2 · structlog · sse-starlette.
- Retrieval — LanceDB · BGE-M3 (sentence-transformers) · rank-bm25 · CPU-capable at query time.
- Agents — Anthropic Python SDK · Haiku 4.5 · Opus 4.7 (1M ctx for Verifier) · FastMCP for the tool boundary.
- Storage — SQLite (cache + feedback) · file-locked JSON manifest for the corpus catalog.
- Ingestion — pdfplumber · PyMuPDF · httpx + BeautifulSoup · uv · ruff · mypy · pytest + pytest-asyncio.
- Code — MIT.
- Corpus (chunks in
catalog/processed/) — public domain.
