Skip to content

Repository files navigation

MedMap India

Agentic Healthcare Intelligence System

Multi-agent system that navigates 10,000+ Indian medical facility records to reduce Discovery-to-Care time and identify medical deserts.

Built for the Databricks Hackathon — Challenge 03: Serving A Nation.


Architecture

User query (symptom text + location)
│
▼
┌───────────────┐
│ Guardrails │ ← blocks prompt injection, enforces max length
└───────┬───────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Orchestrator │
│ (asyncio pipeline + audit log) │
└──────┬──────────────┬────────────────┬────────────┘
│ │ │
▼ ▼ ▼
TriageAgent SearchAgent GeoAgent
(symptoms → (vector RAG (geocode,
urgency, over 10k+ haversine,
specialties) facilities) desert detect)
│ │ │
└──────────────┼────────────────┘
▼
IDPAgent (capability
schema normalization +
evidence extraction)
│
▼
TrustScorerAgent
(30% completeness +
30% consistency +
20% evidence score +
20% recency + CI)
│
▼
ValidatorAgent
(self-correction,
standards check)
│
▼
Synthesis + FastAPI
(REST response with trace
+ Flutter mobile UI)

File Structure

medmap-india/
├── backend/
│ ├── agents/
│ │ ├── orchestrator.py ← coordinator, asyncio pipeline, audit
│ │ ├── triage_agent.py ← symptom → clinical requirements
│ │ ├── search_agent.py ← semantic retrieval over 10k rows
│ │ ├── trust_scorer_agent.py ← weighted trust score + confidence intervals
│ │ ├── geo_agent.py ← geocoding, distances, desert detection
│ │ ├── idp_agent.py ← capability schema normalization
│ │ └── validator_agent.py ← self-correction, standards check
│ ├── core/
│ │ ├── config.py ← all env vars (env-overridable)
│ │ ├── schemas.py ← Pydantic models (agent contracts)
│ │ ├── data_loader.py ← dataset → RawFacility[]
│ │ ├── data_quality.py ← validation checkpoint (DataQualityError)
│ │ ├── vector_store.py ← FAISS (local) / Databricks (prod)
│ │ ├── llm_factory.py ← OpenAI / Anthropic / Databricks LLM
│ │ ├── evidence_extractor.py ← regex evidence extraction + contradictions
│ │ ├── capability_schema.py ← FacilityCapabilities normalized model
│ │ ├── desert_analysis.py ← specialized desert analysis (6 types)
│ │ ├── guardrails.py ← prompt injection protection
│ │ └── audit_logger.py ← JSONL audit trail per trace_id
│ ├── api/
│ │ └── routes.py ← FastAPI endpoints
│ └── main.py ← app entry point
├── frontend/
│ └── lib/
│ └── medmap_screen.dart ← Flutter UI with triage conversation
├── scripts/
│ ├── validate_dataset.py ← CLI data quality check
│ ├── build_index.py ← CLI FAISS index builder
│ └── export_from_databricks.py ← Unity Catalog → Parquet export template
├── tests/
│ ├── test_guardrails.py
│ ├── test_data_quality.py
│ ├── test_trust_scorer.py
│ ├── test_data_loader.py
│ ├── test_geo_agent.py
│ └── test_api.py
├── data/
│ └── (place dataset here — see below)
├── requirements.txt
├── .env.example
├── Makefile
├── SECURITY.md
├── MODEL_CARD.md
└── README.md

Setup

1. Install dependencies

python -m venv venv
source venv/bin/activate # Linux/Mac# venv\Scripts\activate # Windows
pip install -r requirements.txt
# or: make install

2. Configure environment

cp .env.example .env
# Edit .env — minimum required:# LLM_PROVIDER=openai# OPENAI_API_KEY=sk-...# DATASET_PATH=data/facilities_clean.parquet

3. Place the dataset

# Copy the VF Hackathon Dataset to data/# Supported formats: .xlsx, .parquet

4. Validate data (optional but recommended)

python scripts/validate_dataset.py
# or: make validate-data

Checks: required columns present, unique IDs, valid lat/lon ranges, non-empty names, trust scores in [0, 100].

5. Build the vector index

python scripts/build_index.py
# or: make build-index

