from __future__ import annotations import json from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any, cast from pydantic import BaseModel from agents import ( Agent, AgentHookContext, RunContextWrapper, RunHooks, Runner, SQLiteSession, Tool, gen_trace_id, trace, ) from agents.run import RunConfig from agents.sandbox import Manifest, SandboxPathGrant, SandboxRunConfig from agents.sandbox.entries import Dir, File, LocalDir from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient from agents.tool_context import ToolContext from examples.sandbox.healthcare_support.data import HealthcareSupportDataStore from examples.sandbox.healthcare_support.models import ( CaseResolution, MemoryRecap, ScenarioCase, ) from examples.sandbox.healthcare_support.support_agents import ( build_orchestrator, build_policy_sandbox_agent, memory_recap_agent, ) from examples.sandbox.healthcare_support.tools import HealthcareSupportContext EXAMPLE_ROOT = Path(__file__).resolve().parent POLICIES_ROOT = EXAMPLE_ROOT / "policies" SKILLS_ROOT = EXAMPLE_ROOT / "skills" SDK_ROOT = EXAMPLE_ROOT.parents[2] CACHE_ROOT = SDK_ROOT / ".cache" / "healthcare_support" SESSION_DB_PATH = CACHE_ROOT / "sessions.db" DEFAULT_SESSION_ID = "healthcare-support-demo-memory" ApprovalHandler = Callable[[dict[str, Any]], Awaitable[bool]] class WorkflowHooks(RunHooks[HealthcareSupportContext]): async def on_agent_start( self, context: AgentHookContext[HealthcareSupportContext], agent: Agent[HealthcareSupportContext], ) -> None: await context.context.emit("agent_start", agent=agent.name) async def on_agent_end( self, context: RunContextWrapper[HealthcareSupportContext], agent: Agent[HealthcareSupportContext], output: Any, ) -> None: await context.context.emit( "agent_end", agent=agent.name, output=_to_jsonable(output), ) async def on_tool_start( self, context: RunContextWrapper[HealthcareSupportContext], agent: Agent[HealthcareSupportContext], tool: Tool, ) -> None: tool_context = cast(ToolContext[HealthcareSupportContext], context) await context.context.emit( "tool_start", agent=agent.name, tool=tool.name, call_id=tool_context.tool_call_id, arguments=tool_context.tool_arguments, ) async def on_tool_end( self, context: RunContextWrapper[HealthcareSupportContext], agent: Agent[HealthcareSupportContext], tool: Tool, result: object, ) -> None: tool_context = cast(ToolContext[HealthcareSupportContext], context) await context.context.emit( "tool_end", agent=agent.name, tool=tool.name, call_id=tool_context.tool_call_id, output=_to_jsonable(result), ) def _to_jsonable(value: Any) -> Any: if isinstance(value, BaseModel): return value.model_dump(mode="json") if isinstance(value, dict | list | str | int | float | bool) or value is None: return value try: return json.loads(json.dumps(value, default=str)) except Exception: return str(value) def build_context( *, store: HealthcareSupportDataStore, scenario_id: str = "eligibility_verification_basic", session_id: str = DEFAULT_SESSION_ID, emit_event: Callable[[dict[str, Any]], Awaitable[None]] | None = None, ) -> HealthcareSupportContext: return HealthcareSupportContext( store=store, scenario=store.get_scenario(scenario_id), session_id=session_id, emit_event=emit_event, ) def _build_manifest(scenario: ScenarioCase) -> Manifest: return Manifest( extra_path_grants=( SandboxPathGrant(path=str(POLICIES_ROOT), read_only=True), SandboxPathGrant(path=str(SKILLS_ROOT), read_only=True), ), entries={ "case": Dir( children={ "scenario.json": File( content=json.dumps(scenario.model_dump(mode="json"), indent=2).encode( "utf-8" ) ), "transcript.txt": File(content=scenario.transcript.encode("utf-8")), }, description="Synthetic support request and scenario metadata.", ), "policies": LocalDir( src=POLICIES_ROOT, description="Local healthcare policy and workflow documents.", ), "output": Dir(description="Generated support artifacts for this case."), }, ) async def _structured_tool_output_extractor(result: Any) -> str: final_output = result.final_output if isinstance(final_output, BaseModel): return json.dumps(final_output.model_dump(mode="json"), sort_keys=True) return str(final_output) def _fallback_artifacts(*, scenario: ScenarioCase, resolution: CaseResolution) -> dict[str, str]: policy_doc = f"""# Policy Findings ## Case {scenario.description} ## Policy summary {resolution.policy_summary} ## Next step {resolution.next_step} """ checklist_doc = f"""# Human Review Checklist - Confirm whether the request needs prior authorization for this service and payer. - Verify referral state and any missing clinical or billing identifiers. - Use this internal summary: {resolution.internal_summary} - Patient-facing response: {resolution.patient_facing_response} """ return { "policy_findings.md": policy_doc, "human_review_checklist.md": checklist_doc, } async def _copy_output_files( *, sandbox: Any, scenario: ScenarioCase, resolution: CaseResolution, ) -> list[dict[str, str]]: scenario_id = scenario.scenario_id destination_root = CACHE_ROOT / "output" / scenario_id destination_root.mkdir(parents=True, exist_ok=True) copied_by_name: dict[str, dict[str, str]] = {} for entry in await sandbox.ls("output"): entry_path = Path(entry.path) if entry.is_dir(): continue handle = await sandbox.read(entry_path) try: payload = handle.read() finally: handle.close() local_path = destination_root / entry_path.name if isinstance(payload, str): content = payload local_path.write_text(content, encoding="utf-8") else: content = bytes(payload).decode("utf-8", errors="replace") local_path.write_text(content, encoding="utf-8") copied_by_name[entry_path.name] = { "name": entry_path.name, "path": str(local_path), "content": content, } for filename, content in _fallback_artifacts( scenario=scenario, resolution=resolution, ).items(): if filename in copied_by_name: continue local_path = destination_root / filename local_path.write_text(content, encoding="utf-8") copied_by_name[filename] = { "name": filename, "path": str(local_path), "content": content, } return [copied_by_name[name] for name in sorted(copied_by_name)] async def _resolve_interruptions( *, result: Any, orchestrator: Agent[HealthcareSupportContext], context: HealthcareSupportContext, conversation_session: SQLiteSession, hooks: WorkflowHooks, approval_handler: ApprovalHandler | None, ) -> Any: approval_round = 0 while result.interruptions: approval_round += 1 if approval_round > 5: raise RuntimeError("Exceeded 5 approval rounds while resuming the workflow.") state = result.to_state() CACHE_ROOT.mkdir(parents=True, exist_ok=True) state_payload = state.to_json( context_serializer=lambda value: { "scenario_id": value.scenario.scenario_id, "session_id": value.session_id, "human_handoffs": value.human_handoffs, } ) (CACHE_ROOT / "pending_state.json").write_text( json.dumps(state_payload, indent=2), encoding="utf-8", ) for interruption in result.interruptions: request = { "agent": interruption.agent.name, "tool": interruption.name, "arguments": _to_jsonable(interruption.arguments), } await context.emit("human_approval_requested", request=request) approved = True if approval_handler is None else await approval_handler(request) if approved: context.human_handoff_approved = True state.approve(interruption, always_approve=False) await context.emit("human_approval_resolved", approved=True, request=request) else: context.human_handoff_approved = False state.reject(interruption) await context.emit("human_approval_resolved", approved=False, request=request) result = await Runner.run( orchestrator, state, session=conversation_session, hooks=hooks, ) return result def _workflow_prompt(scenario: ScenarioCase) -> str: return json.dumps( { "scenario_id": scenario.scenario_id, "description": scenario.description, "transcript": scenario.transcript, "patient_metadata": scenario.patient_metadata, "followup_answers": scenario.followup_qa, }, indent=2, ) async def run_healthcare_support_workflow( *, context: HealthcareSupportContext, scenario_id: str, approval_handler: ApprovalHandler | None = None, ) -> dict[str, Any]: scenario = context.store.get_scenario(scenario_id) context.scenario = scenario context.human_handoffs.clear() context.human_handoff_approved = False await context.emit( "scenario_loaded", scenario_id=scenario.scenario_id, description=scenario.description, transcript=scenario.transcript, ) CACHE_ROOT.mkdir(parents=True, exist_ok=True) conversation_session = SQLiteSession( session_id=context.session_id or DEFAULT_SESSION_ID, db_path=SESSION_DB_PATH ) await context.emit("memory_ready", session_id=conversation_session.session_id) hooks = WorkflowHooks() sandbox_client = UnixLocalSandboxClient() sandbox = await sandbox_client.create(manifest=_build_manifest(scenario)) await context.emit( "sandbox_ready", backend="unix_local", workspace=["case/scenario.json", "case/transcript.txt", "policies/", "output/"], ) policy_agent = build_policy_sandbox_agent(skills_root=SKILLS_ROOT) sandbox_policy_tool = policy_agent.as_tool( tool_name="sandbox_policy_packet", tool_description="Inspect policy files in a sandbox and generate support artifacts.", custom_output_extractor=_structured_tool_output_extractor, run_config=RunConfig( sandbox=SandboxRunConfig(session=sandbox), workflow_name="Healthcare support sandbox packet", ), hooks=hooks, max_turns=20, ) orchestrator = build_orchestrator(sandbox_policy_tool=sandbox_policy_tool) trace_id = gen_trace_id() trace_url = f"https://platform.openai.com/traces/trace?trace_id={trace_id}" try: async with sandbox: await context.emit("trace_ready", trace_id=trace_id, trace_url=trace_url) with trace( "Healthcare support workflow", trace_id=trace_id, group_id=scenario.scenario_id, ): result = await Runner.run( orchestrator, _workflow_prompt(scenario), context=context, session=conversation_session, hooks=hooks, ) result = await _resolve_interruptions( result=result, orchestrator=orchestrator, context=context, conversation_session=conversation_session, hooks=hooks, approval_handler=approval_handler, ) resolution = result.final_output_as(CaseResolution) copied_files = await _copy_output_files( sandbox=sandbox, scenario=scenario, resolution=resolution, ) await context.emit("artifacts_ready", files=copied_files) memory_result = await Runner.run( memory_recap_agent, ( "Summarize what you remember from the session. Include patient, intent, " "handoff state, generated files, and next step." ), context=context, session=conversation_session, hooks=hooks, ) recap = memory_result.final_output_as(MemoryRecap) history_items = await conversation_session.get_items() payload = { "scenario_id": scenario.scenario_id, "description": scenario.description, "transcript": scenario.transcript, "trace_id": trace_id, "trace_url": trace_url, "resolution": resolution.model_dump(mode="json"), "memory_recap": recap.model_dump(mode="json"), "artifacts": copied_files, "session_id": conversation_session.session_id, "session_memory_items": len(history_items), } await context.emit("workflow_complete", payload=payload) return payload finally: await sandbox_client.delete(sandbox) await context.emit("sandbox_stopped", backend="unix_local")