Developer Guide

Everything you need to build against (or hack on) MediMergent: how the system fits together, how to authenticate, and the request flows that matter, with copy-pasteable examples.

The big picture

New to the codebase? Start here. Two sections give you the mental model: what the moving parts are, and which cloud services sit behind them.

Architecture

The short version: one FastAPI service in the middle. Patients and staff talk to MediMergent; MediMergent talks to a RAG service for document search, to AWS for storage, and to AI providers for the actual chat, voice, and avatar.

Patient browser consultation console Staff browser admin panel + dashboard MediMergent API FastAPI, port 8200 session cookie Basic auth TextSentry conversational intelligence engine Semantic understanding meaning, not keywords Grounded answers from the trial's own documents, in near real time Call analysis summary, sentiment, eligibility index content, retrieve, analyze Storage (AWS) DynamoDB trials, sessions, chats, calls, invites, doc records S3 uploaded trial documents (memory/file backends locally) records + files AI services Chat replies AWS Bedrock Voice AWS Polly (TTS) + Transcribe (STT) Avatar LiveAvatar video sessions chat, voice, avatar
High-level architecture. Every arrow is plain HTTPS except the browser mic stream, which is a WebSocket.

Following one patient question through the system:

  1. The patient asks something in the consultation console. The browser calls POST /api/chat/stream with the session cookie.
  2. MediMergent asks TextSentry (Fraunhofer's conversational intelligence engine) for the passages that answer the question, drawn from the trial's own documents.
  3. MediMergent builds a prompt from the trial's curated record plus those passages, and asks the LLM (AWS Bedrock) to write the reply.
  4. Sentences stream back to the browser as they are ready; the browser speaks them through the avatar.
  5. The conversation is stored (DynamoDB in production, memory locally) so the dashboard can show it later.

Cloud services

Plain-English inventory of every external service, what we use it for, and when it's actually needed. Locally you can run with none of the AWS ones: every repository falls back to memory or files.

AWS

ServiceWhat we use it forWhen
DynamoDB One table stores all records: trials, document metadata, sessions, chat history, call records, invites. Production (*_BACKEND=dynamodb)
S3 The original uploaded trial documents (PDFs, Word files). Bucket comes from DOCUMENTS_BUCKET. Production (FILE_STORAGE_BACKEND=s3)
Bedrock The LLM that writes Carmen's replies. LLM_BACKEND=bedrock
Polly Text-to-speech (Carmen's voice). TTS_BACKEND=polly
Transcribe Speech-to-text for the patient's mic, streamed through the WS /api/stt/stream proxy. STT_BACKEND=transcribe
ECS Fargate + ECR Where the deployed service runs: CI builds the Docker image, pushes it to ECR, and a CDK stack runs it on Fargate. Deployment only

Everything else

ServiceWhat we use it for
TextSentry Fraunhofer's conversational intelligence engine, the cognitive core behind Carmen.
  • Builds the trial documents into a knowledge base it can reason over
  • Understands patient questions by meaning, not keywords
  • Grounds every answer in the trial's own documents, in near real time
  • Analyzes each finished call: summary, sentiment, eligibility readout
  • Knowledge base lives on Fraunhofer-side infrastructure; original files and all records stay in MediMergent's storage
  • Proprietary Fraunhofer technology behind a secure API
LiveAvatar Carmen's face: the browser gets a short-lived session token from us, then streams avatar video directly from LiveAvatar.

Infrastructure

The application infrastructure is managed with AWS CDK in infra/. The CDK app composes five stacks:

StackOwnsNotes
MediMentor-VPC-{environment} VPC, public subnets, private egress subnets, NAT gateway Two availability zones; app tasks run in private subnets.
MediMentor-ECS-{environment} ECS cluster, Cloud Map namespace, public ALB, HTTP/HTTPS listeners HTTP redirects to HTTPS when a certificate ARN is configured.
MediMentor-DynamoDB-{environment} carmen-db-{environment} Single-table DynamoDB store with PK and SK; retained on stack destroy.
MediMentor-S3-{environment} medimentor-documents-{environment} Versioned, encrypted, private bucket for uploaded trial documents; retained on stack destroy.
MediMentor-Service-{environment} MediMergent Fargate service, task definition, target group, logs, IAM grants Runs medimergent-service on container port 8200.

In the current production context, the service image comes from 878769271170.dkr.ecr.us-east-1.amazonaws.com/medimentor/carmen, TextSentry is reached at https://textsentry.fhusa.xyz, and the default runtime backends are AWS-native: LLM_BACKEND=bedrock, TTS_BACKEND=polly, and STT_BACKEND=transcribe. Application records are stored in DynamoDB, document files in S3, and secrets such as LIVEAVATAR_API_KEY, TEXTSENTRY_API_KEY, and AUTH_SECRET_KEY are read from SSM Parameter Store under /carmen/production/env/....

Deployment split: CDK controls changes in the AWS infrastructure. Routine app releases can update the existing ECS service by publishing an immutable ECR tag and using deploy/deploy-service.sh, which updates the task-definition and waits for ECS stability without changing the infrastructure stacks.

Key commands to setup the infrastructure

# Work from infra/

# Confirm the AWS account/profile before making changes.
aws sts get-caller-identity --profile MediMergentDeploy

# One-time CDK bootstrap for the production account and region.
cdk bootstrap aws://878769271170/us-east-1 \
  --profile MediMergentDeploy \
  --qualifier fhusa \
  --cloudformation-execution-policies arn:aws:iam::878769271170:policy/MediMentorCDKAccess \
  --termination-protection \

# One-time secure runtime parameters read by the ECS task.
aws ssm put-parameter --profile MediMergentDeploy --region us-east-1 \
  --name /carmen/production/env/LIVEAVATAR_API_KEY \
  --type SecureString --value '...' --overwrite
aws ssm put-parameter --profile MediMergentDeploy --region us-east-1 \
  --name /carmen/production/env/TEXTSENTRY_API_KEY \
  --type SecureString --value '...' --overwrite

# Review, then deploy or update the CDK-managed infrastructure stacks.
cdk diff --all --profile MediMergentDeploy
cdk deploy --all --profile MediMergentDeploy
      

Key service deployment commands

# Copy an immutable app image into the production ECR repository.
# Note: We use skopeo for convenience to copy and re-tag an image from one repo to another 
aws ecr get-login-password --region us-east-1 --profile TextSentryAdmin \
  | skopeo login --username AWS --password-stdin 608380991622.dkr.ecr.us-east-1.amazonaws.com

aws ecr get-login-password --region us-east-1 --profile MediMergentDeploy \
  | skopeo login --username AWS --password-stdin 878769271170.dkr.ecr.us-east-1.amazonaws.com

skopeo copy \
  --override-os linux \
  --override-arch amd64 \
  --format v2s2 \
  docker://608380991622.dkr.ecr.us-east-1.amazonaws.com/carmen/medimergent:<src-tag> \
  docker://878769271170.dkr.ecr.us-east-1.amazonaws.com/medimentor/carmen:<dst-tag>

# Roll the existing ECS service to that image without changing CDK stacks.
./deploy/deploy-service.sh \
  --cluster '<ecs-cluster-name>' \
  --service medimergent-service \
  --task-definition '<task-definition-family>' \
  --image 878769271170.dkr.ecr.us-east-1.amazonaws.com/medimentor/carmen:<dst-tag> \
  --region us-east-1 \
  --profile MediMergentDeploy

The API contract

You've seen the parts; here is the formal description of the HTTP surface between them.

The specification

The API spec is docs/medimergent-openapi.yaml. It documents every route with request/response schemas, examples, auth requirements, and error responses. Browse it at /api-docs, or point Postman and code generators at /docs/openapi.yaml.

Keep the spec in sync. If you add or change a route, update docs/medimergent-openapi.yaml in the same PR.

Authentication

There are exactly two ways in: patients carry a session cookie, staff and scripts use Basic auth. That's the whole model.

Patient session cookie

Everything a patient touches (/api/chat*, /api/greeting, /api/tts/pcm, /api/trial, tokens, ...) requires the carmen_session cookie: an opaque ID with a 7-day TTL, bound server-side to a conversation and a trial. There is no login. The cookie is minted when a one-time invite is redeemed:

# 1. Redeem an invite (what the browser does when the patient presses start)
curl -i -X POST https://HOST/invite/TOKEN/redeem
#   -> 303 See Other
#   -> Set-Cookie: carmen_session=...; Max-Age=604800; HttpOnly; SameSite=Lax

# 2. Use the cookie on every API call
curl -b "carmen_session=SESSION_ID" https://HOST/api/greeting

Missing or expired cookie means 401 {"detail": "Active Carmen session required"}. POST /api/conversation/end expires the session and clears the cookie. For development, an admin can mint a session without an invite via POST /admin/test-session (form field trial_id).

Admin Basic auth

Everything under /api/admin/*, plus /api/calls, /api/conversations*, /dashboard and the admin pages, uses HTTP Basic auth. Username is always admin; the password comes from the ADMIN_PASSWORD environment variable.

curl -u admin:$ADMIN_PASSWORD https://HOST/api/admin/trials

Core flows

These are the request sequences you'll actually write. Copy them into a terminal, replace HOST and the credentials, and go.

Onboard a trial (admin)

The API version of the five-step pipeline from the usage guide: create, upload, extract, correct, activate, then mint an invite.

BASE=https://HOST; AUTH="admin:$ADMIN_PASSWORD"

# create a draft
curl -u $AUTH -X POST $BASE/api/admin/trials \
     -H 'Content-Type: application/json' \
     -d '{"trial_id": "acadabra-her2-nsclc"}'

# upload a document (multipart; optional document_type)
curl -u $AUTH -X POST $BASE/api/admin/trials/acadabra-her2-nsclc/documents \
     -F 'file=@protocol-synopsis.pdf' \
     -F 'document_type=Protocol synopsis'

# extract structured sections from all uploaded documents (LLM)
curl -u $AUTH -X POST $BASE/api/admin/trials/acadabra-her2-nsclc/extract \
     -H 'Content-Type: application/json' -d '{"document_ids": null}'

# correct a section by hand (any edit flips the trial to draft)
curl -u $AUTH -X PUT $BASE/api/admin/trials/acadabra-her2-nsclc/sections/general \
     -H 'Content-Type: application/json' \
     -d '{"trial_name": "ACADABRA: Acadabra in HER2-Mutant Advanced NSCLC", "phase": "Phase II"}'

# activate = index all documents + flip to active (fails atomically)
curl -u $AUTH -X POST $BASE/api/admin/trials/acadabra-her2-nsclc/activate

# mint a patient invite (7 days, single use)
curl -u $AUTH -X POST $BASE/api/admin/invites \
     -H 'Content-Type: application/json' \
     -d '{"trial_id": "acadabra-her2-nsclc", "label": "Pt. 0142"}'
# -> { "token": "...", "url": "https://HOST/invite/...", "status": "available", ... }

Hold a consultation (patient session)

What the consultation console does, reduced to four calls.

# redeem the invite, then grab the cookie from Set-Cookie
curl -i -X POST $BASE/invite/TOKEN/redeem
C="carmen_session=SESSION_ID"

curl -b $C $BASE/api/greeting                     # scripted greeting (recorded)
curl -b $C -X POST $BASE/api/chat \
     -H 'Content-Type: application/json' \
     -d '{"message": "How long does each infusion take?", "response_style": "brief"}'
curl -b $C $BASE/api/chat/history                 # full transcript so far
curl -b $C -X POST $BASE/api/conversation/end     # analyze + store call, kill session

Streaming chat (SSE)

The console uses the streaming variant so Carmen starts speaking after the first sentence instead of the whole reply. POST /api/chat/stream returns text/event-stream; each data: line is one of:

Event payloadMeaning
{"sentence": "...", "spoken": "..."} One complete sentence. sentence is for display, spoken for TTS (keeps inline SSML).
{"type": "meta", "phase": "post", ...} Response stats (readability grade, latency, sentence count), only when the request set include_meta.
{"error": "..."} Backend failure mid-stream.
[DONE]Literal terminator string.

Aborting the request mid-stream (patient barge-in) stores the partial reply suffixed with the literal marker (interrupted). When the interruption happens during audio playback (after the text stream finished), the client reports the heard prefix to POST /api/chat/interrupted instead, and the stored turn is trimmed to match. Transcripts read exactly what the patient heard.

Reference

Quick lookups: what errors look like, and where every route lives.

Errors

All errors use FastAPI's envelope, {"detail": ...}:

StatusWhen
400Missing or invalid input (e.g. empty message, unknown document type)
401No session cookie (patient routes) or bad Basic credentials (admin routes)
404Trial, document, conversation, or invite not found
409Creating a trial whose ID already exists
410Invite expired or already used (HTML pages)
422Body failed schema validation; detail is a pydantic error list
500Backend failure (LLM error, indexing failure on activate, storage error)
502Upstream service rejected the call (extraction LLM, voice service, LiveAvatar)

Route map

The complete list with schemas and examples is in the spec; this is the orientation table.

AreaRoutesAuth
Health GET/api/health none
Session GET/invite/{token} · POST/invite/{token}/redeem · POST/api/conversation/end none / cookie
Chat POST/api/chat · POST/api/chat/stream · POST/api/chat/interrupted · GET/api/chat/history · GET/api/greeting · POST/api/clear · POST/api/chat/reset cookie
RAG POST/api/query · POST/api/index cookie
Voice & avatar POST/api/tts/pcm · GET/api/avatar/token · GET/api/speech/token · WS /api/stt/stream cookie
Trial (patient) GET/api/trial cookie
Admin: trials GET/POST/api/admin/trials · GET/PUT/DEL/api/admin/trials/{id} · PUT…/sections/{section} · POST…/extract · POST…/index · POST…/activate · POST…/deactivate · criteria add/delete Basic
Admin: documents POST/GET…/documents · GET…/documents/{id}/content · DEL…/documents/{id} Basic
Admin: invites GET/POST/api/admin/invites Basic
Admin: review GET/api/calls · GET/api/conversations · GET/api/conversations/{id} Basic
Admin: maintenance POST/api/admin/reset-vectors · POST/api/admin/reset-all Basic

Development

Two ways to run it. Docker Compose gives you the full stack (MediMergent plus its supporting services) in one command; bare uvicorn is fastest for API-only work.

Local setup

Option A: the full stack with Docker Compose (from the repo root):

docker compose up
# MediMergent on :8200; the supporting services start alongside it.
# src/ and web/ are volume-mounted with --reload: edit and refresh.

Option B: just the API with uvicorn:

cd medimergent
pip install -r requirements.txt
TRIALS_DIR=trials uvicorn medimergent.app:app --app-dir src --port 8200 --reload

# tests
PYTHONPATH=src python -m pytest tests/ -v

With no backend variables set, everything falls back to local storage. No AWS account is needed to hack on the API.

Environment variables

VariableDefaultSelects
TRIALS_BACKEND / TRIALS_DIRfile / /app/trialsTrial storage (file vs DynamoDB)
SESSION_BACKEND, DOCUMENTS_BACKEND, CHAT_BACKEND, CALLS_BACKEND, INVITE_BACKENDmemoryPer-repository memory vs DynamoDB (DYNAMODB_TABLE_NAME, AWS_REGION)
FILE_STORAGE_BACKEND / DOCUMENTS_BUCKETlocal diskDocument file storage (disk vs S3)
LLM_BACKENDset per deploymentThe LLM provider (bedrock for AWS Bedrock)
STT_BACKEND / TTS_BACKENDset per deploymentVoice providers (transcribe / polly for AWS)
TEXTSENTRY_URL (and its access credentials)set by the deploymentWhere the retrieval service runs
ADMIN_PASSWORDdev default in codeBasic-auth password
LIVEAVATAR_API_KEYnoneAvatar video (only needed for that feature)