Builds the FAISS index at data/cache/faiss_index. Skip this step if data/cache/faiss_index already exists — the backend also builds it automatically on first startup.

6. Start the backend

python -m backend.main
# or: make run# or: uvicorn backend.main:app --reload --port 8000

7. Run tests

pytest -q
# or: make test

8. Open the frontend

The Flutter app connects to http://localhost:8000 by default. Interactive API docs: http://localhost:8000/docs


API Endpoints

MethodPathDescription
GET/api/healthLiveness check
POST/api/queryMain agent pipeline
GET/api/facilitiesBrowse facilities (paginated)
GET/api/facilities/{id}Single facility detail
GET/api/desertsMedical desert summary by state
GET/api/deserts/specializedSpecialized desert query (dialysis, NICU, etc.)
POST/api/build-indexRebuild vector index

Example query

curl -X POST http://localhost:8000/api/query \
-H "Content-Type: application/json" \
-d '{ "symptom_text": "45 year old male, severe chest pain, sweating. Rural Sitamarhi Bihar.", "patient_location": "Sitamarhi, Bihar", "max_results": 5 }'

Example specialized desert query

curl "http://localhost:8000/api/deserts/specialized?capability=dialysis&state=Bihar&radius_km=100"

Trust Score Formula

Trust Score = 30% × Completeness
+ 30% × Consistency
+ 20% × Evidence Score
+ 20% × Recency
Completeness: fraction of important fields that are non-null
Consistency: starts at 100, -20 per contradiction detected
Evidence Score: 20 pts per key capability verified via text evidence
(emergency, icu, oxygen, surgery, blood_bank)
Recency: 100 if updated <90 days ago, decaying to 15 if >2 years old
Confidence intervals:
uncertainty += 10 (text < 100 chars)
uncertainty += 10 (no last_updated date)
uncertainty += 15 (low-confidence inference used)
uncertainty += 20 (contradictions detected)
uncertainty += 10 (missing coordinates)
ci_low = max(0, score − uncertainty)
ci_high = min(100, score + uncertainty / 2)
Tiers:
≥ 70 → HIGH (safe to recommend)
40-69 → MEDIUM (recommend with caveats)
< 40 → LOW (do not recommend without verification)

Contradiction Detection

The evidence extractor checks for these contradictions automatically:

  • Claims surgery but no anesthesiologist or OT mentioned
  • Claims ICU but no ventilator or monitor listed
  • Claims NICU but no neonatal/incubator evidence
  • Claims dialysis but no dialysis machine mentioned
  • Claims trauma centre but no 24/7 coverage evidence

Databricks Production Deployment

  1. Set USE_DATABRICKS=true and VECTOR_BACKEND=databricks in .env
  2. Set LLM_PROVIDER=databricks and configure DATABRICKS_HOST / DATABRICKS_TOKEN
  3. Set ENABLE_MLFLOW_TRACING=true for agent observability
  4. Use scripts/export_from_databricks.py as a template to export a Unity Catalog Delta table to Parquet, or point UNITY_CATALOG_TABLE at an existing table
  5. Create a Mosaic AI Vector Search index and set DATABRICKS_VS_ENDPOINT + DATABRICKS_VS_INDEX
  6. Deploy FastAPI via Databricks Apps or Model Serving

MLflow Tracing

When ENABLE_MLFLOW_TRACING=true, each query creates an MLflow run logging:

  • urgency — triage classification
  • num_facilities_returned — pipeline output size
  • top_trust_score — confidence in top recommendation
  • latency_seconds — end-to-end pipeline time
  • symptom_text — query input (first 200 chars)

Security & Audit

All user input passes through backend/core/guardrails.py before reaching any agent. Blocked patterns include prompt injection, role-escalation, and system-prompt extraction attempts.

Every query generates a trace_id and writes a JSONL audit trail to logs/audit/<trace_id>.jsonl. Logs never contain full symptom text (truncated to 200 chars) or any patient identifiers.

See SECURITY.md for the full policy.


Alignment with Evaluation Criteria

Multi-Agent Architecture

The pipeline runs five specialized agents — TriageAgent, SearchAgent, GeoAgent, IDPAgent, TrustScorerAgent, ValidatorAgent — coordinated by a stateless Orchestrator using asyncio.gather for parallel execution. Each agent has a typed input/output contract (Pydantic schemas) so they can be tested and replaced independently.

