chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Healthcare support
|
||||
|
||||
This example shows how to build a healthcare support workflow with Agents SDK using both standard agents and a sandbox agent. The scenario is intentionally synthetic and generic: a patient asks a billing or coverage question, the workflow checks local records, inspects policy documents in an isolated sandbox workspace, writes support artifacts, and optionally routes one ambiguous case to a human reviewer.
|
||||
|
||||
## What this example demonstrates
|
||||
|
||||
- **Standard agent orchestration** with a top-level support orchestrator and a benefits subagent.
|
||||
- **Sandbox agents** with a mounted workspace, shell commands, a generated output folder, and runtime-selected sandbox config.
|
||||
- **Sandbox capabilities** including `Shell`, `Filesystem`, and lazy-loaded `Skills`.
|
||||
- **Human-in-the-loop approvals** using an approval-gated queue-routing tool.
|
||||
- **Persistent memory** with `SQLiteSession`, shared across scenario runs.
|
||||
- **Structured outputs** for each specialist agent and the final case resolution.
|
||||
- **Tracing** so you can inspect every model call and tool call in the OpenAI trace viewer.
|
||||
- **CLI-first workflow** that can be run scenario by scenario from the repository checkout.
|
||||
|
||||
## Architecture
|
||||
|
||||
The workflow has two execution modes working together:
|
||||
|
||||
1. A **standard orchestrator agent** runs in the normal Agents SDK loop, calls the benefits subagent first, then calls a sandbox agent tool, and decides whether to request a human handoff.
|
||||
2. A **sandbox policy agent** runs behind `agents.sandbox`, reads the mounted case files and policy documents, uses shell commands plus a lazily loaded skill, writes markdown artifacts into `output/`, and returns a structured policy summary.
|
||||
|
||||
The local fixture data lives in `data/scenarios/*.json` and `data/fixtures/*.json`. The sandbox policy library lives in `policies/*.md`. Generated artifacts are copied to `.cache/healthcare_support/output/<scenario_id>/`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
The built-in scenarios increase in complexity:
|
||||
|
||||
- `eligibility_verification_basic` checks a straightforward benefits question.
|
||||
- `referral_status_check` adds a referral lookup.
|
||||
- `blue_cross_pt_benefits` shows a follow-up turn that benefits from the shared SQLite memory.
|
||||
- `prior_auth_confusion_ct` focuses on prior-authorization and intake-routing confusion.
|
||||
- `billing_coverage_clarification` combines benefits lookup with sandbox policy search and document generation.
|
||||
- `messy_ambiguous_knee_case` triggers the human approval flow before queueing a handoff.
|
||||
|
||||
## Run the CLI demo
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
uv run python examples/sandbox/healthcare_support/main.py
|
||||
```
|
||||
|
||||
Useful options:
|
||||
|
||||
```bash
|
||||
uv run python examples/sandbox/healthcare_support/main.py --list-scenarios
|
||||
uv run python examples/sandbox/healthcare_support/main.py --scenario blue_cross_pt_benefits
|
||||
uv run python examples/sandbox/healthcare_support/main.py --scenario messy_ambiguous_knee_case
|
||||
uv run python examples/sandbox/healthcare_support/main.py --reset-memory
|
||||
```
|
||||
|
||||
For unattended runs, set `EXAMPLES_INTERACTIVE_MODE=auto` to auto-answer prompts:
|
||||
|
||||
```bash
|
||||
EXAMPLES_INTERACTIVE_MODE=auto uv run python examples/sandbox/healthcare_support/main.py --scenario messy_ambiguous_knee_case
|
||||
```
|
||||
|
||||
## Files to read first
|
||||
|
||||
- [`main.py`](./main.py) runs the standalone CLI demo.
|
||||
- [`workflow.py`](./workflow.py) contains the shared workflow execution logic, sandbox setup, artifact copying, tracing, and approval resume loop.
|
||||
- [`support_agents.py`](./support_agents.py) defines the orchestrator, benefits subagent, sandbox policy agent, and memory recap agent.
|
||||
- [`tools.py`](./tools.py) defines the local lookup tools and the approval-gated human handoff tool.
|
||||
- [`skills/prior-auth-packet-builder/SKILL.md`](./skills/prior-auth-packet-builder/SKILL.md) is the sandbox skill loaded at runtime.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a demo workflow, not a production healthcare system.
|
||||
- All patient, payer, and policy data in this example is synthetic.
|
||||
- The example loads environment defaults from the repository-root `.env` file and from this demo's optional local `.env` file.
|
||||
@@ -0,0 +1 @@
|
||||
"""Synthetic healthcare support sandbox example."""
|
||||
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from examples.sandbox.healthcare_support.models import KnowledgeSnippet, ScenarioCase
|
||||
|
||||
EXAMPLE_ROOT = Path(__file__).resolve().parent
|
||||
SCENARIOS_DIR = EXAMPLE_ROOT / "data" / "scenarios"
|
||||
FIXTURES_DIR = EXAMPLE_ROOT / "data" / "fixtures"
|
||||
POLICIES_DIR = EXAMPLE_ROOT / "policies"
|
||||
ROOT_ENV_PATH = EXAMPLE_ROOT.parents[2] / ".env"
|
||||
DEMO_ENV_PATH = EXAMPLE_ROOT / ".env"
|
||||
|
||||
|
||||
def load_root_env() -> None:
|
||||
"""Load environment defaults from the repository root and this demo folder."""
|
||||
for env_path in (ROOT_ENV_PATH, DEMO_ENV_PATH):
|
||||
if not env_path.exists():
|
||||
continue
|
||||
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||
continue
|
||||
key, value = stripped.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def normalize_text(value: str) -> str:
|
||||
return " ".join(re.findall(r"[a-z0-9]+", value.lower()))
|
||||
|
||||
|
||||
def tokenize(value: str) -> set[str]:
|
||||
return set(re.findall(r"[a-z0-9]+", value.lower()))
|
||||
|
||||
|
||||
def normalize_date(value: str | None) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%Y/%m/%d", "%m-%d-%Y"):
|
||||
try:
|
||||
return datetime.strptime(value, fmt).strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
continue
|
||||
return "".join(re.findall(r"\d+", value))
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyDocument:
|
||||
document_id: str
|
||||
title: str
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class HealthcareSupportDataStore:
|
||||
scenarios: dict[str, ScenarioCase]
|
||||
patient_records: list[dict[str, Any]]
|
||||
eligibility_records: list[dict[str, Any]]
|
||||
referral_records: list[dict[str, Any]]
|
||||
policy_documents: list[PolicyDocument]
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> HealthcareSupportDataStore:
|
||||
scenarios = {
|
||||
path.stem: ScenarioCase.model_validate(json.loads(path.read_text(encoding="utf-8")))
|
||||
for path in sorted(SCENARIOS_DIR.glob("*.json"))
|
||||
}
|
||||
patient_records = json.loads(
|
||||
(FIXTURES_DIR / "patient_profiles.json").read_text(encoding="utf-8")
|
||||
)["records"]
|
||||
eligibility_records = json.loads(
|
||||
(FIXTURES_DIR / "insurance_eligibility.json").read_text(encoding="utf-8")
|
||||
)["records"]
|
||||
referral_records = json.loads(
|
||||
(FIXTURES_DIR / "referral_status.json").read_text(encoding="utf-8")
|
||||
)["records"]
|
||||
policy_documents = [
|
||||
PolicyDocument(
|
||||
document_id=path.stem,
|
||||
title=path.stem.replace("_", " ").title(),
|
||||
text=path.read_text(encoding="utf-8"),
|
||||
)
|
||||
for path in sorted(POLICIES_DIR.glob("*.md"))
|
||||
]
|
||||
return cls(
|
||||
scenarios=scenarios,
|
||||
patient_records=patient_records,
|
||||
eligibility_records=eligibility_records,
|
||||
referral_records=referral_records,
|
||||
policy_documents=policy_documents,
|
||||
)
|
||||
|
||||
def list_scenario_ids(self) -> list[str]:
|
||||
return sorted(self.scenarios)
|
||||
|
||||
def get_scenario(self, scenario_id: str) -> ScenarioCase:
|
||||
try:
|
||||
return self.scenarios[scenario_id]
|
||||
except KeyError as exc:
|
||||
raise KeyError(f"Unknown scenario_id: {scenario_id}") from exc
|
||||
|
||||
def search_policies(self, query: str, top_k: int = 4) -> list[KnowledgeSnippet]:
|
||||
query_terms = tokenize(query)
|
||||
if not query_terms:
|
||||
return []
|
||||
|
||||
scored: list[KnowledgeSnippet] = []
|
||||
for document in self.policy_documents:
|
||||
matched_terms = sorted(query_terms & tokenize(document.text))
|
||||
if not matched_terms:
|
||||
continue
|
||||
score = round(len(matched_terms) / max(len(query_terms), 1), 4)
|
||||
snippet = " ".join(document.text.split())[:320]
|
||||
scored.append(
|
||||
KnowledgeSnippet(
|
||||
document_id=document.document_id,
|
||||
title=document.title,
|
||||
chunk_id=f"{document.document_id}:0",
|
||||
score=score,
|
||||
snippet=snippet,
|
||||
matched_terms=matched_terms,
|
||||
)
|
||||
)
|
||||
|
||||
scored.sort(key=lambda item: item.score, reverse=True)
|
||||
return scored[:top_k]
|
||||
|
||||
def lookup_patient(
|
||||
self,
|
||||
*,
|
||||
patient_id: str | None = None,
|
||||
phone: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
for record in self.patient_records:
|
||||
if patient_id and record.get("patient_id") == patient_id:
|
||||
return {"lookup_status": "matched", "record": record}
|
||||
if phone and record.get("phone") == phone:
|
||||
return {"lookup_status": "matched", "record": record}
|
||||
if name and normalize_text(record.get("name", "")) == normalize_text(name):
|
||||
return {"lookup_status": "matched", "record": record}
|
||||
return {"lookup_status": "not_found", "record": None}
|
||||
|
||||
def lookup_eligibility(
|
||||
self,
|
||||
*,
|
||||
payer: str | None = None,
|
||||
member_id: str | None = None,
|
||||
dob: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payer_norm = normalize_text(payer or "")
|
||||
dob_norm = normalize_date(dob)
|
||||
fallback_match: dict[str, Any] | None = None
|
||||
|
||||
for record in self.eligibility_records:
|
||||
if member_id and record.get("member_id") != member_id:
|
||||
continue
|
||||
if dob_norm and normalize_date(record.get("dob")) != dob_norm:
|
||||
continue
|
||||
if payer_norm:
|
||||
if normalize_text(record.get("payer", "")) == payer_norm:
|
||||
return {"lookup_status": "matched", **record}
|
||||
continue
|
||||
if fallback_match is None:
|
||||
fallback_match = {"lookup_status": "matched", **record}
|
||||
|
||||
if fallback_match is not None:
|
||||
return fallback_match
|
||||
|
||||
return {
|
||||
"lookup_status": "not_found",
|
||||
"eligibility_status": "unknown",
|
||||
"notes": "No eligibility match. Ask for payer, member ID, and date of birth.",
|
||||
}
|
||||
|
||||
def lookup_referral(
|
||||
self,
|
||||
*,
|
||||
referral_id: str | None = None,
|
||||
patient_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
for record in self.referral_records:
|
||||
if referral_id and record.get("referral_id") == referral_id:
|
||||
return {"lookup_status": "matched", **record}
|
||||
if patient_id and record.get("patient_id") == patient_id:
|
||||
return {"lookup_status": "matched", **record}
|
||||
return {"lookup_status": "not_found", "status": "unknown"}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-4439201",
|
||||
"dob": "1985-02-14",
|
||||
"plan_name": "Blue Cross PPO Silver 4500",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$35",
|
||||
"copay_specialist": "$60",
|
||||
"deductible_remaining": "$1,200",
|
||||
"prior_auth_required_services": [
|
||||
"mri",
|
||||
"ct angiogram",
|
||||
"elective surgery"
|
||||
],
|
||||
"notes": "Coverage active. MRI requires prior authorization except emergency use."
|
||||
},
|
||||
{
|
||||
"payer": "UnitedHealthcare",
|
||||
"member_id": "UHC-771032",
|
||||
"dob": "1990-09-03",
|
||||
"plan_name": "UHC Choice Plus Bronze",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$30",
|
||||
"copay_specialist": "$75",
|
||||
"deductible_remaining": "$2,050",
|
||||
"prior_auth_required_services": [
|
||||
"ct angiogram",
|
||||
"inpatient admission",
|
||||
"outpatient surgery"
|
||||
],
|
||||
"notes": "Prior auth required for CT angiogram unless ordered in emergency setting."
|
||||
},
|
||||
{
|
||||
"payer": "Aetna",
|
||||
"member_id": "AET-562100",
|
||||
"dob": "1978-11-20",
|
||||
"plan_name": "Aetna Open Access Basic",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$25",
|
||||
"copay_specialist": "$50",
|
||||
"deductible_remaining": "$850",
|
||||
"prior_auth_required_services": [
|
||||
"specialist consult"
|
||||
],
|
||||
"notes": "Referral on file for specialist consult."
|
||||
},
|
||||
{
|
||||
"payer": "Cigna",
|
||||
"member_id": "CG-291001",
|
||||
"dob": "1982-06-30",
|
||||
"plan_name": "Cigna Connect Gold",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$20",
|
||||
"copay_specialist": "$45",
|
||||
"deductible_remaining": "$300",
|
||||
"prior_auth_required_services": [
|
||||
"advanced imaging",
|
||||
"elective procedures"
|
||||
],
|
||||
"notes": "Claims for advanced imaging can deny if authorization is missing."
|
||||
},
|
||||
{
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-8822009",
|
||||
"dob": "1974-05-12",
|
||||
"plan_name": "Blue Cross PPO Platinum",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$20",
|
||||
"copay_specialist": "$40",
|
||||
"deductible_remaining": "$0",
|
||||
"prior_auth_required_services": [
|
||||
"physical therapy after 12 visits"
|
||||
],
|
||||
"notes": "Physical therapy benefit allows 12 visits without prior authorization per calendar year."
|
||||
},
|
||||
{
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-9017710",
|
||||
"dob": "1992-04-17",
|
||||
"plan_name": "Blue Cross PPO Silver 3000",
|
||||
"eligibility_status": "active",
|
||||
"copay_primary_care": "$30",
|
||||
"copay_specialist": "$55",
|
||||
"deductible_remaining": "$1,600",
|
||||
"prior_auth_required_services": [
|
||||
"mri",
|
||||
"knee surgery consult",
|
||||
"outpatient surgery"
|
||||
],
|
||||
"notes": "Prior auth normally required for knee surgery consult and advanced imaging."
|
||||
}
|
||||
],
|
||||
"default_response": {
|
||||
"eligibility_status": "unknown",
|
||||
"notes": "No eligibility match. Confirm payer, member ID, and DOB."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"patient_id": "PAT-1001",
|
||||
"name": "Maya Thompson",
|
||||
"dob": "1985-02-14",
|
||||
"phone": "555-0111",
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-4439201",
|
||||
"referral_id": "REF-44120"
|
||||
},
|
||||
{
|
||||
"patient_id": "PAT-1002",
|
||||
"name": "Victor Chen",
|
||||
"dob": "1990-09-03",
|
||||
"phone": "555-0122",
|
||||
"payer": "UnitedHealthcare",
|
||||
"member_id": "UHC-771032",
|
||||
"referral_id": "REF-77100"
|
||||
},
|
||||
{
|
||||
"patient_id": "PAT-1003",
|
||||
"name": "Nora Patel",
|
||||
"dob": "1978-11-20",
|
||||
"phone": "555-0133",
|
||||
"payer": "Aetna",
|
||||
"member_id": "AET-562100",
|
||||
"referral_id": "REF-88421"
|
||||
},
|
||||
{
|
||||
"patient_id": "PAT-1004",
|
||||
"name": "Luis Romero",
|
||||
"dob": "1982-06-30",
|
||||
"phone": "555-0144",
|
||||
"payer": "Cigna",
|
||||
"member_id": "CG-291001",
|
||||
"referral_id": "REF-12880"
|
||||
},
|
||||
{
|
||||
"patient_id": "PAT-1005",
|
||||
"name": "Ella Brooks",
|
||||
"dob": "1974-05-12",
|
||||
"phone": "555-0155",
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-8822009",
|
||||
"referral_id": "REF-33002"
|
||||
},
|
||||
{
|
||||
"patient_id": "PAT-1006",
|
||||
"name": "Jordan Lee",
|
||||
"dob": "1992-04-17",
|
||||
"phone": "555-0134",
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-9017710",
|
||||
"referral_id": "REF-90171"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"referral_id": "REF-88421",
|
||||
"patient_id": "PAT-1003",
|
||||
"status": "approved",
|
||||
"specialty": "Cardiology",
|
||||
"requested_provider": "Dr. Ramos",
|
||||
"authorized_visits": 6,
|
||||
"remaining_visits": 4,
|
||||
"notes": "Authorization valid through 2026-07-31."
|
||||
},
|
||||
{
|
||||
"referral_id": "REF-77100",
|
||||
"patient_id": "PAT-1002",
|
||||
"status": "pending_clinical_review",
|
||||
"specialty": "Radiology",
|
||||
"requested_provider": "Riverfront Imaging",
|
||||
"authorized_visits": 1,
|
||||
"remaining_visits": 0,
|
||||
"notes": "Pending prior authorization packet completion."
|
||||
},
|
||||
{
|
||||
"referral_id": "REF-90171",
|
||||
"patient_id": "PAT-1006",
|
||||
"status": "pending",
|
||||
"specialty": "Orthopedics",
|
||||
"requested_provider": "Summit Ortho Group",
|
||||
"authorized_visits": 8,
|
||||
"remaining_visits": 8,
|
||||
"notes": "Awaiting payer determination."
|
||||
}
|
||||
]
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"scenario_id": "billing_coverage_clarification",
|
||||
"description": "Patient received an unexpected imaging bill and wants coverage clarification.",
|
||||
"transcript": "Hey, this is Luis Romero. I got a bill after an ultrasound on 2026-02-08 and I thought it was covered.\nMy insurance is Cigna and my member ID is CG-291001.\nCan someone explain what happened and what I should do now?",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1004"
|
||||
},
|
||||
"followup_qa": {
|
||||
"date of service": "2026-02-08",
|
||||
"payer": "Cigna"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "billing_coverage_clarification",
|
||||
"required_entities": {
|
||||
"payer": "Cigna",
|
||||
"member_id": "CG-291001"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"insurance_eligibility_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"billing coverage review",
|
||||
"recommended next step"
|
||||
],
|
||||
"expected_payer": "Cigna"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Route to billing review with EOB and service date context."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"scenario_id": "blue_cross_pt_benefits",
|
||||
"description": "Blue Cross member asks about remaining physical therapy benefit and coverage path.",
|
||||
"transcript": "This is Ella Brooks. I am a Blue Cross member and my ID is BCX-8822009.\nI am trying to continue physical therapy and need to know if I still have covered visits left.\nI do not have my date of birth in front of me if you need it.",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1005"
|
||||
},
|
||||
"followup_qa": {
|
||||
"date of birth": "05/12/1974",
|
||||
"physical therapy": "physical therapy"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "eligibility_verification",
|
||||
"required_entities": {
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-8822009"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"insurance_eligibility_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"eligibility verified",
|
||||
"recommended next step"
|
||||
],
|
||||
"expected_payer": "Blue Cross"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Confirm PT visit limits and advise on when additional review is needed."
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"scenario_id": "eligibility_verification_basic",
|
||||
"description": "Basic eligibility verification call with clear Blue Cross identifiers.",
|
||||
"transcript": "Hi, this is Maya Thompson. I have an MRI next week and I want to confirm if it is covered.\nI have Blue Cross and my member ID is BCX-4439201. My date of birth is 02/14/1985.\nCan you tell me what my benefits look like and what I should do next?",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1001"
|
||||
},
|
||||
"followup_qa": {
|
||||
"member ID": "BCX-4439201",
|
||||
"date of birth": "02/14/1985"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "eligibility_verification",
|
||||
"required_entities": {
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-4439201"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"insurance_eligibility_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"eligibility verified",
|
||||
"recommended next step"
|
||||
],
|
||||
"expected_payer": "Blue Cross"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Confirm prior auth requirement for MRI and proceed with scheduling."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"scenario_id": "messy_ambiguous_knee_case",
|
||||
"description": "Messy real-world call with ambiguous details requiring follow-up, retrieval, and multiple tool invocations.",
|
||||
"transcript": "Hi, this is Jordan Lee. I had a knee surgery consult and maybe some imaging planned, then I got mixed messages about auth.\nI also saw a bill and I am not sure if this is Blue something PPO or what.\nMy phone is 555-0134 and I think the referral might be REF-90171.\nCan you figure out what I need to do next?",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1006"
|
||||
},
|
||||
"followup_qa": {
|
||||
"insurance payer": "Blue Cross",
|
||||
"member ID": "BCX-9017710",
|
||||
"date of birth": "04/17/1992",
|
||||
"procedure or visit type": "knee surgery consult",
|
||||
"referral ID": "REF-90171"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "prior_auth_confusion",
|
||||
"required_entities": {
|
||||
"payer": "Blue Cross",
|
||||
"member_id": "BCX-9017710"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"insurance_eligibility_lookup",
|
||||
"appointment_referral_status_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"prior authorization",
|
||||
"recommended next step"
|
||||
],
|
||||
"expected_payer": "Blue Cross"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Route to auth queue and share referral pending status with patient."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"scenario_id": "prior_auth_confusion_ct",
|
||||
"description": "Caller is confused about whether CT angiogram needs prior auth and what intake should do.",
|
||||
"transcript": "This is Victor Chen. I was told to schedule a CT angiogram, but another office said prior authorization is missing.\nMy insurance is UnitedHealthcare and I think my ID is UHC-771032.\nI need to know if I can move forward or if you need more information.",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1002"
|
||||
},
|
||||
"followup_qa": {
|
||||
"date of birth": "09/03/1990",
|
||||
"procedure or visit type": "CT angiogram",
|
||||
"payer": "UnitedHealthcare",
|
||||
"member ID": "UHC-771032"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "prior_auth_confusion",
|
||||
"required_entities": {
|
||||
"payer": "UnitedHealthcare",
|
||||
"member_id": "UHC-771032"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"insurance_eligibility_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"prior authorization",
|
||||
"recommended next step"
|
||||
],
|
||||
"expected_payer": "UnitedHealthcare"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Route to utilization review with CT angiogram authorization packet."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"scenario_id": "referral_status_check",
|
||||
"description": "Patient asks for specialist referral status with known referral ID.",
|
||||
"transcript": "Hi, this is Nora Patel. I am checking on referral number REF-88421 for cardiology with Dr. Ramos.\nCan you tell me if it has been approved and how many visits I still have?",
|
||||
"patient_metadata": {
|
||||
"patient_id": "PAT-1003"
|
||||
},
|
||||
"followup_qa": {
|
||||
"referral number": "REF-88421",
|
||||
"provider": "Dr. Ramos"
|
||||
},
|
||||
"expected": {
|
||||
"intent": "referral_status_question",
|
||||
"required_entities": {
|
||||
"referral_id": "REF-88421"
|
||||
},
|
||||
"required_tool_calls": [
|
||||
"appointment_referral_status_lookup"
|
||||
],
|
||||
"required_resolution_elements": [
|
||||
"referral",
|
||||
"remaining authorized visits"
|
||||
],
|
||||
"expected_payer": "Aetna"
|
||||
},
|
||||
"gold": {
|
||||
"expected_next_step": "Notify patient referral is approved and proceed to specialist scheduling."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
if __package__ is None or __package__ == "":
|
||||
_DEMO_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_DEMO_DIR.parents[2]))
|
||||
sys.path.insert(0, str(_DEMO_DIR))
|
||||
|
||||
from examples.auto_mode import confirm_with_fallback, input_with_fallback # noqa: E402
|
||||
from examples.sandbox.healthcare_support.data import ( # noqa: E402
|
||||
HealthcareSupportDataStore,
|
||||
load_root_env,
|
||||
)
|
||||
from examples.sandbox.healthcare_support.models import ScenarioCase # noqa: E402
|
||||
from examples.sandbox.healthcare_support.tools import HealthcareSupportContext # noqa: E402
|
||||
from examples.sandbox.healthcare_support.workflow import ( # noqa: E402
|
||||
CACHE_ROOT,
|
||||
DEFAULT_SESSION_ID,
|
||||
SESSION_DB_PATH,
|
||||
build_context,
|
||||
run_healthcare_support_workflow,
|
||||
)
|
||||
|
||||
DEFAULT_SCENARIO_ID = "eligibility_verification_basic"
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the healthcare support Agents SDK demo from the command line.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scenario",
|
||||
dest="scenario_id",
|
||||
default=None,
|
||||
help="Scenario ID to run. If omitted, the CLI asks interactively.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-scenarios",
|
||||
action="store_true",
|
||||
help="Print the built-in scenario IDs and exit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reset-memory",
|
||||
action="store_true",
|
||||
help="Delete the shared SQLite session database before running.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def _print_scenarios(store: HealthcareSupportDataStore) -> None:
|
||||
print("Available scenarios:\n")
|
||||
for scenario_id in store.list_scenario_ids():
|
||||
scenario = store.get_scenario(scenario_id)
|
||||
print(f"- {scenario.scenario_id}")
|
||||
print(f" {scenario.description}")
|
||||
|
||||
|
||||
def _pick_scenario(store: HealthcareSupportDataStore, requested_id: str | None) -> ScenarioCase:
|
||||
if requested_id:
|
||||
return store.get_scenario(requested_id)
|
||||
|
||||
scenario_id = input_with_fallback(
|
||||
"Enter a scenario ID: ",
|
||||
DEFAULT_SCENARIO_ID,
|
||||
).strip()
|
||||
if not scenario_id:
|
||||
scenario_id = DEFAULT_SCENARIO_ID
|
||||
return store.get_scenario(scenario_id)
|
||||
|
||||
|
||||
async def _approval_handler(request: dict[str, Any]) -> bool:
|
||||
print("\nHuman approval requested")
|
||||
print(f"Agent: {request.get('agent', 'unknown')}")
|
||||
print(f"Tool: {request.get('tool', 'route_to_human_queue')}")
|
||||
print(json.dumps(request.get("arguments", {}), indent=2))
|
||||
return confirm_with_fallback("Approve handoff to a human queue? [y/N]: ", True)
|
||||
|
||||
|
||||
def _print_run_header(*, scenario: ScenarioCase, context: HealthcareSupportContext) -> None:
|
||||
print("\n" + "=" * 80)
|
||||
print("Healthcare Support Agents SDK Demo")
|
||||
print(f"Scenario: {scenario.scenario_id}")
|
||||
print(f"Description: {scenario.description}")
|
||||
print(f"SQLite memory session: {context.session_id}")
|
||||
print("\nCustomer transcript:\n")
|
||||
print(scenario.transcript)
|
||||
|
||||
|
||||
def _print_run_result(payload: dict[str, Any]) -> None:
|
||||
print("\nTrace URL:")
|
||||
print(payload["trace_url"])
|
||||
|
||||
print("\nPatient-facing response:\n")
|
||||
print(payload["resolution"]["patient_facing_response"])
|
||||
|
||||
print("\nInternal summary:")
|
||||
print(payload["resolution"]["internal_summary"])
|
||||
|
||||
print("\nNext step:")
|
||||
print(payload["resolution"]["next_step"])
|
||||
|
||||
if payload["resolution"].get("handoff_id"):
|
||||
print("\nHuman handoff:")
|
||||
print(payload["resolution"]["handoff_id"])
|
||||
|
||||
print("\nGenerated sandbox artifacts:")
|
||||
for artifact in payload.get("artifacts", []):
|
||||
print(f"- {artifact['path']}")
|
||||
|
||||
print("\nMemory recap:")
|
||||
print(json.dumps(payload["memory_recap"], indent=2))
|
||||
|
||||
print(f"\nSession memory items: {payload['session_memory_items']}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
load_root_env()
|
||||
args = _build_parser().parse_args()
|
||||
store = HealthcareSupportDataStore.load()
|
||||
|
||||
if args.list_scenarios:
|
||||
_print_scenarios(store)
|
||||
return
|
||||
|
||||
if args.reset_memory and SESSION_DB_PATH.exists():
|
||||
SESSION_DB_PATH.unlink()
|
||||
|
||||
scenario = _pick_scenario(store, args.scenario_id)
|
||||
context = build_context(
|
||||
store=store,
|
||||
scenario_id=scenario.scenario_id,
|
||||
session_id=DEFAULT_SESSION_ID,
|
||||
)
|
||||
CACHE_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_print_run_header(scenario=scenario, context=context)
|
||||
payload = await run_healthcare_support_workflow(
|
||||
context=context,
|
||||
scenario_id=scenario.scenario_id,
|
||||
approval_handler=_approval_handler,
|
||||
)
|
||||
_print_run_result(payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
IntentName = Literal[
|
||||
"eligibility_verification",
|
||||
"prior_auth_confusion",
|
||||
"referral_status_question",
|
||||
"billing_coverage_clarification",
|
||||
"general_intake",
|
||||
]
|
||||
|
||||
|
||||
class ScenarioExpectation(BaseModel):
|
||||
intent: IntentName
|
||||
required_entities: dict[str, str] = Field(default_factory=dict)
|
||||
required_tool_calls: list[str] = Field(default_factory=list)
|
||||
required_resolution_elements: list[str] = Field(default_factory=list)
|
||||
expected_payer: str | None = None
|
||||
|
||||
|
||||
class ScenarioCase(BaseModel):
|
||||
scenario_id: str
|
||||
description: str
|
||||
transcript: str
|
||||
patient_metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
followup_qa: dict[str, str] = Field(default_factory=dict)
|
||||
expected: ScenarioExpectation
|
||||
gold: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class KnowledgeSnippet(BaseModel):
|
||||
document_id: str
|
||||
title: str
|
||||
chunk_id: str
|
||||
score: float
|
||||
snippet: str
|
||||
matched_terms: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BenefitReview(BaseModel):
|
||||
patient_name: str
|
||||
patient_id: str
|
||||
payer: str
|
||||
member_id: str
|
||||
eligibility_status: str
|
||||
plan_summary: str
|
||||
referral_status: str
|
||||
prior_auth_recommended: bool
|
||||
recommended_queue: str
|
||||
summary: str
|
||||
|
||||
|
||||
class SandboxPolicyPacket(BaseModel):
|
||||
matched_policy_files: list[str] = Field(default_factory=list)
|
||||
generated_files: list[str] = Field(default_factory=list)
|
||||
shell_commands: list[str] = Field(default_factory=list)
|
||||
policy_summary: str
|
||||
human_review_recommended: bool
|
||||
|
||||
|
||||
class CaseResolution(BaseModel):
|
||||
scenario_id: str
|
||||
intent: IntentName
|
||||
patient_name: str
|
||||
benefits_summary: str
|
||||
policy_summary: str
|
||||
next_step: str
|
||||
route_to_human: bool
|
||||
handoff_id: str | None = None
|
||||
generated_files: list[str] = Field(default_factory=list)
|
||||
internal_summary: str
|
||||
patient_facing_response: str
|
||||
|
||||
|
||||
class MemoryRecap(BaseModel):
|
||||
remembered_patient: str | None = None
|
||||
remembered_intent: IntentName | None = None
|
||||
remembered_next_step: str
|
||||
remembered_handoff: str | None = None
|
||||
remembered_files: list[str] = Field(default_factory=list)
|
||||
@@ -0,0 +1,6 @@
|
||||
# Auth Review Queue Routing
|
||||
|
||||
- Route to auth-review-queue when prior authorization is required, likely required, or blocked by missing CPT/diagnosis details.
|
||||
- Route to care-team-intake-queue when referral or scheduling data is incomplete but payer auth is not yet indicated.
|
||||
- Route to billing-review-queue only for claim denial, refund, or balance disputes.
|
||||
- High-priority auth review applies when surgery or advanced imaging is expected within 14 days.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Billing After Consult FAQ
|
||||
|
||||
- A consult bill can be generated before imaging or surgery authorization is complete.
|
||||
- Patients often confuse referral approval, prior authorization, and claim adjudication.
|
||||
- Staff should explain that consult billing does not confirm surgery authorization.
|
||||
- If the patient reports a bill plus auth confusion, verify eligibility and route to billing only when the question is about claim denial or patient balance.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Blue Cross Benefits Reference
|
||||
|
||||
- Common PPO orthopedic specialist copays range from $40 to $75 depending on employer group.
|
||||
- Deductible and coinsurance still apply to imaging and outpatient surgery.
|
||||
- Benefit verification should capture specialist copay, deductible remaining, and coinsurance.
|
||||
- Benefits data should be summarized separately from authorization status.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blue Cross PPO Prior Authorization
|
||||
|
||||
- PPO members require prior authorization for inpatient surgery, outpatient surgery over $1,500, and advanced imaging tied to surgical planning.
|
||||
- Knee surgery consults do not require prior authorization by themselves.
|
||||
- MRI or CT imaging ordered after the consult may require prior authorization if performed at a hospital outpatient department.
|
||||
- If referral status is pending, route to auth review before scheduling imaging.
|
||||
- Required fields: member ID, date of birth, ordering provider, CPT code, diagnosis code.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Blue Cross Referral Rules
|
||||
|
||||
- PPO plans do not usually require a PCP referral for orthopedic consults.
|
||||
- Some employer groups still require a referral number for specialist scheduling.
|
||||
- If a referral exists but is pending, staff should verify status before confirming downstream imaging or surgery appointments.
|
||||
- Pending referrals should be routed to the care-team intake queue or auth-review queue depending on whether authorization is also required.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Commercial Eligibility Checklist
|
||||
|
||||
- Verify payer name, member ID, date of birth, and plan status.
|
||||
- Confirm effective date, termination date, copay, deductible, and coinsurance.
|
||||
- If payer name is ambiguous, use member ID and DOB to identify the most likely eligibility match.
|
||||
- Eligibility verification does not replace prior authorization review.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Human Escalation Policy
|
||||
|
||||
- Escalate to a human when payer is ambiguous, prior authorization is likely, referral is pending, or procedure coding is incomplete.
|
||||
- Escalate when patient asks for next steps and multiple operational dependencies are unresolved.
|
||||
- Human queue payloads should include patient summary, payer, member ID, referral ID, requested service, and missing information.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Knee Surgery Medical Necessity
|
||||
|
||||
- Surgical review packets should include consult notes, imaging results, diagnosis, failed conservative treatment, and requested CPT code.
|
||||
- Missing imaging results are a common reason for delayed authorization.
|
||||
- If the patient has a consult but no final procedure code, route to human review for packet completion before payer submission.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Orthopedic Imaging Policy
|
||||
|
||||
- X-ray does not require prior authorization for most commercial plans.
|
||||
- MRI of knee without contrast often requires prior authorization when ordered before surgery.
|
||||
- CT lower extremity may require prior authorization when tied to operative planning.
|
||||
- Imaging requests should include laterality, diagnosis code, and conservative treatment history when available.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Outbound Fax Packet Requirements
|
||||
|
||||
- Prior auth packets should include cover sheet, demographics, insurance card data, consult notes, imaging reports, and requested CPT/ICD-10 codes.
|
||||
- If any required artifact is missing, create a missing-items checklist before faxing.
|
||||
- Human review is required before outbound fax when packet data is incomplete or referral status is pending.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Patient Messaging Guidelines
|
||||
|
||||
- Use plain language and separate what is verified from what is still under review.
|
||||
- Do not tell a patient that surgery is approved unless payer authorization is confirmed.
|
||||
- If referral is pending, say that the referral is still being reviewed and that the care team is checking whether payer authorization is also needed.
|
||||
- Provide one clear next step and one expected owner queue.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Referral Pending SOP
|
||||
|
||||
- Confirm referral ID, patient identity, and rendering specialist before escalation.
|
||||
- If referral status is pending for more than two business days, send to care-team intake queue.
|
||||
- If referral is pending and prior authorization is also likely, send to auth-review queue with a note that referral clearance is still outstanding.
|
||||
- Patient messaging should distinguish referral review from payer authorization.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Scheduling Hold Policy
|
||||
|
||||
- Do not schedule surgery until required payer authorization is approved.
|
||||
- Imaging may be tentatively scheduled only when policy allows no-auth outpatient imaging.
|
||||
- If referral or authorization is pending, place a scheduling hold and notify the patient of the review owner.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: prior-auth-packet-builder
|
||||
description: Build a concise prior authorization packet from local case files and payer policy docs.
|
||||
---
|
||||
|
||||
# Prior Auth Packet Builder
|
||||
|
||||
Use this skill when a case requires prior authorization review, referral validation, imaging review, or payer-specific policy checks.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect `case/scenario.json` and `case/transcript.txt`.
|
||||
2. Use `rg` against `policies/` to find payer, prior auth, referral, imaging, and PPO guidance.
|
||||
3. Read only the most relevant policy files.
|
||||
4. Create `output/policy_findings.md` with:
|
||||
- case summary
|
||||
- matched policy files
|
||||
- prior auth determination
|
||||
- referral determination
|
||||
- missing information
|
||||
5. Create `output/human_review_checklist.md` with:
|
||||
- what a human reviewer should verify
|
||||
- what to tell the patient
|
||||
- what queue should own the case
|
||||
|
||||
## Rules
|
||||
|
||||
- Use targeted `rg` searches over broad file reads.
|
||||
- Only cite policy files you actually inspected.
|
||||
- Keep outputs concise and operational.
|
||||
- If referral status is pending and prior auth is unclear, recommend human review.
|
||||
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from agents import Agent, AgentOutputSchema, ModelSettings, Tool
|
||||
from agents.sandbox import SandboxAgent
|
||||
from agents.sandbox.capabilities import Filesystem, LocalDirLazySkillSource, Shell, Skills
|
||||
from agents.sandbox.entries import LocalDir
|
||||
from examples.sandbox.healthcare_support.models import (
|
||||
BenefitReview,
|
||||
CaseResolution,
|
||||
MemoryRecap,
|
||||
SandboxPolicyPacket,
|
||||
)
|
||||
from examples.sandbox.healthcare_support.tools import (
|
||||
HealthcareSupportContext,
|
||||
lookup_insurance_eligibility,
|
||||
lookup_patient,
|
||||
lookup_referral_status,
|
||||
route_to_human_queue,
|
||||
)
|
||||
|
||||
BENEFITS_PROMPT = """
|
||||
You are a healthcare benefits specialist in a synthetic support workflow.
|
||||
|
||||
Use the available lookup tools to verify patient, eligibility, and referral details, then return a
|
||||
structured benefits review.
|
||||
|
||||
Rules:
|
||||
1. Call `patient_info_lookup` first when you have a patient ID, phone number, or patient name.
|
||||
2. Call `insurance_eligibility_lookup` when payer, member ID, or date of birth is available.
|
||||
3. Call `appointment_referral_status_lookup` when referral ID or patient ID is available.
|
||||
4. Recommend prior-auth review only when the case involves imaging, surgery, a pending referral, or
|
||||
policy-specific authorization language.
|
||||
5. Set `recommended_queue` to one of `care-team-intake-queue`, `auth-review-queue`, or
|
||||
`billing-review-queue`.
|
||||
6. Keep the summary concise and grounded in tool output.
|
||||
""".strip()
|
||||
|
||||
|
||||
POLICY_SANDBOX_PROMPT = """
|
||||
You are a policy packet specialist running inside a sandbox workspace.
|
||||
|
||||
Inspect the case files and local policy library, generate concise markdown artifacts in `output/`,
|
||||
and return a structured packet summary.
|
||||
|
||||
You must:
|
||||
1. Load and use the `prior-auth-packet-builder` skill.
|
||||
2. Inspect the workspace with shell commands before writing anything.
|
||||
3. Use `rg` against `policies/` for prior-auth, imaging, referral, billing, PPO, and Blue Cross
|
||||
policy guidance.
|
||||
4. Create `output/policy_findings.md` with the most relevant policy guidance.
|
||||
5. Create `output/human_review_checklist.md` with a short checklist for a human reviewer.
|
||||
6. Set `human_review_recommended=true` only when the policy search or case input shows missing
|
||||
authorization/referral details that should be reviewed by a human before responding.
|
||||
7. Include the exact shell commands you ran in `shell_commands`.
|
||||
8. Return only facts grounded in the files you inspected.
|
||||
""".strip()
|
||||
|
||||
|
||||
ORCHESTRATOR_PROMPT = """
|
||||
You are a healthcare support orchestrator.
|
||||
|
||||
Coordinate a synthetic support case by combining a benefits review, a sandbox policy packet review,
|
||||
and a human handoff only when the case genuinely needs it.
|
||||
|
||||
Rules:
|
||||
1. Always call `benefits_review` first.
|
||||
2. Always call `sandbox_policy_packet` second.
|
||||
3. For this demo, call `route_to_human_queue` only for the
|
||||
`messy_ambiguous_knee_case` scenario when the sandbox packet recommends human review.
|
||||
4. Do not escalate the other four scenarios; answer those directly from the benefits and sandbox
|
||||
outputs.
|
||||
5. If you call `route_to_human_queue`, include the returned `handoff_id` and set
|
||||
`route_to_human=true`.
|
||||
6. Produce a clear patient-facing response, a short internal summary, and a concrete next step.
|
||||
7. Use only facts from the tool outputs and the supplied scenario payload.
|
||||
""".strip()
|
||||
|
||||
|
||||
MEMORY_PROMPT = """
|
||||
Summarize what you remember from this SQLite-backed session about the prior patient support cases.
|
||||
|
||||
Include the most recently remembered patient, intent, handoff status, generated files, and next
|
||||
step. Do not call tools.
|
||||
""".strip()
|
||||
|
||||
|
||||
benefits_agent = Agent[HealthcareSupportContext](
|
||||
name="HealthcareBenefitsAgent",
|
||||
model="gpt-5.6-sol",
|
||||
instructions=BENEFITS_PROMPT,
|
||||
model_settings=ModelSettings(reasoning=Reasoning(effort="low"), verbosity="low"),
|
||||
tools=[
|
||||
lookup_patient,
|
||||
lookup_insurance_eligibility,
|
||||
lookup_referral_status,
|
||||
],
|
||||
output_type=AgentOutputSchema(BenefitReview, strict_json_schema=False),
|
||||
)
|
||||
|
||||
|
||||
def build_policy_sandbox_agent(*, skills_root: Path) -> SandboxAgent[HealthcareSupportContext]:
|
||||
return SandboxAgent[HealthcareSupportContext](
|
||||
name="HealthcarePolicySandboxAgent",
|
||||
model="gpt-5.6-sol",
|
||||
instructions=(
|
||||
POLICY_SANDBOX_PROMPT + "\n\n"
|
||||
"Use `load_skill` before reading the skill file. Use `exec_command` with `pwd`, "
|
||||
"`ls`, `cat`, and `rg` to inspect the sandbox workspace. Use `apply_patch` to create "
|
||||
"`output/policy_findings.md` and `output/human_review_checklist.md`."
|
||||
),
|
||||
capabilities=[
|
||||
Shell(),
|
||||
Filesystem(),
|
||||
Skills(
|
||||
lazy_from=LocalDirLazySkillSource(
|
||||
# This is a host path read by the SDK process.
|
||||
# Requested skills are copied into `skills_path` in the sandbox.
|
||||
source=LocalDir(src=skills_root),
|
||||
)
|
||||
),
|
||||
],
|
||||
model_settings=ModelSettings(
|
||||
reasoning=Reasoning(effort="low"),
|
||||
verbosity="low",
|
||||
tool_choice="required",
|
||||
),
|
||||
output_type=AgentOutputSchema(SandboxPolicyPacket, strict_json_schema=False),
|
||||
)
|
||||
|
||||
|
||||
def build_orchestrator(*, sandbox_policy_tool: Tool) -> Agent[HealthcareSupportContext]:
|
||||
return Agent[HealthcareSupportContext](
|
||||
name="HealthcareSupportOrchestrator",
|
||||
model="gpt-5.6-sol",
|
||||
instructions=ORCHESTRATOR_PROMPT,
|
||||
model_settings=ModelSettings(
|
||||
reasoning=Reasoning(effort="low"),
|
||||
verbosity="low",
|
||||
),
|
||||
tools=[
|
||||
benefits_agent.as_tool(
|
||||
tool_name="benefits_review",
|
||||
tool_description="Review patient eligibility, benefits, and referral status.",
|
||||
),
|
||||
sandbox_policy_tool,
|
||||
route_to_human_queue,
|
||||
],
|
||||
output_type=AgentOutputSchema(CaseResolution, strict_json_schema=False),
|
||||
)
|
||||
|
||||
|
||||
memory_recap_agent = Agent[HealthcareSupportContext](
|
||||
name="HealthcareSupportMemoryAgent",
|
||||
model="gpt-5.6-sol",
|
||||
instructions=MEMORY_PROMPT,
|
||||
model_settings=ModelSettings(reasoning=Reasoning(effort="low"), verbosity="low"),
|
||||
output_type=AgentOutputSchema(MemoryRecap, strict_json_schema=False),
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from agents import RunContextWrapper, function_tool
|
||||
from examples.sandbox.healthcare_support.data import HealthcareSupportDataStore
|
||||
from examples.sandbox.healthcare_support.models import ScenarioCase
|
||||
|
||||
|
||||
@dataclass
|
||||
class HealthcareSupportContext:
|
||||
store: HealthcareSupportDataStore
|
||||
scenario: ScenarioCase
|
||||
session_id: str = ""
|
||||
human_handoffs: list[dict[str, Any]] = field(default_factory=list)
|
||||
human_handoff_approved: bool = False
|
||||
emit_event: Callable[[dict[str, Any]], Awaitable[None]] | None = None
|
||||
|
||||
async def emit(self, event_name: str, **payload: Any) -> None:
|
||||
if self.emit_event is None:
|
||||
return
|
||||
await self.emit_event(
|
||||
{
|
||||
"type": "workflow_event",
|
||||
"event": event_name,
|
||||
**payload,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@function_tool(name_override="patient_info_lookup")
|
||||
def lookup_patient(
|
||||
context: RunContextWrapper[HealthcareSupportContext],
|
||||
patient_id: str | None = None,
|
||||
phone: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Look up a synthetic patient profile by patient ID, phone, or name."""
|
||||
return context.context.store.lookup_patient(
|
||||
patient_id=patient_id,
|
||||
phone=phone,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
@function_tool(name_override="insurance_eligibility_lookup")
|
||||
def lookup_insurance_eligibility(
|
||||
context: RunContextWrapper[HealthcareSupportContext],
|
||||
payer: str | None = None,
|
||||
member_id: str | None = None,
|
||||
dob: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Look up synthetic insurance eligibility by payer, member ID, and DOB."""
|
||||
return context.context.store.lookup_eligibility(
|
||||
payer=payer,
|
||||
member_id=member_id,
|
||||
dob=dob,
|
||||
)
|
||||
|
||||
|
||||
@function_tool(name_override="appointment_referral_status_lookup")
|
||||
def lookup_referral_status(
|
||||
context: RunContextWrapper[HealthcareSupportContext],
|
||||
referral_id: str | None = None,
|
||||
patient_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Look up synthetic referral status by referral ID or patient ID."""
|
||||
return context.context.store.lookup_referral(
|
||||
referral_id=referral_id,
|
||||
patient_id=patient_id,
|
||||
)
|
||||
|
||||
|
||||
async def _needs_human_approval(
|
||||
context: RunContextWrapper[HealthcareSupportContext],
|
||||
_params: dict[str, Any],
|
||||
_call_id: str,
|
||||
) -> bool:
|
||||
return not context.context.human_handoff_approved
|
||||
|
||||
|
||||
@function_tool(name_override="route_to_human_queue", needs_approval=_needs_human_approval)
|
||||
def route_to_human_queue(
|
||||
context: RunContextWrapper[HealthcareSupportContext],
|
||||
queue: str,
|
||||
priority: str,
|
||||
reason: str,
|
||||
summary: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Route a synthetic case to a human queue after explicit approval."""
|
||||
payload = {
|
||||
"queue": queue,
|
||||
"priority": priority,
|
||||
"reason": reason,
|
||||
"summary": summary,
|
||||
"scenario_id": context.context.scenario.scenario_id,
|
||||
}
|
||||
digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()[:12]
|
||||
result = {
|
||||
"status": "queued",
|
||||
"handoff_id": f"HUMAN-{digest.upper()}",
|
||||
"queue": queue,
|
||||
"priority": priority,
|
||||
"reason": reason,
|
||||
"summary": summary,
|
||||
}
|
||||
context.context.human_handoffs.append({"payload": payload, "result": result})
|
||||
return result
|
||||
@@ -0,0 +1,419 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user