ewaast-demo / assessment_agent.py
NurseCitizenDeveloper's picture
feat: Expand Student Mode with AI Preceptor, ScoreCard, and new Patients
2e2cf08
# 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"})