Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, UploadFile, File, HTTPException, Query, Header, Depends | |
| from typing import Any, List | |
| from .ingest import ingest_csv_file | |
| from .fhir import build_codesystem, build_conceptmap | |
| from .storage import terminology_store | |
| from .who_api import who_client | |
| from .snomed_loinc import snomed_service, loinc_service, get_semantic_coding | |
| from .fhir_resources import ( | |
| create_problem_list_entry, create_encounter, create_patient, | |
| create_practitioner, create_consent, create_audit_event, create_provenance | |
| ) | |
| from .iso_22600 import check_resource_access, load_consent_from_fhir, access_control | |
| from datetime import datetime | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| from pathlib import Path | |
| import json | |
| router = APIRouter() | |
| def _ensure_loaded() -> None: | |
| # Auto-load default dataset if store is empty | |
| if len(terminology_store.namaste) == 0: | |
| data_path = Path(__file__).resolve().parents[1] / 'data' / 'namaste_200.csv' | |
| if data_path.exists(): | |
| try: | |
| ingest_csv_file(data_path.read_bytes()) | |
| except Exception: | |
| pass | |
| async def ingest_csv(file: UploadFile = File(...)) -> Any: | |
| if not file.filename.lower().endswith('.csv'): | |
| raise HTTPException(status_code=400, detail='Only CSV files are supported') | |
| content = await file.read() | |
| try: | |
| count = ingest_csv_file(content) | |
| except Exception as exc: # keep broad for hackathon demo | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {'ingested': count} | |
| def get_codesystem() -> Any: | |
| _ensure_loaded() | |
| return build_codesystem() | |
| def get_conceptmap() -> Any: | |
| _ensure_loaded() | |
| return build_conceptmap() | |
| def search_terms(q: str = Query(..., min_length=1)) -> Any: | |
| _ensure_loaded() | |
| return terminology_store.search(q) | |
| async def translate(code: str = Query(...), system: str = Query(..., pattern='^(namaste|icd11)$')) -> Any: | |
| _ensure_loaded() | |
| raw = terminology_store.translate(code, system) | |
| # Normalize, de-duplicate and enrich with titles | |
| def norm_icd(c: str) -> str: | |
| return c.replace('ICD-11:', '').strip() | |
| # Basic demo title map (would come from WHO API/cache in production) | |
| title_map = { | |
| 'TM2-AY134': 'Acid dyspepsia (TM2)', | |
| 'K29.7': 'Gastritis, unspecified', | |
| '5A11': 'Type 2 diabetes mellitus', | |
| 'TM2-AY999': 'Vata imbalance (TM2)' | |
| } | |
| matches = raw.get('matches', []) | |
| if system == 'namaste': | |
| # Return ICD targets with titles | |
| seen: set[str] = set() | |
| targets: list[dict[str, Any]] = [] | |
| for m in matches: | |
| code_only = norm_icd(m.get('code', '')) | |
| if not code_only or code_only in seen: | |
| continue | |
| seen.add(code_only) | |
| targets.append({ | |
| 'code': code_only, | |
| 'title': title_map.get(code_only, code_only), | |
| 'system': 'ICD-11' | |
| }) | |
| # Try to enrich using WHO mock data if still missing titles | |
| if any(t['title'] == t['code'] for t in targets): | |
| tm2 = await who_client.get_tm2_entities() | |
| bio = await who_client.get_biomedicine_entities() | |
| full = {e['id']: e.get('title') for e in tm2} | |
| full.update({e['id']: e.get('title') for e in bio}) | |
| for t in targets: | |
| if t['title'] == t['code'] and t['code'] in full: | |
| t['title'] = full[t['code']] | |
| return { 'targets': targets } | |
| else: | |
| # system == icd11 → return NAMASTE codes with labels | |
| seen: set[str] = set() | |
| targets: list[dict[str, Any]] = [] | |
| for m in matches: | |
| c = m.get('code') | |
| if not c or c in seen: | |
| continue | |
| seen.add(c) | |
| term = terminology_store.namaste.get(c) | |
| targets.append({ | |
| 'code': c, | |
| 'title': term.label if term else c, | |
| 'system': 'NAMASTE' | |
| }) | |
| return { 'targets': targets } | |
| def suggest(q: str = Query(..., min_length=1)) -> Any: | |
| _ensure_loaded() | |
| return terminology_store.suggest_with_confidence(q) | |
| # --- Mock ABHA OAuth --- | |
| def auth(abha_id: str = Query(...)) -> Any: | |
| if not abha_id or len(abha_id) < 6: | |
| raise HTTPException(status_code=400, detail='Invalid ABHA ID') | |
| # simple mock token | |
| token = f"mock.{abha_id}.token" | |
| exp = int(datetime.utcnow().timestamp()) + 3600 | |
| return {'access_token': token, 'token_type': 'bearer', 'expires_at': exp} | |
| def _require_auth(authorization: str | None) -> None: | |
| if not authorization or not authorization.startswith('Bearer mock.'): | |
| raise HTTPException(status_code=401, detail='Unauthorized') | |
| # --- FHIR Bundle ingest with Audit/Provenance --- | |
| AUDIT_LOG: list[dict[str, Any]] = [] | |
| PROV_LOG: list[dict[str, Any]] = [] | |
| def ingest_bundle(bundle: dict[str, Any], authorization: str | None = Header(default=None)) -> Any: | |
| _require_auth(authorization) | |
| if bundle.get('resourceType') != 'Bundle': | |
| raise HTTPException(status_code=400, detail='Invalid Bundle') | |
| entries = bundle.get('entry') or [] | |
| has_condition = any((e.get('resource') or {}).get('resourceType') == 'Condition' for e in entries) | |
| if not has_condition: | |
| raise HTTPException(status_code=400, detail='Bundle missing Condition resource') | |
| now = datetime.utcnow().isoformat() + 'Z' | |
| AUDIT_LOG.append({ | |
| 'resourceType': 'AuditEvent', | |
| 'type': {'code': 'rest'}, | |
| 'action': 'C', | |
| 'recorded': now, | |
| 'outcome': '0', | |
| 'agent': [{'requestor': True}], | |
| 'source': {'site': 'ayush-fhir'}, | |
| }) | |
| PROV_LOG.append({ | |
| 'resourceType': 'Provenance', | |
| 'recorded': now, | |
| 'agent': [{'type': {'text': 'system'}, 'who': {'display': 'ayush-fhir'}}], | |
| 'target': [{'reference': 'Bundle/new'}], | |
| }) | |
| return {'status': 'accepted'} | |
| def get_audit() -> Any: | |
| return {'entries': AUDIT_LOG} | |
| def get_provenance() -> Any: | |
| return {'entries': PROV_LOG} | |
| # --- Simple stats for dashboard --- | |
| def top_terms() -> Any: | |
| # frequency based on presence; in real apps, count occurrences. Here, return top 5 by code order | |
| items = [ | |
| {'code': t.code, 'label': t.label, 'count': 1} | |
| for t in list(terminology_store.namaste.values())[:5] | |
| ] | |
| return {'items': items} | |
| def dual_coding_rate() -> Any: | |
| total = len(terminology_store.namaste) | |
| dual = sum(1 for t in terminology_store.namaste.values() if t.icd11_tm2_codes) | |
| rate = (dual / total * 100.0) if total else 0.0 | |
| return {'total_terms': total, 'dual_coded_terms': dual, 'rate_percent': round(rate, 2)} | |
| def ingest_default() -> Any: | |
| # Load the default 200-record CSV from data/namaste_200.csv | |
| data_path = Path(__file__).resolve().parents[1] / 'data' / 'namaste_200.csv' | |
| if not data_path.exists(): | |
| raise HTTPException(status_code=404, detail='Default dataset not found') | |
| content = data_path.read_bytes() | |
| count = ingest_csv_file(content) | |
| return {'ingested': count, 'source': str(data_path.name)} | |
| # --- WHO ICD-11 API Integration --- | |
| async def search_tm2( | |
| q: str | None = Query(default=None), | |
| query: str | None = Query(default=None) | |
| ) -> Any: | |
| """Search WHO ICD-11 TM2 entities""" | |
| _ensure_loaded() | |
| term = q or query | |
| if not term: | |
| raise HTTPException(status_code=422, detail="Missing ?q= or ?query=") | |
| try: | |
| entities = await who_client.get_tm2_entities() | |
| # Filter by query | |
| filtered = [ | |
| e for e in entities | |
| if term.lower() in e.get('title', '').lower() or | |
| term.lower() in ' '.join(e.get('synonyms', [])).lower() | |
| ] | |
| return {'entities': filtered, 'count': len(filtered)} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"WHO API error: {str(e)}") | |
| async def search_biomedicine(query: str = Query(..., min_length=1)) -> Any: | |
| """Search WHO ICD-11 Biomedicine entities""" | |
| _ensure_loaded() | |
| try: | |
| entities = await who_client.get_biomedicine_entities(query) | |
| return {'entities': entities, 'count': len(entities)} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"WHO API error: {str(e)}") | |
| # --- SNOMED CT / LOINC Integration --- | |
| def search_snomed( | |
| q: str | None = Query(default=None), | |
| query: str | None = Query(default=None), | |
| ) -> Any: | |
| """Search SNOMED CT concepts""" | |
| _ensure_loaded() | |
| term = q or query | |
| if not term: | |
| raise HTTPException(status_code=422, detail="Missing ?q= or ?query=") | |
| concepts = snomed_service.search_concepts(term) | |
| return { | |
| 'concepts': [ | |
| {'code': c.code, 'display': c.display, 'system': c.system, 'category': c.category} | |
| for c in concepts | |
| ], | |
| 'count': len(concepts) | |
| } | |
| def search_loinc( | |
| q: str | None = Query(default=None), | |
| query: str | None = Query(default=None), | |
| ) -> Any: | |
| """Search LOINC codes""" | |
| _ensure_loaded() | |
| term = q or query | |
| if not term: | |
| raise HTTPException(status_code=422, detail="Missing ?q= or ?query=") | |
| codes = loinc_service.search_codes(term) | |
| return { | |
| 'codes': [ | |
| {'code': c.code, 'display': c.display, 'system': c.system, 'category': c.category} | |
| for c in codes | |
| ], | |
| 'count': len(codes) | |
| } | |
| # --- FHIR Problem List with Dual Coding --- | |
| def create_problem_list( | |
| namaste_code: str = Query(...), | |
| patient_id: str = Query(default="patient-001"), | |
| practitioner_id: str = Query(default="practitioner-001"), | |
| encounter_id: str = Query(default="encounter-001"), | |
| authorization: str | None = Header(default=None) | |
| ) -> Any: | |
| """Create FHIR Problem List entry with dual coding""" | |
| _ensure_loaded() | |
| _require_auth(authorization) | |
| # Get ICD-11 codes for the NAMASTE term | |
| term = terminology_store.namaste.get(namaste_code) | |
| if not term: | |
| raise HTTPException(status_code=404, detail=f"NAMASTE code {namaste_code} not found") | |
| icd11_codes = term.icd11_tm2_codes | |
| try: | |
| # Create Problem List entry | |
| condition = create_problem_list_entry( | |
| namaste_code=namaste_code, | |
| icd11_codes=icd11_codes, | |
| patient_id=patient_id, | |
| practitioner_id=practitioner_id, | |
| encounter_id=encounter_id | |
| ) | |
| # Create audit event | |
| audit_event = create_audit_event( | |
| action="C", | |
| outcome="0", | |
| agent_name="Ayush FHIR Service", | |
| resource_type="Condition" | |
| ) | |
| # Create provenance | |
| provenance = create_provenance( | |
| target_reference=f"Condition/{condition['id']}", | |
| agent_name="Ayush FHIR Service" | |
| ) | |
| return { | |
| 'condition': condition, | |
| 'audit_event': audit_event, | |
| 'provenance': provenance, | |
| 'dual_coding': { | |
| 'namaste': { | |
| 'code': namaste_code, | |
| 'display': term.label | |
| }, | |
| 'icd11': [ | |
| { | |
| 'code': code, | |
| 'display': f"{code} (TM2)" if code.startswith("TM2-") else f"{code} (Biomedicine)" | |
| } | |
| for code in icd11_codes | |
| ] | |
| } | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error creating problem list: {str(e)}") | |
| # --- ISO 22600 Access Control --- | |
| def create_consent_resource( | |
| patient_id: str = Query(...), | |
| purpose: str = Query(default="TREATMENT"), | |
| authorization: str | None = Header(default=None) | |
| ) -> Any: | |
| """Create FHIR Consent resource""" | |
| _require_auth(authorization) | |
| consent = create_consent(patient_id=patient_id, purpose=purpose) | |
| # Load consent rules into access control | |
| load_consent_from_fhir(consent) | |
| return consent | |
| def check_access( | |
| subject_id: str = Query(...), | |
| subject_type: str = Query(...), | |
| subject_roles: str = Query(...), # Comma-separated roles | |
| action: str = Query(...), | |
| resource_type: str = Query(...), | |
| resource_id: str = Query(...), | |
| purpose: str = Query(default="TREATMENT"), | |
| patient_id: str | None = Query(default=None), | |
| authorization: str | None = Header(default=None) | |
| ) -> Any: | |
| """Check resource access per ISO 22600""" | |
| _require_auth(authorization) | |
| roles = [role.strip() for role in subject_roles.split(',')] | |
| allowed, reason = check_resource_access( | |
| subject_id=subject_id, | |
| subject_type=subject_type, | |
| subject_roles=roles, | |
| action=action, | |
| resource_type=resource_type, | |
| resource_id=resource_id, | |
| purpose=purpose, | |
| patient_id=patient_id | |
| ) | |
| return { | |
| 'allowed': allowed, | |
| 'reason': reason, | |
| 'request': { | |
| 'subject_id': subject_id, | |
| 'subject_type': subject_type, | |
| 'subject_roles': roles, | |
| 'action': action, | |
| 'resource_type': resource_type, | |
| 'resource_id': resource_id, | |
| 'purpose': purpose, | |
| 'patient_id': patient_id | |
| } | |
| } | |