Databricks / Mosaic AI Integration

  • vector_store.py supports both local FAISS and Databricks Vector Search via the same interface; switch with VECTOR_BACKEND=databricks.
  • llm_factory.py supports OpenAI, Anthropic, and databricks-meta-llama-3-3-70b-instruct via the same LLMClient interface.
  • MLflow tracing hooks in the orchestrator emit structured spans when ENABLE_MLFLOW_TRACING=true.
  • scripts/export_from_databricks.py shows the Unity Catalog → Parquet pipeline.

Responsible AI

  • Guardrails: regex-based injection detection before any LLM call (backend/core/guardrails.py).
  • No medical diagnosis: every response carries a hard-coded medical disclaimer; the MODEL_CARD.md explicitly lists prohibited use cases.
  • Uncertainty quantification: every trust score ships with a confidence interval and a list of uncertainty reasons so users know how much to trust a result.
  • Audit logging: complete JSONL trace per query for post-hoc review.
  • Data quality gate: validate_facility_dataframe() runs at startup and rejects datasets with missing required columns or out-of-range coordinates.

Transparency & Explainability

  • transparency_trace field on every QueryResponse lists human-readable steps (urgency classification, candidates retrieved, trust filtering, contradiction flags, final ranking rationale) — not raw chain-of-thought.
  • EvidenceSpan objects on each facility show exactly which sentence in the source text was used to verify each capability claim, with a confidence score and extraction method.
  • Contradiction detection mutates evidence spans to "contradictory" status and surfaces the conflict in the trust score uncertainty reasons.

Real-World Impact

  • Specialized medical desert analysis covers six under-served care types: dialysis, oncology, emergency trauma, NICU, surgical emergency, and ICU.
  • Desert alerts surface automatically when a patient location has no verified facility within DESERT_RADIUS_KM (default 100 km) for the required care type.
  • Trust score tiers (HIGH / MEDIUM / LOW) prevent low-quality facility records from reaching patients without explicit caveats.

Recent Updates — Sprint Log

Pipeline architecture (current state)

User query (symptom_text + patient_location + radius_km)
│
▼
┌──────────────┐
│ Guardrails │ ← injection detection, max-length enforcement
└──────┬───────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Orchestrator │
│ │
│ Step 1 ── TriageAgent │
│ rule-based safety floor → LLM upgrade │
│ URGENCY_RANK ensures rules win on severity │
│ │
│ Step 2 ── asyncio.gather │
│ ├─ SearchAgent (vector semantic retrieval) │
│ └─ GeoAgent (geocode + haversine distances) │
│ │
│ Step 3a ── GeoAgent.nearest_facilities │
│ full-table geo scan → inject nearby candidates │
│ not found by vector search (geo_added counter) │
│ │
│ Step 4 ── TrustScorerAgent (parallel per candidate) │
│ 30% completeness + 30% consistency │
│ + 20% evidence + 20% recency │
│ + dataset trust_score floor (_source_trust_score) │
│ + confidence intervals │
│ │
│ Step 5 ── _rank() composite score + tuple tie-break │
│ _select_local_first() │
│ ├─ bucket 1: locally relevant (≤ radius_km + QR) │
│ ├─ bucket 2: far but relevant (> radius_km + QR) │
│ ├─ bucket 3: local, irrelevant │
│ └─ bucket 4: far, irrelevant │
│ │
│ Step 6 ── Medical desert detection │
│ Step 7 ── ValidatorAgent (safety / contradiction pass) │
│ Step 8 ── Regional desert analysis │
│ Step 9 ── Synthesis → QueryResponse │
└──────────────────────────────────────────────────────────────┘
│
▼
FastAPI → Flutter frontend
├─ facility cards (border color = _facilityFitColor)
├─ map (nearby ≤ radius shown first)
└─ radius selector (5 / 10 / 25 / 50 km)

Changes applied

Backend — agents

