# Copyright 2025 - EWAAST Project # Adapted from Google's appoint-ready architecture # Licensed under the Apache License, Version 2.0 """ EWAAST Assessment Agent Core assessment logic with streaming output. Now powered by Google Gemini 1.5 Pro (Real AI) via gemini_client. """ import json import time import os import base64 import io from typing import Generator from PIL import Image # Import Real AI Client from gemini_client import get_gemini_client # ===== MST VISUAL GUIDANCE ===== MST_GUIDANCE = { 1: {"category": "Light", "visual": "Look for non-blanchable erythema (redness), warmth"}, 2: {"category": "Light", "visual": "Look for red to pink coloration, visible redness"}, 3: {"category": "Light", "visual": "Look for pink to light red, subtle redness"}, 4: {"category": "Medium", "visual": "Look for subtle darkening, warmth may be more reliable than color"}, 5: {"category": "Medium", "visual": "Look for color change rather than redness, warmth, shiny skin"}, 6: {"category": "Medium", "visual": "Look for darkening of skin, purple or brownish discoloration"}, 7: {"category": "Deep", "visual": "CRITICAL: Do NOT rely on redness. Look for purple/blue discoloration, warmth, induration"}, 8: {"category": "Deep", "visual": "CRITICAL: Do NOT rely on redness. Look for purple, maroon, or deep burgundy; may appear black"}, 9: {"category": "Deep", "visual": "CRITICAL: Do NOT rely on redness. Look for deep purple, indigo, or dark discoloration; warmth, texture changes"}, 10: {"category": "Deep", "visual": "CRITICAL: Do NOT rely on redness. Look for subtle purple/blue undertones; induration, warmth, localized heat"} } def _b64_to_pil(image_b64: str) -> Image.Image: """Convert base64 string to PIL Image.""" try: if "," in image_b64: image_b64 = image_b64.split(",")[1] image_data = base64.b64decode(image_b64) return Image.open(io.BytesIO(image_data)) except Exception as e: print(f"Error converting image: {e}") return Image.new('RGB', (100, 100), color='gray') def classify_skin_tone(image_b64: str, context: str = "") -> dict: """ Classify the Monk Skin Tone from an image using Gemini. """ client = get_gemini_client() if client.is_available(): try: image = _b64_to_pil(image_b64) result = client.analyze_mst(image) mst_value = result.get("mst_value", 5) # Normalize constraints mst_value = max(1, min(10, int(mst_value))) guidance = MST_GUIDANCE.get(mst_value, MST_GUIDANCE[5]) return { "mst_value": mst_value, "category": guidance["category"], "visual_guidance": guidance["visual"], "confidence": result.get("confidence", 0.85) } except Exception as e: print(f"AI Classification failed: {e}") # Fallback / Demo Logic print("Falling back to heuristic MST classification") # Check if this is a "valid" fallback (user provided context) or just missing API if not context and not client.is_available(): return { "mst_value": "Unknown", "category": "Unknown", "visual_guidance": "⚠️ AI Unavailable: Please add GOOGLE_API_KEY to .env or Space Secrets", "confidence": 0.0 } # Rest of heuristic logic import re mst_match = re.search(r'MST[:\s]*(\d+)', context, re.IGNORECASE) if mst_match: mst_value = int(mst_match.group(1)) elif 'deep' in context.lower() or 'dark' in context.lower(): mst_value = 9 elif 'medium' in context.lower(): mst_value = 5 else: # Default safe fallback - do not assume light skin if unknown mst_value = 5 guidance = MST_GUIDANCE.get(mst_value, MST_GUIDANCE[5]) return { "mst_value": mst_value, "category": guidance["category"], "visual_guidance": guidance["visual"] + " (Estimated)", "confidence": 0.5 } def generate_assessment_report(image_b64: str, mst_result: dict, context: str) -> dict: """ Generate the clinical assessment report using Gemini. """ client = get_gemini_client() if client.is_available(): try: image = _b64_to_pil(image_b64) return client.assess_wound(image, mst_result, context) except Exception as e: print(f"AI Assessment failed: {e}") error_msg = str(e) # Fallback Logic return { "stage": "Assessment Failed", "rationale": f"AI Error: {error_msg}" if 'error_msg' in locals() else "AI Assessment Unavailable. Please check your GOOGLE_API_KEY configuration.", "care_plan": "1. Verify API Key in Settings\n2. Reload Application\n3. Check Server Logs for Details", "urgency": "N/A" } def generate_preceptor_feedback(mst_value: int, wound_truth: dict, student_diagnosis: dict) -> dict: """ Generate educational feedback comparing student diagnosis to truth. Acts as a "Clinical Nurse Educator" using EPUAP guidelines. """ client = get_gemini_client() # Construct the scenario context for the AI prompt = f""" You are an expert Clinical Nurse Educator specializing in Wound Care and Health Equity. A student nurse has just assessed a patient. Provide feedback on their diagnosis. PATIENT CONTEXT: - Monk Skin Tone (MST): {mst_value} (Scale 1-10) GOLD STANDARD (CORRECT) DIAGNOSIS: - Wound Stage: {wound_truth.get('id')} ({wound_truth.get('name')}) - Description: {wound_truth.get('description')} STUDENT NURSE DIAGNOSIS: - Stage: {student_diagnosis.get('stage')} - Tissue Type: {student_diagnosis.get('tissueType')} - Priority Action: {student_diagnosis.get('priorityAction')} YOUR TASK: Compare the student's diagnosis to the Gold Standard. Provide constructive feedback. CRITICAL EPUAP GUIDELINE RULES TO ENFORCE: 1. If MST >= 7 (Deep Skin Tone) and the student relied on "Redness" or missed a DTI/Stage 1: - Educate them that "Redness is not a reliable indicator in dark skin". - Remind them to check for: Temperature (Warmth/Coolness), Induration (Firmness), and Texture changes. 2. If the student missed a DTI (Deep Tissue Injury): - Explain that DTIs can look like a bruise or be "boggy" on palpation. OUTPUT FORMAT (JSON): {{ "is_correct": boolean, "title": "Short title (e.g., 'Correct Diagnosis' or 'Missed Stage 1')", "feedback": "2-3 sentences of direct feedback to the student.", "learning_point": "State the relevant EPUAP guideline or clinical pearl." }} """ if client.is_available(): try: from google.genai import types response = client.client.models.generate_content( model=client.model_id, contents=prompt, config=types.GenerateContentConfig( response_mime_type="application/json" ) ) return json.loads(response.text) except Exception as e: print(f"AI Preceptor Error: {e}") # Fallback Logic if AI fails is_correct = student_diagnosis.get('stage') == wound_truth.get('id') return { "is_correct": is_correct, "title": "Diagnosis Result" if is_correct else "Incorrect Diagnosis", "feedback": "AI Preceptor unavailable. Please compare your answer to the Gold Standard manually.", "learning_point": "EPUAP Guidelines emphasize checking for temperature and induration differences on dark skin." } def stream_assessment(image_b64: str, context: str) -> Generator[str, None, None]: """ Stream the wound assessment process. """ # Step 1: Acknowledge yield json.dumps({ "step": "received", "thinking": "Image received. validating...", "data": {} }) time.sleep(0.5) # Step 2: Validate Image (New Step with Gemini) client = get_gemini_client() if client.is_available(): try: img = _b64_to_pil(image_b64) val_result = client.validate_wound_image(img) if not val_result.get("is_valid", True): yield json.dumps({ "step": "error", "thinking": f"Image Rejected: {val_result.get('reason')}", "data": {"error": val_result.get('reason')} }) yield json.dumps({"event": "end"}) return except Exception: pass # Step 3: Classify skin tone yield json.dumps({ "step": "classifying_mst", "thinking": "Analyzing skin tone using Monk Skin Tone (MST) scale with Gemini Vision...", "data": {} }) mst_result = classify_skin_tone(image_b64, context) yield json.dumps({ "step": "mst_complete", "thinking": f"Identified MST {mst_result['mst_value']} ({mst_result['category']}). Adjusted visual guidance: {mst_result['visual_guidance']}", "data": mst_result }) # Step 4: Analyze wound yield json.dumps({ "step": "analyzing_wound", "thinking": f"Gemini is analyzing wound features. Looking for MST-specific signs (e.g., discoloration vs redness)...", "data": {} }) # Step 5: Generate report report = generate_assessment_report(image_b64, mst_result, context) yield json.dumps({ "step": "complete", "thinking": "Assessment complete. Clinical rationale generated.", "data": { "mst": mst_result, "report": report } }) yield json.dumps({"event": "end"})