Files
aionboarding/ai-patient-onboarding/app/services/result_extraction_service.py
T

219 lines
8.5 KiB
Python

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()