Source code for agents.scenario_agents

"""Defines AI agents and orchestration for medical scenario generation.

This module contains the core components for the AI-driven scenario creation
process. It defines specialist agents for different parts of a scenario
(initial info, timeline, script), functions to generate detailed prompts, and
a MedicalScenarioTeam class to orchestrate the generation pipeline using
agno's structured output capabilities.

Version: 5.0.1 
"""

import logging
from typing import List

from agno.agent import Agent, RunResponse
from fastapi import HTTPException
from pydantic import ValidationError

from models import (
    BaseScenario,
    FullScenario,
    ScenarioRequest,
    Timeline,
    Sceneggiatura,
)
from config import PRESIDI_MEDICI
from utils import get_big_model, get_knowledge_base

# Logger instance
logger = logging.getLogger(__name__)


# --- Agent Definitions ---
info_agent = Agent(
    name="Scenario Info Generator",
    role="An expert in creating the foundational elements of a medical simulation.",
    knowledge=get_knowledge_base(),
    model=get_big_model(),
    response_model=BaseScenario,
    instructions=[
        "Your task is to generate the static, initial part of a medical scenario.",
        "All text content must be in Italian.",
        "Generate specific, plausible data for all fields based on the user's request.",
    ]
)

timeline_agent = Agent(
    name="Clinical Timeline Generator",
    role="An expert in creating dynamic, evolving clinical timelines for medical simulations.",
    model=get_big_model(),
    knowledge=get_knowledge_base(),
    response_model=Timeline,
    instructions=[
        "Your task is to generate a series of timeline events ('tempi').",
        "All text content must be in Italian.",
        "Based on the provided initial scenario context, create a realistic clinical evolution.",
        "Start from T0 reflecting the initial parameters, then evolve the scenario with 4-5 events.",
        "If the patient is pediatric, include a 'ruoloGenitore' field in each event if necessary.",
    ]
)

script_agent = Agent(
    name="Patient Script Writer",
    role="A creative writer specializing in scripts for simulated patients.",
    model=get_big_model(),
    response_model=Sceneggiatura,
    instructions=[
        "Your task is to write a script ('sceneggiatura') for a simulated patient.",
        "The script must be in Italian and formatted as HTML text (e.g., using <p> tags).",
        "Based on the complete scenario provided, write a script that the patient actor can follow.",
    ]
)


