A multi-agent LLM system that takes a plain list of questions and conducts it as a natural, neutral, multi-turn interview. Built for Parkinson's disease follow-up, but generic: swap in any questionnaire as a JSON file and the system administers it — asking one question at a time, following up when answers are incomplete, never suggesting symptoms, and remembering the patient across sessions. Runs entirely on local, open-source models (privacy and low cost), speaks four languages (French, English, German, Italian), and accepts voice input via Whisper.
- Questionnaire in, interview out — a flat JSON list of questions becomes a conversational interview; no code change to add a new form.
- Neutral by design — the interviewer asks the bank question faithfully and never presupposes or suggests symptoms.
- Deterministic backbone — question coverage, follow-up dwell cap, force-advance and termination are plain code, not LLM judgment, so every interview provably covers every question and ends.
- Long-term memory — durable facts distilled during conversations are recalled in later sessions via embeddings (with a lexical fallback when no embedding server is configured).
- Full auditability — every turn stores the routing decision plus the complete prompt and raw output of every agent call; completed interviews export automatically as structured JSON + PDF.
- Local & open-source models only — any OpenAI-compatible server works; no data leaves your infrastructure.
- 4 languages — fr / en / de / it, selected per patient.
- Voice input — Whisper transcription, remote or in-process.
Scope note: there is deliberately no automated safety/crisis-detection layer — this is a research prototype intended for use in a supervised setting with a clinician present.
Three LLM agents around a deterministic frame:
| Component | Where | Role |
|---|---|---|
| Orchestrator | backend/modules/agents/orchestrator.py |
Understands the patient's message and routes it (answer / question / digression / refusal...) as strict JSON at temperature 0. |
| Deterministic frame | backend/modules/frame.py |
Plain code: tracks question coverage, caps dwelling on one question at 3 turns, forces advancement, guarantees termination. |
| Interviewer | backend/modules/agents/interviewer.py |
Writes every patient-facing message from a per-turn directive — there are no pre-written patient-facing strings anywhere. |
| Memory builder | backend/modules/agents/memory_builder.py |
Every 5 patient turns, distils durable facts from the recent conversation window. |
| Memory store | backend/modules/memory.py |
Flat memory_facts table (text + vector), cosine recall of the top 5 facts, Jaccard dedup at 0.7, lexical fallback without embeddings. |
| Audit & export | backend/modules/audit.py, backend/modules/export.py |
Per-turn traces in turn_traces; automatic JSON + PDF export to backend/exports/conversations/ on completion. |
Questionnaires are positional (q1..qN) and snapshotted per conversation, so editing a form never corrupts a running interview.
Prerequisites: Python 3.11+, Node.js 18+, a reachable PostgreSQL database, and any OpenAI-compatible chat server (vLLM, llama.cpp, LM Studio, ...).
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp ../.env.example ../.env # then edit the 3 REQUIRED values (see below)
uvicorn main:app --host 127.0.0.1 --port 8000The database schema is created automatically at startup — no migration step. Health checks:
curl http://127.0.0.1:8000/api/health/live # process is up
curl http://127.0.0.1:8000/api/health/ready # process is up AND database reachablecd frontend
echo NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000/api > .env.local
npm install
npm run devOpen http://localhost:3000, register a patient account, and start an interview.
Only three values are required; everything else is optional. See .env.example for the full annotated list.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | Any reachable PostgreSQL database (schema auto-created). |
OPENAI_COMPAT_BASE_URL |
yes | Base URL of an OpenAI-compatible chat server. |
OPENAI_COMPAT_MODEL |
yes | Model name served there. |
OPENAI_COMPAT_API_KEY |
no | Leave empty for local servers. |
SECRET_KEY |
recommended | Auth token signing secret — set a long random value in production. |
OPENAI_COMPAT_EMBEDDING_BASE_URL / _MODEL |
no | Embeddings server for long-term memory. Leave empty to fall back to lexical recall. |
WHISPER_BASE_URL / WHISPER_MODEL |
no | Remote OpenAI-compatible Whisper server for voice input. Without it, chat still works — only the microphone fails. |
WHISPER_LOCAL_ENABLED / WHISPER_LOCAL_MODEL |
no | Set true to run faster-whisper in-process instead (e.g. large-v3-turbo). |
FRONTEND_ORIGINS / FRONTEND_ORIGIN_REGEX |
no | CORS allowlist for the frontend. |
Offline, zero services — a deterministic simulation of the memory system and the frame (in-memory SQLite, hash-based fake embeddings, no model server). It checks fact storage, Jaccard dedup, cosine recall, the lexical fallback, per-patient scoping, question coverage, the dwell-cap force-advance, and termination, printing a PASS/FAIL line per check:
cd backend && source venv/bin/activate
python scripts/simulate_memory_system.pyLive, with a model server — LLM-driven adversarial "patients" (forgetful, rambling, refusing, contradicting, language-switching, prompt-injecting, ...) stress-test the real pipeline over an isolated in-memory SQLite. Transcripts and results land in backend/exports/adversarial/:
python scripts/adversarial_patients.py # all personas
python scripts/adversarial_patients.py forgetful # one persona
MAX_TURNS=8 python scripts/adversarial_patients.py forgetful # quick smoke runEnd-to-end over HTTP — drives the real /api/chat route with an LLM-simulated patient until the interview closes, then verifies full coverage, no duplicate questions, no robotic openers, and judges per-turn coherence with an LLM. Requires the backend running:
python scripts/full_interview_test.py # API_BASE_URL overridable, default http://127.0.0.1:8000/apicd backend
DISABLE_RATE_LIMIT=1 pytest tests/76 tests covering routing, the chat pipeline, forms, parsing, auth, audit, export and rate limiting. No database or model server needed.
Drop a JSON file in backend/data/forms/:
{
"_meta": { "name": "My follow-up", "description": "...", "version": 1 },
"questions": [
"How have you been overall recently?",
"How is your sleep?"
]
}That's it — no code change. Questions are numbered by position (q1..qN), and the form appears in the settings dropdown. Five forms ship by default (general and short Parkinson follow-up, diabetes, post-op, mood).
- PostgreSQL holds users, conversations, messages, per-conversation form snapshots, question progress,
memory_facts(text + vector) and per-turnturn_traces— the schema is created automatically at startup. - On interview completion, a named folder under
backend/exports/conversations/receivesconversation.json(question hierarchy with nested probes, distilled memory facts, complete agent traces) and a formattedconversation.pdf.
Reference deployment — any OpenAI-compatible model can be substituted:
| Role | Model | Licence |
|---|---|---|
| Chat (all 3 agents) | Qwen3.6-35B-A3B (Qwen3 MoE family) | Apache-2.0 |
| Embeddings (memory) | Qwen3-Embedding-8B | Apache-2.0 |
| Speech-to-text | Whisper large-v3-turbo | MIT |
backend/
main.py # FastAPI app, schema auto-creation
core/ # settings, auth, rate limiting
routes/ # /api: auth, chat (per-turn pipeline), conversations, forms, audio, ...
modules/
agents/ # orchestrator, interviewer, memory_builder (+ tolerant JSON parsing)
frame.py # deterministic coverage / dwell / termination
memory.py # embedding memory + lexical fallback
chatbot.py # LLM client: chat + embeddings, retries, trace capture
export.py # JSON + PDF export
data/forms/ # questionnaire JSON files
scripts/ # simulations (offline + live)
tests/ # pytest suite (76 tests)
frontend/ # Next.js app (login, chat, settings)
report/ # scientific report (LaTeX sources + PDF)
The full scientific report — motivation, architecture, evaluation — lives in report/ (LaTeX sources and compiled main.pdf).
