import json from app.ai.openai_client import OpenAIClient from app.config import OPENAI_API_KEY from app.schemas.onboarding import PatientOnboardingResult class ResultExtractionService: def __init__(self): self.openai = OpenAIClient() async def extract_result( self, session, state, conversation_history, ) -> dict: current_state = json.loads( state.state_json ) prompt = f""" You are a senior clinical data extraction engine at a healthcare facility. Your task is to convert the collected patient onboarding conversation and structured state into a clean, comprehensive, structured patient intake record for the attending doctor. CLINICAL RULES: 1. You are NOT a doctor. Do NOT diagnose or add medical interpretations. 2. Capture ALL patient-reported symptoms, onset/duration, severity, location, frequency. 3. Capture ALL reported medical history, medications (with dosage/frequency), allergies (with reaction), surgical history, and previous episodes. 4. Capture any functional impact (sleep disruption, appetite changes, weakness) in additional_information. 5. Do NOT invent information. If something was not provided, leave it null or an empty list. PATIENT CONTEXT: {json.dumps({ "id": session.patient_id, "name": session.patient_name, "age": session.age, "gender": session.gender, }, indent=2, ensure_ascii=False)} CATEGORY / SPECIALTY: {json.dumps({ "type": session.category_type, "code": session.category_code, "name": session.category_name, }, indent=2, ensure_ascii=False)} CURRENT INTAKE STATE: {json.dumps(current_state, indent=2, ensure_ascii=False)} FULL CONVERSATION: {json.dumps(conversation_history, indent=2, ensure_ascii=False)} Return ONLY a valid JSON object matching this structure: {{ "patient_id": "{session.patient_id}", "category": {{ "type": "{session.category_type}", "code": "{session.category_code}", "name": "{session.category_name}" }}, "chief_complaint": "primary reason for visit or main symptom", "symptoms": [ {{ "name": "symptom name", "duration": "duration or null", "severity": "severity or null", "location": "location or null", "description": "brief details or null" }} ], "medical_history": [ {{ "condition": "condition name", "duration": "duration or null", "status": "active/past or null" }} ], "medications": [ {{ "name": "medication name", "dosage": "dosage or null", "frequency": "frequency or null", "duration": "duration or null" }} ], "allergies": [ {{ "name": "allergen name", "type": "drug/food/etc or null", "reaction": "reaction or null" }} ], "surgical_history": [], "previous_episodes": [], "family_history": [], "dietary_habits": null, "additional_information": "any functional impact, previous episodes, or additional notes", "language": "{session.language}", "onboarding_completed": true }} """ from app.config import GEMINI_API_KEY, GEMINI_MODEL, ANTHROPIC_API_KEY, ANTHROPIC_MODEL # Primary: Google Gemini extraction if GEMINI_API_KEY and GEMINI_API_KEY.strip() not in ["your_gemini_key", "your_gemini_api_key", ""]: try: import asyncio from google import genai from google.genai import types client = genai.Client(api_key=GEMINI_API_KEY) config = types.GenerateContentConfig(response_mime_type="application/json") res = await asyncio.wait_for( client.aio.models.generate_content( model=GEMINI_MODEL or "gemini-1.5-flash", contents=prompt, config=config, ), timeout=3.5 ) cleaned = res.text.replace("```json", "").replace("```", "").strip() parsed = json.loads(cleaned) validated = PatientOnboardingResult.model_validate(parsed) return validated.model_dump() except Exception as e: print(f"[Gemini Result Extraction Warning]: {e}") # Secondary: OpenAI extraction if OPENAI_API_KEY and OPENAI_API_KEY.strip() not in ["your_openai_api_key", "your_openai_key", ""]: try: raw_response = await self.openai.generate_response( system_prompt=prompt, user_message="Generate the final structured patient onboarding result.", ) cleaned = raw_response.strip() if cleaned.startswith("```json"): cleaned = cleaned[7:] if cleaned.startswith("```"): cleaned = cleaned[3:] if cleaned.endswith("```"): cleaned = cleaned[:-3] cleaned = cleaned.strip() parsed = json.loads(cleaned) validated = PatientOnboardingResult.model_validate(parsed) return validated.model_dump() except Exception as e: print(f"[OpenAI Result Extraction Warning]: {e}") # Tertiary: Anthropic Claude extraction if ANTHROPIC_API_KEY and ANTHROPIC_API_KEY.strip() not in ["your_anthropic_key", ""]: try: from anthropic import AsyncAnthropic client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY) res = await client.messages.create( model=ANTHROPIC_MODEL or "claude-sonnet-4-5-20250929", max_tokens=800, system=prompt, messages=[{"role": "user", "content": "Generate structured onboarding result"}], ) cleaned = res.content[0].text.replace("```json", "").replace("```", "").strip() parsed = json.loads(cleaned) validated = PatientOnboardingResult.model_validate(parsed) return validated.model_dump() except Exception as e: print(f"[Claude Result Extraction Error]: {e}") # Fallback structured builder from state chief_complaint = current_state.get("reason_for_visit") if isinstance(chief_complaint, dict): chief_complaint = chief_complaint.get("value") symptoms_raw = current_state.get("symptoms") symptoms_list = [] if isinstance(symptoms_raw, dict): symptoms_list = symptoms_raw.get("items", []) elif isinstance(symptoms_raw, list): symptoms_list = symptoms_raw meds_raw = current_state.get("medications") meds_list = meds_raw.get("items", []) if isinstance(meds_raw, dict) else (meds_raw or []) allergies_raw = current_state.get("allergies") allergies_list = allergies_raw.get("items", []) if isinstance(allergies_raw, dict) else (allergies_raw or []) history_raw = current_state.get("medical_history") history_list = history_raw.get("items", []) if isinstance(history_raw, dict) else (history_raw or []) surgical_history = current_state.get("surgical_history", []) previous_episodes = current_state.get("previous_episodes", []) family_history = current_state.get("family_history", []) add_info = current_state.get("additional_information") fallback_result = { "patient_id": session.patient_id, "category": { "type": session.category_type, "code": session.category_code, "name": session.category_name, }, "chief_complaint": chief_complaint or None, "symptoms": symptoms_list, "medical_history": history_list, "medications": meds_list, "allergies": allergies_list, "surgical_history": surgical_history if isinstance(surgical_history, list) else [], "previous_episodes": previous_episodes if isinstance(previous_episodes, list) else [], "family_history": family_history if isinstance(family_history, list) else [], "additional_information": add_info, "language": session.language, "onboarding_completed": True, } validated = PatientOnboardingResult.model_validate(fallback_result) return validated.model_dump()