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.
Following one patient question through the system:
- The patient asks something in the consultation console. The browser
calls
POST /api/chat/streamwith the session cookie. - MediMergent asks TextSentry (Fraunhofer's conversational intelligence engine) for the passages that answer the question, drawn from the trial's own documents.
- MediMergent builds a prompt from the trial's curated record plus those passages, and asks the LLM (AWS Bedrock) to write the reply.
- Sentences stream back to the browser as they are ready; the browser speaks them through the avatar.
- 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
| Service | What we use it for | When |
|---|---|---|
| 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
| Service | What we use it for |
|---|---|
| TextSentry | Fraunhofer's conversational intelligence engine, the cognitive
core behind Carmen.
|
| 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:
| Stack | Owns | Notes |
|---|---|---|
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/....
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.
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 payload | Meaning |
|---|---|
{"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": ...}:
| Status | When |
|---|---|
400 | Missing or invalid input (e.g. empty message, unknown document type) |
401 | No session cookie (patient routes) or bad Basic credentials (admin routes) |
404 | Trial, document, conversation, or invite not found |
409 | Creating a trial whose ID already exists |
410 | Invite expired or already used (HTML pages) |
422 | Body failed schema validation; detail is a pydantic error list |
500 | Backend failure (LLM error, indexing failure on activate, storage error) |
502 | Upstream 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.
| Area | Routes | Auth |
|---|---|---|
| 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
| Variable | Default | Selects |
|---|---|---|
TRIALS_BACKEND / TRIALS_DIR | file / /app/trials | Trial storage (file vs DynamoDB) |
SESSION_BACKEND, DOCUMENTS_BACKEND, CHAT_BACKEND, CALLS_BACKEND, INVITE_BACKEND | memory | Per-repository memory vs DynamoDB (DYNAMODB_TABLE_NAME, AWS_REGION) |
FILE_STORAGE_BACKEND / DOCUMENTS_BUCKET | local disk | Document file storage (disk vs S3) |
LLM_BACKEND | set per deployment | The LLM provider (bedrock for AWS Bedrock) |
STT_BACKEND / TTS_BACKEND | set per deployment | Voice providers (transcribe / polly for AWS) |
TEXTSENTRY_URL (and its access credentials) | set by the deployment | Where the retrieval service runs |
ADMIN_PASSWORD | dev default in code | Basic-auth password |
LIVEAVATAR_API_KEY | none | Avatar video (only needed for that feature) |