A conversational AI assistant that helps caregivers manage a patient's therapy schedule. The caregiver interacts in natural language; the assistant uses LLM tool-calling to add, modify, and remove therapy activities while enforcing safety checks and scheduling constraints.
┌─────────────────────────────────────────────────────────┐
│ LLM (Ollama / OpenAI) │
│ tool-calling via OpenAI-compatible API │
└───────────────────────┬─────────────────────────────────┘
│ tools
┌─────────────┼──────────────┐
▼ ▼ ▼
therapy.json ChromaDB PostgreSQL
(working state) (RAG store) (version history)
| File | Role |
|---|---|
chat.py |
Core Chat class – sends messages, executes tools, manages conversation history, runs end-of-session processing |
agent.py |
Base Agent class and specialised subclasses (TherapyManagerAgent, TherapyCheckAgent) – each agent owns its prompt, tools, context injection, and tool execution logic |
tools.py |
All tool implementations: CRUD on therapy activities, scheduling conflict detection, RAG lookups |
vector_db.py |
VectorDBManager – wraps ChromaDB; manages 4 collections (see below) |
sql_db.py |
DatabaseManager – wraps PostgreSQL via SQLAlchemy; stores patients and versioned therapy snapshots |
session_extractor.py |
End-of-session LLM extraction: saves conflict resolutions and patient preferences to ChromaDB |
session_manager.py |
SessionManager – handles runtime therapy snapshots (save/restore) and past session loading from disk |
log_parser.py |
Parses chat.log files and reconstructs conversation history for session resume |
scenario_loader.py |
Loads scenario files, installs therapy state, and converts therapy data to natural language for the CaregiverAgent |
prompts.py |
System prompt for the main assistant and extraction prompts for session_extractor |
config_loader.py |
Loads all settings from the .env file |
main.py |
Terminal entry point – supports both interactive and agent-driven modes |
chat_interface.py |
Streamlit web UI entry point |
test.py |
Batch test runner – executes multiple scenarios automatically and produces structured evaluation reports |
The system uses a supervisor/worker multi-agent pattern:
TherapyManagerAgent(supervisor) – manages the conversation with the caregiver, handles therapy CRUD operations, and delegates to worker agents when needed.TherapyCheckAgent(worker) – invoked automatically before any activity is added or updated; checks compatibility with the patient's current medications, history, and preferences.CaregiverAgent– simulates a caregiver interacting with the system, driven by a natural language script describing objectives. Used in agent-driven mode and batch testing.JudgeAgent– evaluates completed conversations against the scenario objectives and produces a structured JSON report (checklist of completed, partial, failed, and not-attempted objectives).
Each agent is self-contained: it owns its system prompt, tool declarations, context injection (inject_context), and tool execution (execute_tool). Chat acts as the orchestrator and is the only component that knows all agents exist; agents do not know about each other.
Adding a new worker agent requires only: creating a new subclass, instantiating it in Chat.__init__, and registering it in _agent_registry. No other files need to change.
| Collection | R/W | Contents |
|---|---|---|
medicines |
Read-only at runtime | Pharmacological data indexed from .md files in data/medicines/ |
patient_history |
Read-only at runtime | Historical safety events per patient (seeded from data/patients/<id>/history.json) |
conflict_resolutions |
Read-Write | Past scheduling conflict resolutions, extracted automatically at session end |
patient_preferences |
Read-Write | Patient habits and preferences, extracted automatically at session end |
- Startup – load patient from PostgreSQL → write
data/therapy.json→ seed ChromaDB collections → initialise chat with context (datetime, current activities, patient preferences). - Conversation loop – for each caregiver message the LLM may call up to 10 tools in sequence:
get_medicine_data– RAG lookup against the medicines collection (mandatory before any medicine activity).get_patient_preferences– retrieve known patient habits for personalised suggestions.add_therapy_activity/update_therapy_activity/remove_therapy_activity– mutatetherapy.json; each write automatically triggers a scheduling conflict check and a patient-history safety check, and saves a runtime therapy snapshot.delegate_to_activity_checker– delegate an activity toTherapyCheckAgentfor safety validation before it is added.get_therapy_activities– read the full current schedule.get_current_datetime– get current date/time.save_session– trigger end-of-session processing (see step 3).
- End of session (triggered by
exit/quitcommand or thesave_sessiontool) –- LLM extracts conflict resolutions from the conversation and persists them to ChromaDB.
- LLM extracts patient preferences from the conversation and upserts them in ChromaDB.
- Current
therapy.jsonis saved as a new versioned snapshot in PostgreSQL.
Conflicts are detected deterministically in tools.py:
- Activities are compared by time overlap and day-of-week overlap and
valid_from/valid_untildate-range overlap. - When a conflict is found, two alternative times are suggested (anticipate / postpone).
- Past resolution hints are retrieved from ChromaDB and included in the tool response so the LLM can surface them to the caregiver.
- The LLM never resolves conflicts autonomously – it always asks the caregiver.
Both the terminal and web interfaces support rewinding the conversation to any previous point:
- Every time a therapy activity is added, updated, or removed, a therapy snapshot is saved in memory (and persisted to
therapy_snapshots.jsonin the session log folder). - Each snapshot stores the full
therapy.jsonstate and a logical message index (counting onlyuserandassistantmessages) so that indices remain stable across save/load cycles. - On rewind, the conversation history is truncated and the most recent snapshot at or before the rewind point is restored to
therapy.json, keeping the therapy state consistent with the conversation.
Previous sessions can be reloaded into the current context:
- Each session is logged in
logs/<patient_id>/<session_id>/(see Data files below). SessionManagerreadschat.logto reconstruct the conversation history andtherapy_snapshots.jsonto restore the therapy timeline.- In the Streamlit UI, past sessions are listed in the sidebar and can be loaded with one click.
- A
[LOAD]entry is appended to the currentchat.logwhenever a past session is loaded, for traceability.
python -m venv .venvLinux / macOS
source .venv/bin/activateWindows (PowerShell)
.venv\Scripts\activateIf PowerShell rejects the activation script, run the following once per user:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
From the repository root (with the venv active):
pip install -e .Copy .env.example to .env and fill in every variable:
# Two backends are configured independently:
# MAIN — the system under test (therapy manager + checker)
# SIM — the test harness (simulated caregiver + judge)
# Each has its own provider AND model. Any SIM_* left empty falls back to MAIN.
# PROVIDER: openai | groq | ollama; empty = inferred from the keys below
# (OpenAI first, then Groq), falling back to Ollama.
PROVIDER=ollama
MODEL=gpt-oss:20b
SIM_PROVIDER=
SIM_MODEL=
# Credentials and endpoints belong to the provider, not the role
OLLAMA_URL=http://localhost:11434
# OPENAI_API_KEY=sk-...
# GROQ_API_KEY=gsk_...
# Example – grade a locally served model with a cloud one:
# PROVIDER=ollama MODEL=gpt-oss:20b
# SIM_PROVIDER=groq SIM_MODEL=openai/gpt-oss-20b
# Example – both on Groq, two models = two separate quotas:
# PROVIDER=groq MODEL=openai/gpt-oss-120b
# SIM_PROVIDER=groq SIM_MODEL=openai/gpt-oss-20b
# Sent on every call; supported by the gpt-oss family (low|medium|high),
# rejected by models without a reasoning mode – leave empty for those.
REASONING_EFFORT=low
# LLM request timeout in seconds
LLM_TIMEOUT=120
# PostgreSQL connection
DB_HOST=localhost
DB_PORT=5432
DB_NAME=therapy_db
DB_USER=postgres
DB_PASSWORD=password
# Patient loaded at startup
DEFAULT_PATIENT_ID=1
# Logging levels: DEBUG | INFO | WARNING | ERROR
FILE_LOG_LEVEL=DEBUG
TERMINAL_LOG_LEVEL=WARNING
# Set to 1 to log NVIDIA GPU info at startup
CHECK_NVIDIA_GPU=0A running PostgreSQL instance is required. Tables are created automatically on first run.
Refer to the PostgreSQL documentation for installation instructions.
Ollama (local) – install Ollama, pull the model, then start the server:
ollama pull qwen2.5:14b
ollama serveThe model must support the /api/chat endpoint (i.e. be a chat model, not a completion-only model).
OpenAI – no local server needed; just set OPENAI_API_KEY in .env.
Groq – no local server needed; set GROQ_API_KEY and PROVIDER=groq. Groq enforces
per-model rate limits (the free tier gives 30 RPM / 8K TPM / 1K RPD / 200K TPD on both
openai/gpt-oss-120b and openai/gpt-oss-20b), so requests are paced client-side before
being sent. The limits follow the provider of each role automatically — leave LLM_* /
SIM_LLM_* unset unless the account has a different tier. A batch stops with a clear
message when a daily quota runs out, and the tokens consumed are reported in
logs/batch_results/<batch_id>/results.json. On the 8K TPM free tier the per-minute budget
is the binding constraint: pacing makes a scenario take several minutes, and a long
conversation can grow past what a single request is allowed to carry.
All commands must be run from the src/ directory.
cd src
python main.pyType exit, quit, or esci to end the session. The therapy state is saved to PostgreSQL automatically.
| Flag | Short | Default | Description |
|---|---|---|---|
--input <file> |
-i |
(none) | Path to a scenario script (.md or any text format) describing the caregiver's objectives. If omitted, runs in interactive mode. |
--delay <seconds> |
-d |
0 |
Pause between turns in agent mode. Useful for readability. |
Agent mode – a CaregiverAgent reads the script and drives the conversation autonomously:
python main.py --input scenarios/1/scenario_script.md
python main.py --input scenarios/1/scenario_script.md --delay 2The script is a free-form markdown file describing the caregiver's objectives. Its full content is passed verbatim to the CaregiverAgent — formatting and structure are the author's responsibility:
# Add a morning walk
## Context
The patient tolerates light exercise well.
## Objectives
1. Add a 30-minute morning walk every Monday, Wednesday and Friday at 08:00.
2. If the assistant reports a conflict, follow its suggestion.
3. Once the activity is confirmed, end the conversation.cd src
streamlit run chat_interface.pytest.py runs multiple scenarios automatically, without human interaction, and produces structured evaluation reports. Each scenario is a self-contained folder under scenarios/ containing a scenario.json file that defines the patient, the initial therapy state, and the objectives for the CaregiverAgent.
Each scenario runs in stateless mode: no database is used and therapy.json is overwritten at the start of each scenario from the scenario file itself, ensuring full isolation between runs.
At the end of each scenario the JudgeAgent evaluates the conversation against the objectives and produces a structured JSON result. All scenario logs and results are saved under logs/batch_results/<batch_id>/.
| Flag | Default | Description |
|---|---|---|
--from <id> |
1 |
First scenario ID to run (inclusive). |
--to <id> |
same as --from |
Last scenario ID to run (inclusive). Omit to run a single scenario. |
--delay <seconds> |
0 |
Pause between conversation turns. |
--max-turns <n> |
30 |
Maximum number of turns per scenario before forcing termination. |
# Run all the scenarios
python test.py
# Run a single scenario
python test.py --from 1 --to 1
# Run a range of scenarios
python test.py --from 1 --to 20Each scenario lives in scenarios/<id>/scenario.json and follows the same structure as therapy.json, with an additional objectives field containing the script passed to the CaregiverAgent:
{
"patient_id": 1,
"patient_full_name": "Mario Rossi",
"gender": "Male",
"birth_date": "1945-06-10T00:00:00",
"age": 80,
"medical_conditions": ["Hypertension"],
"activities": [ "..." ],
"expired_activities": [],
"objectives": "# Scenario 1\n\n## Objectives\n1. Add a 30-minute evening walk..."
}logs/batch_results/<batch_id>/
├── batch.log # global batch log (errors, skips, summary)
├── results.json # all evaluation results aggregated
└── <scenario_id>/
├── scenario_<id>.log # per-scenario log
├── chat.log # USER/ASSISTANT transcript
└── full.log # full log including tool calls
└── evaluation.json # result of the judge evaluation
results.json contains one entry per scenario with the JudgeAgent evaluation:
{
"batch_id": "20260630_143000",
"results": [
{
"scenario_id": 1,
"overall_status": "completed",
"turns": 6,
"patient": "Mario Rossi",
"elapsed_seconds": 42.3,
"objectives": [
{
"id": 1,
"description": "Add a 30-minute evening walk on Tuesday and Thursday at 17:00",
"status": "completed",
"evidence": "Assistant confirmed: activity added for Tue, Thu at 17:00",
"notes": null
}
],
"summary": "All objectives were completed successfully within 6 turns."
}
],
"failed": []
}If a scenario raises an unhandled exception it is logged in batch.log under failed, and the batch continues with the next scenario.
| Path | Description |
|---|---|
data/therapy.json |
Working copy of the current patient's therapy (overwritten at startup and mutated during a session) |
data/medicines/*.md |
Pharmacological data files indexed into ChromaDB on startup. Add new .md files here to extend the medicine knowledge base |
data/patients/<id>/history.json |
Seed safety-event history for a patient |
data/patients/<id>/preferences.json |
Seed preferences for a patient |
data/patients/<id>/conflict_resolutions.json |
Seed past conflict resolutions for a patient |
scenarios/<id>/scenario.json |
Self-contained test scenario: patient data, initial therapy, and caregiver objectives |
chromadb/ |
Persistent ChromaDB store (auto-created) |
logs/<patient_id>/<session_id>/chat.log |
Chat-only log (USER/ASSISTANT messages); used for session resume |
logs/<patient_id>/<session_id>/full.log |
Full log including tool calls, errors, and system events |
logs/<patient_id>/<session_id>/agent_<name>.log |
Per-agent log for each active agent |
logs/<patient_id>/<session_id>/therapy_snapshots.json |
Runtime therapy snapshots with logical message indices; used for conversation rewind |
logs/batch_results/<batch_id>/results.json |
Aggregated evaluation results for a batch test run |