MongoDB Atlas + Temporal MVP for durable clinical-trial matching with vector search, GeoJSON filtering, crash recovery, and human approval.
Important
This repository is a synthetic technical demo, not a clinical application. It must not be used for diagnosis, treatment decisions, or real patient care.
Most AI demos prove that an LLM can answer a question. Durable Trial Match demonstrates what happens when the application also has to survive failures, apply hard operational constraints, and wait safely for a human decision.
The split is intentionally simple:
- MongoDB Atlas owns operational data, GeoJSON, structured eligibility filters, Vector Search, match state, and Change Streams.
- Voyage AI generates document and query embeddings.
- Temporal owns durable ingestion, retries, crash recovery, workflow history, and human-in-the-loop waiting.
- LLM (optional) normalizes the synthetic EHR text and generates the final demo summary.
Semantic similarity should not decide whether a trial is physically reachable or whether a patient satisfies hard eligibility constraints.
Synthetic patient
β
βββ age / stage / recruiting status
β
βββ GeoJSON location + radius
β
βΌ
MongoDB $geoNear
β
nearby trialIds
β
βΌ
MongoDB $vectorSearch
strict metadata filter
β
βΌ
semantic ranking
Trial locations live in trial_sites as GeoJSON Point values with a 2dsphere index.
The application first uses $geoNear to resolve nearby trial IDs. Those IDs are then passed into the $vectorSearch pre-filter together with strict metadata conditions such as:
{
"$and": [
{ "trialId": { "$in": nearbyTrialIds } },
{ "minAge": { "$lte": patientAge } },
{ "maxAge": { "$gte": patientAge } },
{ "stage": { "$eq": patientStage } },
{ "status": { "$eq": "RECRUITING" } }
]
}This is deliberately two MongoDB queries because $vectorSearch must be the first stage of the aggregation pipeline in which it appears.
Code: activities/search_clinical_trials.py
The Temporal workflow exposes five meaningful boundaries:
1. Extract EHR
2. Embed patient query
3. Search clinical trials
4. Check drug interactions
5. Generate summary
Enable the demo failure:
DEMO_FAIL_DRUG_CHECK_ONCE=trueThe first attempt at step 4 fails:
β Extract EHR
β Embed patient query
β Search clinical trials
β Check drug interactions β injected timeout
Temporal retries from the failed Activity boundary. Completed Activity results are already recorded in Workflow history, so the completed extraction, embedding, and search calls are not needlessly executed again.
β Extract EHR
β Embed patient query
β Search clinical trials
β Check drug interactions β retry succeeds
β Generate summary
For the more dramatic version, set a longer DEMO_DRUG_CHECK_DELAY_SECONDS, kill the worker during step 4, and restart it.
Code: workflows/match_patient.py and activities/check_drug_interactions.py
After a recommendation is generated, the workflow writes:
{ "status": "AWAITING_PHYSICIAN_APPROVAL" }to MongoDB and waits for a Temporal Signal.
await workflow.wait_condition(lambda: self.approval is not None)The review action sends APPROVE or REJECT. Temporal resumes the durable workflow, updates MongoDB, and MongoDB Change Streams expose the business-state transition.
Temporal Signal
β
βΌ
Workflow resumes
β
βΌ
MongoDB matches.status = APPROVED
β
βΌ
Change Stream
β
βΌ
Observer / UI sees the change
Code: client/approve_match.py and app/change_stream.py
βββββββββββββββββββββββββ
β Streamlit UI β
β Match + Review β
βββββββββββββ¬ββββββββββββ
β
workflow / signal
β
βΌ
βββββββββββββββββββββββββ
β Temporal β
β β
β IngestTrialWorkflow β
β MatchPatientWorkflow β
βββββββββββββ¬ββββββββββββ
β
βββββββββββββββββββββββββΌβββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββ ββββββββββββββββββββ βββββββββββββββ
β Voyage AI β β MongoDB Atlas β β LLM β
β embeddings β β β β optional β
βββββββββββββββ β patients β βββββββββββββββ
β raw_trials β
β trial_chunks β
β trial_sites β
β matches β
β β
β 2dsphere β
β Vector Search β
β Change Streams β
ββββββββββββββββββββ
durable-trial-match/
βββ README.md
βββ assets/banner.svg
βββ requirements.txt
βββ config.py
βββ worker.py
βββ activities/
βββ workflows/
βββ client/
βββ app/
βββ scripts/
βββ data/
No separate giant design .md file. The architecture is represented by the code that implements it.
Everything is intentionally in one place: config.py.
MONGODB_URI = "mongodb+srv://..."
MONGODB_DB = "durable_trial_match"
VOYAGE_API_KEY = "..."
VOYAGE_MODEL = "voyage-4"
mongo_client = MongoClient(MONGODB_URI)
db = mongo_client[MONGODB_DB]
voyage_client = voyageai.Client(api_key=VOYAGE_API_KEY)There is no .env, getenv, or separate database helper in this MVP.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtOpen config.py and paste the demo values directly:
MONGODB_URI = "mongodb+srv://..."
VOYAGE_API_KEY = "..."config.py creates the shared MongoDB database handle and Voyage client used by the demo.
You can either put the Atlas URI in .env:
MONGODB_URI="mongodb+srv://..."
MONGODB_DB="durable_trial_match"and verify it:
python scripts/check_atlas.pyor launch Streamlit and enter the Atlas connection string directly in the sidebar. The UI performs an Atlas ping before loading any MongoDB-backed screens.
The Temporal worker and command-line scripts still use
.env, so for the full end-to-end demo setMONGODB_URIthere as well.
temporal server start-devpython scripts/seed_data.py
python scripts/create_indexes.pypython worker.pypython scripts/ingest_trials.pypython client/start_match.py patient-demo-001 --radius 50Or launch the UI:
streamlit run app/ui.pypython app/change_stream.pypython client/approve_match.py <workflow-id> approve "Dr. Demo"or:
python client/approve_match.py <workflow-id> reject "Dr. Demo"| Collection | Purpose |
|---|---|
patients |
Synthetic operational patient profiles |
raw_trials |
Source documents before durable ingestion |
trial_chunks |
Embedded trial text + strict metadata used by Vector Search |
trial_sites |
GeoJSON recruiting locations |
matches |
Application-visible workflow/result state |
The ingestion workflow produces deterministic chunk IDs:
trialId:version:chunk:NNN
Each write uses an upsert. If Temporal retries a MongoDB Activity, it converges on the same logical record instead of creating duplicate vectors.
The included data intentionally contains:
- a nearby, highly relevant eligible trial,
- a strong semantic match with the wrong stage,
- a matching trial hundreds of miles away,
- a nearby trial outside the patient's age range,
- a matching trial that is closed,
- weaker broad-condition matches,
- multiple recruiting sites for the same trial.
That makes hard filtering vs semantic ranking visible during the demo.
Terminal 1 β temporal server start-dev
Terminal 2 β python3.10 worker.py
Terminal 3 β python3.10 -m streamlit run app/ui2.py
MongoDB owns the operational truth and retrieval. Temporal owns reliable execution across that truth.