FileChange
orchestrator.pyfinal_limit derived from request.max_results; geo-augmented candidate injection (geo_added); _select_local_first with 4-bucket clinical relevance ordering; _clinically_relevant helper; _rank uses tuple key for distance/trust tie-break; radius_km forwarded through the full pipeline
triage_agent.pyURGENCY_RANK dict; LLM output capped by rule-based urgency (safety floor); new rules: chest-cough → respiratory, chest-bruise → ortho, headache → neurology, dental → dentistry, skin rash → dermatology; expanded cardiac terms; "short of breath" / "falta de aire" added to breathing emergency
geo_agent.py_haversine module alias; nearest_facilities() method — full-table geo scan with state filter, independent of vector search
search_agent.pyexplain_match() public wrapper over _compute_match_reasons — called by orchestrator when candidates come from geo, not vector
trust_scorer_agent.pyimport re; _source_trust_score() reads facility.trust_score column (or extracts from raw text) and uses it as a floor for the computed score (×0.75 if contradictions present); explanation appended

Backend — core

FileChange
schemas.pyradius_km: Optional[float] added to QueryRequest
data_loader.pyColumn alias dict entries for trust_score, trust score, trust_category, trust category

Frontend

FileChange
api_service.dartradiusKm parameter wired into POST /query body
medmap_screen.dartLineSplitter for cross-platform CSV parsing; radiusKm forwarded to API; location message shows km radius; bestMatch sort splits into nearby-relevant / far-relevant / nearby-other / far-other; _isClinicallyRelevant and _facilityFitColor helpers; sort mode resets to bestMatch after query; Medical Desert heatmap with 7 service filters and concentric coverage rings; Facility Explorer tab with searchable/sortable/filterable DataTable (10k+ rows, 100-per-page, state+trust dropdowns)
facility_card.dart_fitColor getter (uses queryReadiness.score when available, falls back to trust tier); card border color driven by _fitColor

Medical Desert heatmap

Added inside the Medical Deserts tab above the executive summary card.

  • 7 service filter chips: All, ICU, Emergency, Dialysis, Surgery, Oncology, Oxygen/Vent
  • Green concentric rings (3 km / 8 km / 18 km) + green dot = facility has the service
  • Red or orange dot = facility missing the service (color from missingServiceSeverity — HIGH → red, MODERATE → orange)
  • Centered on the searched region, zoom auto-calculated from radius
  • Legend overlay; risk badge shown for the selected service

Facility Explorer tab

Replaced the "coming soon" placeholder in the Facility Explorer tab with a full data directory.

  • Search bar filters across name, city, and state in real time
  • State dropdown — unique values from the dataset, alphabetically sorted
  • Trust dropdown — High / Medium / Low tier filter
  • DataTable with sortable columns: Name, City, State, Pin Code, URL, Email, Phone, Trust score, Capabilities
  • Trust score rendered as a coloured badge (green ≥ 70, orange ≥ 40, red < 40)
  • Capability tags (ICU, Surgery, Emergency, Dialysis, Oncology) shown inline
  • Pagination — 100 rows per page with prev/next navigation and row-range indicator
  • Note: PIN code, URL, email, and phone are not present in the current dataset (columns show "—")

Tests

FileDescription
tests/test_orchestrator_ranking.py5 tests for _select_local_first: no-radius pass-through, limit enforcement, local preference, far-backup fill, and clinical relevance ordering (irrelevant nearby < relevant far)
tests/test_triage_agent.py3 tests: chest pain → critical/cardiac, chest cough → medium/pulmonology, chest bruise → medium/orthopaedics

Ranking formula (updated)

Composite score per facility:
0.40 × QueryReadiness (capability match to triage)
+ 0.28 × Proximity (1 − distance_km / 300)
+ 0.18 × Trust score
+ 0.14 × Semantic match
− contradiction penalty (0.08 per contradiction, max 0.25)
− missing critical penalty (up to 0.65 for absent ICU/surgery/etc.)
Tie-break key: (composite, −distance_km, trust.score)
Selection order when radius_km is set:
1. Locally relevant ← up to 4 slots (≤ radius, QR > 0 or non-trivial match)
2. Far but relevant ← fill remaining (> radius, same clinical relevance test)
3. Local, irrelevant ← fill remaining (≤ radius, only "Nearby facility" reason)
4. Far, irrelevant ← fill remaining
Trust score floor from dataset:
if facility.trust_score column present → floor = dataset_value
if contradictions → floor capped at dataset_value × 0.75

Test status

pytest tests/test_triage_agent.py tests/test_orchestrator_ranking.py -v
→ 8 / 8 passed

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages