Home Applications TriageAide

TriageAide Awaiting Review

InterSystems does not provide technical support for this project. Please contact its developer for the technical assistance.
0
0 reviews
0
Awards
0
Views
0
IPM installs
0
0
Details
Releases (1)
Reviews
Issues
TriageAide is an AI agent that retrieves a patient's FHIR clinical history, conducts a personalized pre-consultation triage via chat, and writes structured triage results back to the FHIR server — so the physician receives a ready-made clinical summary before the appointment.

What's new in this version

Initial Release

TriageAide — FHIR-First Pre-Consultation Triage

TriageAide is an AI agent that retrieves a patient’s FHIR clinical history, conducts a personalized pre-consultation triage via chat, and writes structured triage results back to the FHIR server — so the physician receives a ready-made clinical summary before the appointment.

“TriageAide first retrieves patient history from a FHIR server, builds contextual clinical understanding, and performs an adaptive pre-consultation triage that enriches and updates the longitudinal patient record.”

TriageAide is an autonomous AI agent that operates on top of FHIR data — it queries the patient’s clinical history from a FHIR server (InterSystems IRIS for Health), conducts an intelligent contextual pre-consultation triage, and writes new FHIR resources back to the patient record.

This is not a generic chatbot that generates FHIR from scratch. It is an interoperable AI agent that reasons over existing clinical data:

  1. FHIR-First — consults patient history BEFORE interacting with the patient
  2. Contextual Triage — asks intelligent questions based on real clinical history, not generic checklists
  3. Bidirectional — reads from AND writes back to the FHIR server
  4. Longitudinal — understands care continuity (e.g., “last visit 8 months ago — follow-up overdue”)
  5. Bilingual — supports Brazilian Portuguese (pt-BR) and English (en-US) in chat

5-Step Workflow

  1. FHIR Query — Agent retrieves Patient, Condition, MedicationRequest, Observation, AllergyIntolerance, Encounter
  2. Contextual Triage — With history in hand, generates intelligent, personalized questions
  3. Interactive Conversation — Chat loop: agent asks one question at a time, patient responds, agent deepens
  4. Clinical Reasoning — Cross-references FHIR history + new symptoms → risk assessment, priority suggestion
  5. FHIR Update — Creates Observation, QuestionnaireResponse, Flag, Task, Encounter back on the server

Architecture

FHIR Server (InterSystems IRIS for Health)      ← iris container
|
+-- fhir_server.py (MCP :8000) — 12 FHIR CRUD tools
+-- triage_server.py (MCP :8001) — 6 contextual triage tools
+-- clinical_reasoning_server.py (MCP :8002) — 4 clinical reasoning tools
|
+-- agent.py (LangChain + OpenAI gpt-4o-mini) — core agent, bilingual system prompt
+-- voice_bridge.py (FastAPI :8003) — OpenAI-compatible endpoint for ElevenLabs *(roadmap)*
+-- voice_session.py — per-session state and language detection *(roadmap)*
+-- cli.py — interactive CLI interface
+-- app.py (Gradio :7860) — web UI: Chat tab (+ Voice tab when ENABLE_VOICE_UI=true)
|
+-- entrypoint.sh — container entrypoint (waits for FHIR, loads seed, starts all services)

Voice Architecture (Roadmap — next step)

Voice interaction via ElevenLabs Conversational AI is implemented in the backend (voice_bridge.py on port 8003) but the UI tab is hidden by default in the MVP. To enable it, set ENABLE_VOICE_UI=true in .env. See ElevenLabs Voice Integration for full setup instructions.
[Patient browser / phone]
|
| WebSocket (ElevenLabs Conversational AI)
v
[ElevenLabs Cloud]

  • STT: speech-to-text (pt-BR / en-US, auto-detect)
  • TTS: voice synthesis (Brazilian Portuguese voice)
    |
    | POST /v1/chat/completions (OpenAI-compatible, Bearer auth)
    v
    [voice_bridge.py — FastAPI :8003]
  • Session management per conversation
  • Language detection (heuristic + ElevenLabs header)
  • Markdown stripping for clean TTS output
  • SSE streaming
    |
    v
    [agent.py — LangChain + gpt-4o-mini] (bilingual: auto-detect mode)
    |
    ┌────┴───────────────┬─────────────────────┐
    v v v
    fhir_server.py triage_server.py clinical_reasoning_server.py
    (MCP :8000) (MCP :8001) (MCP :8002)
    |
    v
    [InterSystems IRIS for Health — http://iris:52773/fhir/r4]

Two independent Docker services share a `fhir-net` bridge network:
- **iris** — IRIS for Health FHIR server
- **triage** — Python app (MCP servers + Voice Bridge *(roadmap)* + Gradio UI)

Prerequisites

Requirement Details
Docker + Docker Compose v2.20+ recommended
OpenAI API key Used by gpt-4o-mini for clinical reasoning
(Optional) LangSmith API key Agent tracing and debugging
(Roadmap) ElevenLabs API key Required only for voice interface — not needed for MVP
(Roadmap) ngrok Expose local voice bridge to ElevenLabs — not needed for MVP

Setup — Step by Step

Step 1 — Clone the repository

git clone https://github.com/musketeers-br/TriageAide.git
cd TriageAide
</code></pre>
<h3>Step 2 — Configure environment variables</h3>
<pre><code class="language-bash">cd python/triage
cp .env.example .env
</code></pre>
<p>Open <code>.env</code> and fill in the required values:</p>
<pre><code class="language-bash"># Required
OPENAI_API_KEY=sk-...your-openai-key-here...

# Optional — LangSmith tracing
LANGSMITH_API_KEY=ls-...your-langsmith-key...
LANGSMITH_PROJECT=triage-aide
LANGSMITH_TRACING=true

# Voice UI — show/hide the Voice tab (voice bridge runs regardless for testing)
ENABLE_VOICE_UI=false

# Roadmap — ElevenLabs voice interface (see "ElevenLabs Voice Integration" section)
# VOICE_BRIDGE_SECRET=your-strong-random-secret # generate: openssl rand -hex 32
# ELEVENLABS_AGENT_ID= # fill after creating the agent
# ELEVENLABS_WIDGET_ID= # fill after creating the agent
# VOICE_BRIDGE_URL=https://your-ngrok-url # fill after starting ngrok
</code></pre>
<blockquote>
<p><strong>Note:</strong> <code>FHIR_BASE_URL</code>, <code>FHIR_USER</code>, and <code>FHIR_PASS</code> have correct defaults in <code>.env.example</code> and do not need to be changed for local development.</p>
</blockquote>
<p>Go back to the root directory:</p>
<pre><code class="language-bash">cd ../..
</code></pre>
<h3>Step 3 — Build the Docker images</h3>
<pre><code class="language-bash">docker compose build --no-cache --progress=plain
</code></pre>
<p>Expected output: two images built — <code>fhir-template</code> (IRIS) and <code>triage-app</code> (Python). The build may take <strong>3–8 minutes</strong> on first run due to IRIS image size.</p>
<h3>Step 4 — Start the services</h3>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>This starts both services in the background. The <code>triage</code> container will:</p>
<ol>
<li>Wait for IRIS to be ready (up to 120 seconds)</li>
<li>Load the 4 test patients via <code>seed_data.py</code> (first boot only)</li>
<li>Start FHIR MCP Server on port 8000</li>
<li>Start Triage MCP Server on port 8001</li>
<li>Start Clinical Reasoning MCP Server on port 8002</li>
<li>Start Voice Bridge on port 8003 <em>(roadmap — always runs, for testing)</em></li>
<li>Start Gradio UI on port 7860</li>
</ol>
<h3>Step 5 — Verify services are running</h3>
<p><strong>Check container status:</strong></p>
<pre><code class="language-bash">docker compose ps
</code></pre>
<p>Expected output: both <code>fhir-template</code> and <code>triage-app</code> with status <code>Up</code>.</p>
<p><strong>Check all services are healthy:</strong></p>
<pre><code class="language-bash"># Gradio UI
curl -s -o /dev/null -w "%{http_code}" http://localhost:7860
# Expected: 200

# FHIR MCP Server
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/mcp
# Expected: 405 or 406 (server responds to GET, expects POST/SSE)

# IRIS FHIR API (direct)
curl -s -o /dev/null -w "%{http_code}" \
-u _SYSTEM:SYS \
-H "Accept: application/fhir+json" \
http://localhost:32783/fhir/r4/metadata
# Expected: 200

# Voice Bridge (roadmap — always runs for testing)
curl -s http://localhost:8003/health
# Expected: {"status":"ok","service":"triageaide-voice-bridge"}
</code></pre>
<p><strong>Follow startup logs:</strong></p>
<pre><code class="language-bash">docker compose logs -f triage
</code></pre>
<p>Look for these lines indicating all services are up:</p>
<pre><code> Port 8000 ready
 Port 8001 ready
 Port 8001 ready
Port 8002 ready
Starting Voice Bridge on port 8003... *(roadmap)*
Starting Gradio UI on port 7860...
Running on local URL: http://0.0.0.0:7860
</code></pre>
<hr />
<h2>Testing the Application</h2>
<h3>Chat Interface (Text)</h3>
<ol>
<li>Open <strong><a href="http://localhost:7860">http://localhost:7860</a></strong> in your browser</li>
<li>Click the <strong>💬 Chat</strong> tab (active by default)</li>
<li>Type one of the example prompts or click an example chip:</li>
</ol>
<p><strong>English:</strong></p>
<pre><code>Start triage for patient Maria Silva
Triage for patient Joao Santos
Patient Ana Costa history
Triage for patient Roberto Lima
</code></pre>
<p><strong>Português:</strong></p>
<pre><code>Iniciar triagem para Maria Silva
Triagem para paciente João Santos
Histórico do paciente Ana Costa
Triagem para Roberto Lima
</code></pre>
<ol start="4">
<li>
<p>Watch the <strong>Agent Trace</strong> panel on the right — it shows each of the 5 workflow steps in real time as the agent queries FHIR, asks questions, reasons, and writes back to IRIS.</p>
</li>
<li>
<p>Respond to the agent’s questions. After all triage questions are answered, the agent will:</p>
<ul>
<li>Assess clinical risk</li>
<li>Suggest care priority (routine / urgent / emergency)</li>
<li>Generate a clinical summary for the physician</li>
<li>Write back FHIR resources (Encounter, Observation, Flag, Task, QuestionnaireResponse)</li>
</ul>
</li>
</ol>
<p><strong>View modes:</strong></p>
<ul>
<li><strong>Side-by-side</strong> — chat on the left, trace panel on the right</li>
<li><strong>Compact</strong> — trace events inline within the chat</li>
</ul>
<h3>Voice Interface (ElevenLabs) <em>(Roadmap)</em></h3>
<p>Voice interaction is implemented in the backend but the UI tab is hidden by default for the MVP. To enable it, set <code>ENABLE_VOICE_UI=true</code> in <code>.env</code> and restart the container. See <a href="#elevenlabs-voice-integration-roadmap">ElevenLabs Voice Integration</a> for full setup instructions.</p>
<h3>Running Automated Dialogue Tests</h3>
<p>The repository includes dialogue test scripts for each of the 4 test patients:</p>
<pre><code class="language-bash"># Run inside the triage container
docker compose exec triage bash

cd /app

# Test Maria Silva — diabetes + hypertension scenario
python3 test_dialogue_maria_silva.py

# Test Joao Santos — polypharmacy + high risk scenario
python3 test_dialogue_joao_santos.py

# Test Ana Costa — healthy, routine triage
python3 test_dialogue_ana_costa.py

# Test Roberto Lima — COPD + respiratory red flags
python3 test_dialogue_roberto_lima.py
</code></pre>
<h3>CLI Interface</h3>
<p>For terminal-based interactive testing:</p>
<pre><code class="language-bash">docker compose exec triage bash -c "cd /app && python3 cli.py"
</code></pre>
<hr />
<h2>Test Patients</h2>
<p>4 patients with distinct clinical scenarios are loaded via <code>seed_data.py</code>:</p>
<table>
<thead>
<tr>
<th>Patient</th>
<th>Age</th>
<th>Conditions</th>
<th>Expected Scenario</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Maria Silva</strong></td>
<td>58, F</td>
<td>DM2 + Hypertension + HbA1c 8.2%</td>
<td>Uncontrolled diabetes, elevated cardiovascular risk</td>
</tr>
<tr>
<td><strong>Joao Santos</strong></td>
<td>72, M</td>
<td>HF + AFib + DM2 + HTN + CKD stage 3</td>
<td>Polypharmacy, drug interactions, high risk</td>
</tr>
<tr>
<td><strong>Ana Costa</strong></td>
<td>28, F</td>
<td>No active conditions</td>
<td>Generic questions, no red flags, routine priority</td>
</tr>
<tr>
<td><strong>Roberto Lima</strong></td>
<td>65, M</td>
<td>COPD + HTN + Osteoarthritis + Depression + SpO2 93%</td>
<td>Respiratory red flags, severe allergy (anaphylaxis), urgent priority</td>
</tr>
</tbody>
</table>
<h3>Managing Test Data</h3>
<pre><code class="language-bash"># List loaded patients and their FHIR IDs
docker compose exec triage bash -c \
  'cd /app && FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 seed_data.py list'

# Reload all test data (clean + load)
docker compose exec triage bash -c \
  'cd /app && FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 seed_data.py clean \
   && FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 seed_data.py load'
</code></pre>
<blockquote>
<p><strong>Note:</strong> Patient IDs change on each reload. Always refer to patients by name when talking to the agent — it uses <code>search_patients</code> to resolve the ID automatically.</p>
</blockquote>
<hr />
<h2>ElevenLabs Voice Integration <em>(Roadmap — next step)</em></h2>
<blockquote>
<p><strong>Status:</strong> Backend implemented, UI hidden by default in the MVP. Enable with <code>ENABLE_VOICE_UI=true</code> in <code>.env</code>. For the full design specification, see <a href="https://github.com/musketeers-br/TriageAide/blob/master/doc/elevenlabs-integration.md"><code>https://github.com/musketeers-br/TriageAide/blob/master/doc/elevenlabs-integration.md</code></a>.</p>
</blockquote>
<p>The voice interface uses ElevenLabs Conversational AI with a <strong>Custom LLM</strong> configuration. ElevenLabs handles all audio (STT + TTS), and the <code>voice_bridge.py</code> service provides the clinical intelligence by wrapping the existing LangChain agent.</p>
<h3>How it Works</h3>
<pre><code>Patient speaks → ElevenLabs STT → POST /v1/chat/completions → voice_bridge.py
→ LangChain agent → MCP servers → IRIS → response text
→ voice_bridge.py strips markdown → SSE to ElevenLabs → TTS → patient hears response
</code></pre>
<h3>Quick Test (no ElevenLabs account needed)</h3>
<p>The Voice Bridge runs on port 8003 regardless of <code>ENABLE_VOICE_UI</code>. Replace <code>&lt;VOICE_BRIDGE_SECRET&gt;</code> with the value from your <code>.env</code>:</p>
<pre><code class="language-bash"># Streaming response (same format ElevenLabs uses)
curl -X POST http://localhost:8003/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <VOICE_BRIDGE_SECRET>" \
-d '{"messages":[{"role":"user","content":"Iniciar triagem para Maria Silva"}],"stream":true}'

# Non-streaming (full JSON response)
curl -X POST http://localhost:8003/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <VOICE_BRIDGE_SECRET>" \
-d '{"messages":[{"role":"user","content":"Iniciar triagem para Maria Silva"}],"stream":false}'
</code></pre>
<h3>Full Setup Instructions</h3>
<details>
<summary>Click to expand full ElevenLabs setup guide</summary>
<h4>Step 1 — Generate a voice bridge secret</h4>
<pre><code class="language-bash">openssl rand -hex 32
</code></pre>
<p>Add this to <code>python/triage/.env</code>:</p>
<pre><code class="language-bash">VOICE_BRIDGE_SECRET=a3f7c2d1e8b4a9f6...
</code></pre>
<blockquote>
<p><strong>Important:</strong> <code>VOICE_BRIDGE_SECRET</code> must be set <strong>only</strong> in <code>python/triage/.env</code>. Do NOT set it in the <code>docker-compose.yml</code> <code>environment:</code> section — that overrides the <code>.env</code> file value and causes 401 errors.</p>
</blockquote>
<h4>Step 2 — Expose the voice bridge publicly (local development)</h4>
<p>ElevenLabs needs to reach your voice bridge over HTTPS. The project includes a <code>start_tunnel.sh</code> script at the repo root that creates a stable tunnel with a fixed subdomain.</p>
<p><strong>Using <code>start_tunnel.sh</code> (localtunnel with fixed subdomain):</strong></p>
<pre><code class="language-bash">./start_tunnel.sh
</code></pre>
<p><strong>Alternative: ngrok</strong></p>
<pre><code class="language-bash">ngrok http 8003
</code></pre>
<p><strong>After starting the tunnel:</strong></p>
<ol>
<li>Update your <code>.env</code>: <code>VOICE_BRIDGE_URL=https://dark-ways-itch.loca.lt</code> (or your ngrok URL)</li>
<li>In the ElevenLabs dashboard, set the Custom LLM base URL to <code>https://dark-ways-itch.loca.lt/v1</code></li>
<li>Verify the tunnel: <code>curl https://&lt;tunnel-url&gt;/health</code></li>
</ol>
<h4>Step 3 — Create the ElevenLabs agent</h4>
<ol>
<li>Go to <a href="https://elevenlabs.io/conversational-ai">elevenlabs.io/conversational-ai</a> and log in</li>
<li>Click <strong>Create Agent</strong></li>
<li>Select <strong>Custom LLM</strong> as the model type</li>
<li>Configure the agent:</li>
</ol>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>LLM URL</strong></td>
<td><code>https://&lt;your-ngrok-or-public-url&gt;/v1/chat/completions</code></td>
</tr>
<tr>
<td><strong>Authentication</strong></td>
<td>Bearer token → paste your <code>VOICE_BRIDGE_SECRET</code></td>
</tr>
<tr>
<td><strong>System Prompt</strong></td>
<td><em>(leave blank — the bridge sends the full prompt)</em></td>
</tr>
<tr>
<td><strong>First Message</strong></td>
<td><code>Olá! Sou o assistente de triagem do TriageAide. Por favor, me diga seu nome para começarmos.</code></td>
</tr>
<tr>
<td><strong>Voice</strong></td>
<td>Choose a Brazilian Portuguese voice</td>
</tr>
<tr>
<td><strong>Language</strong></td>
<td>Enable auto-detection; add Portuguese (Brazil) and English (US)</td>
</tr>
</tbody>
</table>
<ol start="5">
<li>Click <strong>Save</strong> and note the <strong>Agent ID</strong></li>
</ol>
<blockquote>
<p><strong>Tip:</strong> You can also import the pre-configured agent from <code>11labs/myagent.json</code>.</p>
</blockquote>
<h4>Step 4 — Configure the Agent ID in .env</h4>
<pre><code class="language-bash">ELEVENLABS_AGENT_ID=agent_xxxxxxxxxx
ELEVENLABS_WIDGET_ID=agent_xxxxxxxxxx
ENABLE_VOICE_UI=true
</code></pre>
<p>Restart: <code>docker compose restart triage</code></p>
<h4>Step 5 — Test the voice integration</h4>
<ol>
<li>Open the 🎙️ Voice tab in Gradio</li>
<li>Say: <em>“Olá, quero fazer triagem para Maria Silva”</em></li>
<li>The agent should respond in Portuguese and begin asking triage questions</li>
</ol>
<p><strong>Language switching test:</strong></p>
<ul>
<li>Start with <em>“Hi, triage for Roberto Lima”</em> → agent responds in English</li>
<li>Start with <em>“Olá, triagem para João Santos”</em> → agent responds in Portuguese</li>
</ul>
<h3>Bilingual Support</h3>
<p>The agent automatically detects language from the patient’s speech:</p>
<ul>
<li><strong>Portuguese detected</strong> → all responses in pt-BR</li>
<li><strong>English detected</strong> → all responses in en-US</li>
<li>Language is <strong>sticky per session</strong></li>
</ul>
<h3>ElevenLabs Agent Configuration Reference</h3>
<p>The <code>11labs/myagent.json</code> file is an exported configuration of the ElevenLabs Conversational AI agent.</p>
<p><strong>Key fields:</strong></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>agent_id</code></td>
<td><code>agent_7001kt5n1cv6fj687wvbaxy81r0y</code></td>
<td>Matches <code>ELEVENLABS_AGENT_ID</code> in <code>.env</code></td>
</tr>
<tr>
<td><code>agent.first_message</code></td>
<td><code>&quot;Olá! Como posso ajudar?&quot;</code></td>
<td>Initial greeting</td>
</tr>
<tr>
<td><code>prompt.llm</code></td>
<td><code>&quot;custom-llm&quot;</code></td>
<td>Tells ElevenLabs to call the Voice Bridge</td>
</tr>
<tr>
<td><code>custom_llm.url</code></td>
<td><code>&quot;https://dark-ways-itch.loca.lt/v1&quot;</code></td>
<td>Base URL — ElevenLabs appends <code>/chat/completions</code></td>
</tr>
<tr>
<td><code>tts.model_id</code></td>
<td><code>&quot;eleven_flash_v2_5&quot;</code></td>
<td>TTS model — low latency</td>
</tr>
<tr>
<td><code>turn.turn_timeout</code></td>
<td><code>7</code></td>
<td>Seconds of silence before agent responds</td>
</tr>
</tbody>
</table>
<p><strong>Custom LLM URL format:</strong></p>
<table>
<thead>
<tr>
<th><code>custom_llm.url</code> (base)</th>
<th>Actual endpoint called by ElevenLabs</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>https://dark-ways-itch.loca.lt/v1</code></td>
<td><code>https://dark-ways-itch.loca.lt/v1/chat/completions</code></td>
</tr>
<tr>
<td><code>https://abc123.ngrok-free.app/v1</code></td>
<td><code>https://abc123.ngrok-free.app/v1/chat/completions</code></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>Important:</strong> When changing the tunnel URL, update <code>custom_llm.url</code> in the ElevenLabs dashboard <strong>and</strong> <code>VOICE_BRIDGE_URL</code> in <code>.env</code>.</p>
</blockquote>
</details>
<hr />
<h2>Running Manually (inside the container)</h2>
<p>For debugging individual components:</p>
<pre><code class="language-bash">docker compose exec triage bash
cd /app
</code></pre>
<p>Start each service separately:</p>
<pre><code class="language-bash"># Terminal 1: FHIR MCP Server
FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 fhir_server.py

# Terminal 2: Triage MCP Server
python3 triage_server.py

# Terminal 3: Clinical Reasoning MCP Server
python3 clinical_reasoning_server.py

# Terminal 4: Voice Bridge *(roadmap)*
VOICE_BRIDGE_SECRET=changeme uvicorn voice_bridge:app --host 0.0.0.0 --port 8003

# Terminal 5: Gradio UI
FHIR_BASE_URL=http://iris:52773/fhir/r4 OPENAI_API_KEY=sk-... python3 app.py
</code></pre>
<p>Or use the convenience script:</p>
<pre><code class="language-bash">bash start_servers.sh
</code></pre>
<hr />
<h2>File Structure</h2>
<pre><code>TriageAide/
├── docker-compose.yml              # Two services: iris + triage
├── Dockerfile                      # IRIS for Health image
├── README.md
└── python/triage/
    ├── .env                        # Credentials (NOT tracked in git)
    ├── .env.example                # Template — copy to .env and fill
    ├── requirements.txt            # Python dependencies
    ├── Dockerfile                  # Python 3.12-slim image for triage service
    ├── entrypoint.sh               # Container boot: wait FHIR → seed → start all
├── start_servers.sh # Manual start script (MCP + Voice Bridge + Gradio)
│
├── agent.py # Core agent: get_system_prompt(), create_triage_agent()
├── voice_bridge.py # FastAPI Voice Bridge (port 8003) *(roadmap)*
├── voice_session.py # Per-session state, language detection, TTL eviction *(roadmap)*
    │
    ├── fhir_server.py              # MCP Server 1 — FHIR CRUD (port 8000)
    ├── triage_server.py            # MCP Server 2 — Contextual triage (port 8001)
    ├── clinical_reasoning_server.py # MCP Server 3 — Clinical reasoning (port 8002)
    │
├── app.py # Gradio web UI: Chat tab (+ Voice tab when ENABLE_VOICE_UI=true)
    ├── cli.py                      # Interactive CLI interface
    │
    ├── seed_data.py                # Load / clean / list test patients
    ├── seed_data/                  # FHIR Bundle JSON files
    │   ├── patient_maria_silva.json
    │   ├── patient_joao_santos.json
    │   ├── patient_ana_costa.json
    │   └── patient_roberto_lima.json
    │
    ├── test_dialogue_maria_silva.py
    ├── test_dialogue_joao_santos.py
    ├── test_dialogue_ana_costa.py
    └── test_dialogue_roberto_lima.py
</code></pre>
<hr />
<h2>MCP Servers &amp; Tools</h2>
<h3>fhir_server.py (port 8000) — 12 FHIR CRUD tools</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>FHIR Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>search_patients</code></td>
<td>GET /Patient?name={name}</td>
<td>Search patients by name (partial match)</td>
</tr>
<tr>
<td><code>get_patient</code></td>
<td>GET /Patient/{id}</td>
<td>Demographics</td>
</tr>
<tr>
<td><code>get_patient_conditions</code></td>
<td>GET /Condition?patient={id}</td>
<td>Conditions</td>
</tr>
<tr>
<td><code>get_patient_medications</code></td>
<td>GET /MedicationRequest?patient={id}</td>
<td>Medications</td>
</tr>
<tr>
<td><code>get_patient_observations</code></td>
<td>GET /Observation?patient={id}</td>
<td>Observations</td>
</tr>
<tr>
<td><code>get_patient_allergies</code></td>
<td>GET /AllergyIntolerance?patient={id}</td>
<td>Allergies</td>
</tr>
<tr>
<td><code>get_patient_encounters</code></td>
<td>GET /Encounter?patient={id}</td>
<td>Encounters</td>
</tr>
<tr>
<td><code>create_observation</code></td>
<td>POST /Observation</td>
<td>New observation</td>
</tr>
<tr>
<td><code>create_condition</code></td>
<td>POST /Condition</td>
<td>New condition</td>
</tr>
<tr>
<td><code>create_questionnaire_response</code></td>
<td>POST /QuestionnaireResponse</td>
<td>Structured triage</td>
</tr>
<tr>
<td><code>create_encounter</code></td>
<td>POST /Encounter</td>
<td>Pre-consultation encounter</td>
</tr>
<tr>
<td><code>create_flag_and_task</code></td>
<td>POST /Flag + POST /Task</td>
<td>Alert + follow-up task</td>
</tr>
</tbody>
</table>
<h3>triage_server.py (port 8001) — 6 contextual triage tools</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>build_contextual_questions</code></td>
<td>Generates contextual questions based on FHIR history</td>
</tr>
<tr>
<td><code>get_next_triage_question</code></td>
<td>Returns the next triage question (one at a time) based on history and covered topics</td>
</tr>
<tr>
<td><code>get_all_triage_topics</code></td>
<td>Lists all triage topics for a patient context</td>
</tr>
<tr>
<td><code>parse_symptoms</code></td>
<td>Extracts symptoms, duration, severity from patient text</td>
</tr>
<tr>
<td><code>check_red_flags</code></td>
<td>Cross-references symptoms with existing conditions for warning signs</td>
</tr>
<tr>
<td><code>build_questionnaire_response_data</code></td>
<td>Assembles FHIR QuestionnaireResponse</td>
</tr>
</tbody>
</table>
<h3>clinical_reasoning_server.py (port 8002) — 4 clinical reasoning tools</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>assess_clinical_risk</code></td>
<td>Risk score with justification</td>
</tr>
<tr>
<td><code>suggest_priority</code></td>
<td>Care priority (routine/urgent/emergency)</td>
</tr>
<tr>
<td><code>generate_clinical_summary</code></td>
<td>Summary for the physician</td>
</tr>
<tr>
<td><code>identify_follow_up_tasks</code></td>
<td>Follow-up tasks based on risk and care gaps</td>
</tr>
</tbody>
</table>
<hr />
<h2>Observability</h2>
<h3>Gradio Trace Panel</h3>
<p>The Gradio UI includes a <strong>trace panel</strong> that shows agent step progress in real-time. Each tool call is mapped to one of the 5 workflow steps with visual indicators:</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>Icon</th>
<th>Tools</th>
</tr>
</thead>
<tbody>
<tr>
<td>FHIR Query</td>
<td>📋</td>
<td>search_patients, get_patient, get_patient_*</td>
</tr>
<tr>
<td>Triage Questions</td>
<td>💬</td>
<td>get_next_triage_question, parse_symptoms</td>
</tr>
<tr>
<td>Red Flags Check</td>
<td>🚩</td>
<td>check_red_flags</td>
</tr>
<tr>
<td>Clinical Reasoning</td>
<td>⚕️</td>
<td>assess_clinical_risk, suggest_priority</td>
</tr>
<tr>
<td>FHIR Update</td>
<td>📝</td>
<td>create_*, generate_clinical_summary</td>
</tr>
</tbody>
</table>
<h3>LangSmith Tracing</h3>
<p>To enable <a href="https://smith.langchain.com/">LangSmith</a> tracing for detailed agent inspection:</p>
<ol>
<li>Add <code>LANGSMITH_API_KEY</code> to <code>python/triage/.env</code></li>
<li>Set <code>LANGSMITH_TRACING=true</code> (enabled by default when the key is present)</li>
<li>Set <code>LANGSMITH_PROJECT=triage-aide</code> (or your preferred project name)</li>
</ol>
<p>Traces are automatically sent to LangSmith when the key is configured.</p>
<h3>Service Logs</h3>
<pre><code class="language-bash"># All services (follow)
docker compose logs -f triage

# Individual service logs (inside container)
docker compose exec triage bash -c 'tail -f /tmp/fhir_server.log'
docker compose exec triage bash -c 'tail -f /tmp/triage_server.log'
docker compose exec triage bash -c 'tail -f /tmp/cr_server.log'
docker compose exec triage bash -c 'tail -f /tmp/voice_bridge.log'
</code></pre>
<hr />
<h2>Ports</h2>
<table>
<thead>
<tr>
<th>Host Port</th>
<th>Container Port</th>
<th>Service</th>
</tr>
</thead>
<tbody>
<tr>
<td>7860</td>
<td>7860</td>
<td>Gradio Web UI (Chat tab + Voice tab when ENABLE_VOICE_UI=true)</td>
</tr>
<tr>
<td>8000</td>
<td>8000</td>
<td>FHIR MCP Server</td>
</tr>
<tr>
<td>8001</td>
<td>8001</td>
<td>Triage MCP Server</td>
</tr>
<tr>
<td>8002</td>
<td>8002</td>
<td>Clinical Reasoning MCP Server</td>
</tr>
<tr>
<td>8003</td>
<td>8003</td>
<td>Voice Bridge <em>(roadmap — always runs for testing)</em></td>
</tr>
<tr>
<td>32783</td>
<td>52773</td>
<td>FHIR API — IRIS for Health (direct access)</td>
</tr>
</tbody>
</table>
<hr />
<h2>Tech Stack</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>Technology</th>
</tr>
</thead>
<tbody>
<tr>
<td>FHIR Server</td>
<td>InterSystems IRIS for Health Community Edition</td>
</tr>
<tr>
<td>MCP Servers</td>
<td>FastMCP with streamable-http transport</td>
</tr>
<tr>
<td>Agent</td>
<td>LangChain + langchain-mcp-adapters + OpenAI gpt-4o-mini</td>
</tr>
<tr>
<td>Voice Bridge</td>
<td>FastAPI + uvicorn — OpenAI-compatible endpoint <em>(roadmap)</em></td>
</tr>
<tr>
<td>Voice I/O</td>
<td>ElevenLabs Conversational AI (STT + TTS + WebSocket) <em>(roadmap)</em></td>
</tr>
<tr>
<td>Chat UI</td>
<td>Gradio with Chat tab (+ Voice tab via ENABLE_VOICE_UI) and real-time trace panel</td>
</tr>
<tr>
<td>Observability</td>
<td>LangSmith tracing (optional)</td>
</tr>
<tr>
<td>Language</td>
<td>Python 3.12</td>
</tr>
<tr>
<td>Deploy</td>
<td>Docker Compose (2 services: iris + triage)</td>
</tr>
</tbody>
</table>
<hr />
<h2>Troubleshooting</h2>
<h3>Containers won’t start</h3>
<pre><code class="language-bash"># Check status
docker compose ps

# View full startup logs
docker compose logs triage
docker compose logs iris
</code></pre>
<p>Common causes:</p>
<ul>
<li><strong>IRIS takes too long to start</strong> — IRIS for Health can take 60–120 seconds on first boot. The triage container waits up to 120 seconds. If it times out, run <code>docker compose restart triage</code>.</li>
<li><strong>Port conflict</strong> — If ports 7860, 8000–8003, or 32783 are in use, stop the conflicting process or change the port mapping in <code>docker-compose.yml</code>.</li>
</ul>
<h3>“OPENAI_API_KEY not set” error</h3>
<p>Verify that <code>python/triage/.env</code> exists and contains a valid key:</p>
<pre><code class="language-bash">grep OPENAI_API_KEY python/triage/.env
</code></pre>
<h3>Check if MCP servers are running</h3>
<pre><code class="language-bash">docker compose exec triage bash -c 'cat /tmp/fhir_server.log'
docker compose exec triage bash -c 'cat /tmp/triage_server.log'
docker compose exec triage bash -c 'cat /tmp/cr_server.log'
</code></pre>
<h3>Restart MCP servers manually</h3>
<pre><code class="language-bash">docker compose exec triage bash -c \
  'pkill -f fhir_server.py; pkill -f triage_server.py; pkill -f clinical_reasoning_server.py'
docker compose exec triage bash -c 'cd /app && bash start_servers.sh'
</code></pre>
<h3>Reload test data</h3>
<pre><code class="language-bash">docker compose exec triage bash -c \
  'cd /app \
   && FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 seed_data.py clean \
   && FHIR_BASE_URL=http://iris:52773/fhir/r4 python3 seed_data.py load'
</code></pre>
<h3>Port 7860 not accessible</h3>
<ol>
<li><code>docker compose ps</code> — confirm <code>triage-app</code> is running</li>
<li>Check the triage log: <code>docker compose logs triage</code></li>
<li>Verify port mapping: <code>docker inspect triage-app | grep -A5 Ports</code></li>
</ol>
<h3>Voice Bridge not responding <em>(Roadmap)</em></h3>
<pre><code class="language-bash"># Check if it started
docker compose exec triage bash -c 'cat /tmp/voice_bridge.log'

# Test directly
curl http://localhost:8003/health

# Check it's listening
docker compose exec triage bash -c 'ss -tlnp | grep 8003'
</code></pre>
<p>If the log shows an error, the bridge may have failed to initialize the agent (MCP servers must be running first). Restart with:</p>
<pre><code class="language-bash">docker compose exec triage bash -c \
  'pkill -f "uvicorn voice_bridge"; cd /app \
   && uvicorn voice_bridge:app --host 0.0.0.0 --port 8003 >> /tmp/voice_bridge.log 2>&1 &'
</code></pre>
<h3>ElevenLabs gets 401 Unauthorized <em>(Roadmap)</em></h3>
<p>The <code>VOICE_BRIDGE_SECRET</code> in your <code>.env</code> must match exactly what you configured as the Bearer token in the ElevenLabs agent settings. Re-check both values:</p>
<pre><code class="language-bash">grep VOICE_BRIDGE_SECRET python/triage/.env
</code></pre>
<p>Also verify the value the container is actually using — if it shows <code>changeme</code> or a different value, the <code>docker-compose.yml</code> <code>environment:</code> section may be overriding <code>.env</code>:</p>
<pre><code class="language-bash">docker compose exec triage bash -c 'echo $VOICE_BRIDGE_SECRET | wc -c'
# Should match the length of your secret (64 chars + newline for a hex-32 secret)
</code></pre>
<p>The <code>.env</code> file must be the <strong>only</strong> source of <code>VOICE_BRIDGE_SECRET</code>. Do not duplicate it in the <code>docker-compose.yml</code> <code>environment:</code> section, because <code>environment:</code> takes precedence over <code>env_file:</code> and silently overrides the value.</p>
<h3>ElevenLabs can’t reach the voice bridge <em>(Roadmap)</em></h3>
<ul>
<li>Make sure the tunnel is running: <code>./start_tunnel.sh</code> (or <code>ngrok http 8003</code>)</li>
<li>If using <code>start_tunnel.sh</code>, verify the fixed URL: <code>curl https://dark-ways-itch.loca.lt/health</code></li>
<li>The ngrok URL changes every restart (free plan). Update <code>VOICE_BRIDGE_URL</code> in <code>.env</code> and in the ElevenLabs agent configuration.</li>
<li>For persistent URLs, use <a href="https://ngrok.com/blog-post/free-static-domains-ngrok-users">ngrok’s static domains</a> (free tier: 1 static domain).</li>
</ul>
<h3>Agent responds in the wrong language <em>(Roadmap — voice only)</em></h3>
<p>Language detection is heuristic-based. If the agent responds in the wrong language, check <code>voice_bridge.log</code> for the detected language. You can override by adding an explicit instruction in your first message: <em>“Please respond in English”</em> or <em>“Por favor, responda em português”</em>.</p>
<h3>Dependency changes are lost on container restart</h3>
<p>Dependencies are installed in <code>python/triage/Dockerfile</code>. If you manually install packages inside the container, they will be lost on restart. Always add new packages to both <code>requirements.txt</code> and <code>Dockerfile</code>, then rebuild:</p>
<pre><code class="language-bash">docker compose build --no-cache triage
docker compose up -d triage
</code></pre>
Version
1.0.006 Jun, 2026
Ideas portal
Category
Technology Example
Works with
InterSystems IRISInterSystems IRIS for Health
First published
06 Jun, 2026
Last edited
06 Jun, 2026