# --- Prompt Functions ---
[docs] def create_info_prompt(request: ScenarioRequest) -> str: """Creates prompt for the Scenario Info Generator agent.""" difficulty_guidelines = { "Facile": {"complications": "Scenario semplice...", "parameters": "Parametri vitali stabili...", "timeline": "Evoluzione lineare...", "actions": "Azioni cliniche di base..."}, "Medio": {"complications": "Scenario con complessità moderata...", "parameters": "Parametri vitali con alterazioni moderate...", "timeline": "Evoluzione con qualche imprevisto...", "actions": "Combinazione di azioni di base e avanzate..."}, "Difficile": {"complications": "Scenario complesso...", "parameters": "Parametri vitali instabili...", "timeline": "Evoluzione rapida...", "actions": "Procedure avanzate..."} } difficulty = request.difficulty or "Facile" guidelines = difficulty_guidelines[difficulty] return f""" Generate the base part of a medical scenario. ## USER REQUEST - Description: {request.description} - Scenario Type: {request.scenario_type} - Target Audience: {request.target} - Difficulty Level: {difficulty} ## INSTRUCTIONS 1. **Adapt to Target Audience**: The scenario's complexity, pathology, and actions must be suitable for the specified target audience. 2. **Adapt to Difficulty ({difficulty})**: - Complications: {guidelines['complications']} - Vitals: {guidelines['parameters']} - Evolution: {guidelines['timeline']} - Actions: {guidelines['actions']} 3. **Language**: All text must be in ITALIAN. 4. **Formatting**: - Use HTML tags (`<p>`, `<ul>`, `<li>`) for descriptive fields as appropriate. - Physical exam sections ('esameFisico.sections') must be in `<p>` tags. - For vital signs, use `0` if a value is not applicable. Do not use text. 5. **Medical Devices ('presidi')**: Choose ONLY from this list: {PRESIDI_MEDICI} 6. **Parent/Guardian Role**: If pediatric, describe the parent's initial state in 'scenario.infoGenitore'. """
[docs] def create_timeline_prompt(base_scenario: BaseScenario, difficulty: str) -> str: """Creates the simplified prompt for the Clinical Timeline Generator agent.""" difficulty_timeline_rules = { "Facile": "Evoluzione graduale e stabile. Tempi lenti (5-10 min).", "Medio": "Evoluzione moderatamente dinamica. Tempi normali (2-5 min).", "Difficile": "Evoluzione rapida e critica. Tempi rapidi (30 sec - 2 min)." } context_json = base_scenario.model_dump_json(indent=2) return f""" Given the following initial scenario, generate a clinical timeline. ## INITIAL CONTEXT {context_json} ## INSTRUCTIONS 1. Generate 4-5 timeline events ('tempi'), starting with T0 reflecting the initial state. 2. Language: All text must be in ITALIAN. 3. **Adapt to Difficulty ({difficulty})**: {difficulty_timeline_rules.get(difficulty, "")} 4. **Vitals**: Use `0` for not applicable numeric vitals. 5. **Parent/Guardian Role**: If pediatric, describe the parent's reaction at each timeline step in the 'ruoloGenitore' field. """
[docs] def create_script_prompt(full_context: FullScenario) -> str: """Creates the simplified prompt for the Patient Script Writer agent.""" context_json = full_context.model_dump_json(indent=2) return f""" Given the complete medical scenario below, write a detailed script for the simulated patient. ## FULL SCENARIO CONTEXT {context_json} ## INSTRUCTIONS 1. Write a script in ITALIAN for the patient actor. 2. The script must reflect the patient's symptoms and their evolution throughout the timeline. 3. Format the script text with HTML tags (e.g., `<p>`). """
[docs] class MedicalScenarioTeam: """Manages a team of agents to generate a complete medical scenario.""" def __init__(self, members: List[Agent]): self.members = {agent.name: agent for agent in members} def _get_agent(self, name: str) -> Agent: if name not in self.members: raise ValueError(f"Agent '{name}' not found in team members.") return self.members[name]
[docs] def run(self, request: ScenarioRequest) -> FullScenario: """Executes the full scenario generation pipeline using validated Pydantic objects.""" try: # --- STEP 1: Run Info Agent --- logger.info("Team Pipeline: Running 'Scenario Info Generator'...") info_prompt = create_info_prompt(request) info_response: RunResponse = self._get_agent("Scenario Info Generator").run(info_prompt) # type: ignore content = info_response.content if not isinstance(content, BaseScenario): raise TypeError(f"Info agent did not return a valid BaseScenario object, got {type(content).__name__} instead.") base_scenario: BaseScenario = content timeline = Timeline(tempi=[]) sceneggiatura = Sceneggiatura(sceneggiatura="") # --- STEP 2: Run Timeline Agent (conditionally) --- if request.scenario_type in ["Advanced Scenario", "Patient Simulated Scenario"]: logger.info("Team Pipeline: Running 'Clinical Timeline Generator'...") timeline_prompt = create_timeline_prompt(base_scenario, request.difficulty or "Facile") timeline_response: RunResponse = self._get_agent("Clinical Timeline Generator").run(timeline_prompt) # type: ignore content = timeline_response.content if not isinstance(content, Timeline): raise TypeError(f"Timeline agent did not return a valid Timeline object, got {type(content).__name__} instead.") timeline: Timeline = content # --- STEP 3: Run Script Agent (conditionally) --- if request.scenario_type == "Patient Simulated Scenario": logger.info("Team Pipeline: Running 'Patient Script Writer'...") script_context_obj = FullScenario(**base_scenario.model_dump(), tempi=timeline.tempi, sceneggiatura="") script_prompt = create_script_prompt(script_context_obj) script_response: RunResponse = self._get_agent("Patient Script Writer").run(script_prompt) # type: ignore content = script_response.content if not isinstance(content, Sceneggiatura): raise TypeError(f"Script agent did not return a valid Sceneggiatura object, got {type(content).__name__} instead.") sceneggiatura: Sceneggiatura = content # --- STEP 4: Final Assembly --- logger.info("Team Pipeline: Assembling and validating final scenario...") final_scenario = FullScenario( **base_scenario.model_dump(), tempi=timeline.tempi, sceneggiatura=sceneggiatura.sceneggiatura ) return final_scenario except (ValidationError, TypeError) as e: logger.error(f"Data validation or type error in pipeline: {e}", exc_info=True) raise HTTPException(status_code=422, detail={"error": "Generated content failed validation or typing.", "details": str(e)}) except Exception as e: logger.error(f"Unexpected error in pipeline: {e}", exc_info=True) raise HTTPException(status_code=500, detail={"error": "Failed to generate scenario", "message": str(e)})
medical_team = MedicalScenarioTeam(members=[info_agent, timeline_agent, script_agent])
[docs] def generate_medical_scenario(_request: ScenarioRequest) -> FullScenario: """Generates a medical scenario by orchestrating a team of agents.""" logger.info(f"Received request to generate medical scenario: {_request.scenario_type} for {_request.target}") return medical_team.run(_request)