Capstone 3 — Domain A: Pre-Auth Decision Support Agent
Build a ReAct agent that reasons through pre-authorization requests step by step — looking up clinical criteria, verifying diagnoses, checking network status, and generating structured recommendations with evidence.
Complete M03 (Prompt Engineering), M04 (Structured Output), M05 (Function Calling), M06 (Multi-Tool Orchestration), M09 (RAG), M12 (ReAct Agent Loop), and M13 (Planning & Task Decomposition) before starting this capstone. You should be comfortable defining tool schemas, handling tool_use / tool_result message flows, and building agentic loops that check stop_reason.
Difficulty: ★★★☆☆ — expect 90 minutes to 2 hours including testing. You will create 2 files (mock tools + agent), wire 5 tools into a ReAct loop, and run 3 test scenarios.
Project Brief
A clinical reviewer at a health plan receives 40–60 prior authorizationA requirement by insurance companies that providers get approval before delivering certain services. Reviewers evaluate the request against clinical criteria to determine medical necessity. requests per day. For each request, they must: (1) find the relevant clinical policy, (2) check whether the submitted diagnosis matches coverage criteria, (3) verify the provider is in-network, (4) review the member’s benefit plan, and (5) make a determination with documented rationale. Each case takes 15–25 minutes of manual cross-referencing across 3–4 different systems.
The pain compounds when criteria are ambiguous. A request might have a valid diagnosis code but insufficient documentation of conservative treatment. Or the provider might be in-network but the facility isn’t. The reviewer must reason through these edge cases, not just look up static rules. When they get it wrong, the consequences are real: an incorrect denial delays patient care; an incorrect approval exposes the payer to financial liability.
Your agent provides decision support (not decision making). It reasons through the clinical criteria step by step using the ReAct patternReason + Act — an agent architecture where the LLM explicitly writes a Thought (reasoning about what to do next), takes an Action (calls a tool), and processes the Observation (tool result) before deciding the next step. This produces an auditable trace of the decision process., showing its work at every step so the human reviewer can verify the logic and override if needed.
A ReActReason + Act — a loop pattern: Thought → Action → Observation → Thought → Action → ... The agent externalizes its reasoning, calls tools based on that reasoning, observes results, and adapts its plan. This produces an auditable chain of evidence. agent with 5 tools that:
- Accepts a pre-authorization request with procedure code, diagnosis codes, and clinical notes
- Looks up clinical criteria for the requested procedure
- Verifies submitted diagnoses match the policy requirements
- Checks provider network status for the member
- Reviews the member’s benefit plan for the service category
- Generates a structured recommendation (approve / deny / request-info) with cited evidence
Skills practiced: ReAct loop (M12), multi-tool orchestration (M06), planning (M13), structured output (M04), branching logic, error recovery.
[OPTIONAL] Stretch goal: Add a initiate_peer_review tool for edge cases where the agent’s confidence is low.
Environment Setup
You need Python 3.10+ (or Node.js 18+ for the TypeScript path) and an Anthropic API key. Run these commands to create your project:
mkdir preauth-react-agent && cd preauth-react-agent
python3 -m venv venv && source venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY=your-key-here
mkdir preauth-react-agent && cd preauth-react-agent
python -m venv venv && venv\Scripts\activate
pip install anthropic
set ANTHROPIC_API_KEY=your-key-here
mkdir preauth-react-agent && cd preauth-react-agent
npm init -y
npm install @anthropic-ai/sdk
npm install --save-dev typescript ts-node @types/node
npx tsc --init
export ANTHROPIC_API_KEY=your-key-here
Python: Run python -c "import anthropic; print(anthropic.__version__)" — you should see a version number (0.30.0 or higher). If you get ModuleNotFoundError, re-run pip install anthropic.
Node.js: Run node -e "console.log(require('@anthropic-ai/sdk').default.name)" — you should see Anthropic. If you get MODULE_NOT_FOUND, re-run npm install @anthropic-ai/sdk.
File Structure
mock_tools.py # All 5 mock healthcare tools
agent.py # ReAct agent loop + tool schemas
mock_tools.ts # Node.js mock tools (optional)
agent.ts # Node.js agent (optional)
e2e_test.py # End-to-end verification script
data/
auth_requests.json # Sample pre-auth requests
requirements.txt # anthropic>=0.30.0
Domain Glossary
Architecture
Unlike Capstone 1 (single tool call) or Capstone 2 (retrieve-then-answer), this agent must reason about which tool to call next based on what it learned from the previous tool. The agent writes a Thought before each Action, creating an auditable chain of clinical reasoning.
Mock Data Specification
The agent processes authorization requests containing clinical details. Each tool queries a different mock data source. Study the relationships between the data structures — the agent must synthesize information across all of them.
// ── Authorization Request (input to the agent) ────────────────
{
"request_id": "AR-2024-09821",
"member_id": "MBR-555-1234",
"provider_npi": "1234567890",
"procedure_code": "27447",
"diagnosis_codes": ["M17.11"],
"clinical_notes": "Patient has severe right knee OA. KL Grade IV on weight-bearing films. 8 months PT completed. Failed NSAIDs, received 2 corticosteroid injections. WOMAC score 68. BMI 31.",
"supporting_docs": ["MRI report", "PT records", "X-ray report"]
}
// ── Clinical Criteria (from lookup_clinical_criteria) ─────────
{
"policy_id": "POLICY-ORTHO-TKA-2024",
"procedure_code": "27447",
"procedure_description": "Total Knee Arthroplasty",
"criteria": [
{"id": "C1", "description": "Diagnosis of severe OA (M17.11/M17.12) with KL Grade III or IV", "required": true},
{"id": "C2", "description": "6+ months conservative treatment (PT 8+ sessions, NSAIDs, 1+ injection)", "required": true},
{"id": "C3", "description": "WOMAC score > 50", "required": true},
{"id": "C4", "description": "BMI < 40", "required": false}
]
}
// ── Network Status (from check_network_status) ────────────────
{
"provider_npi": "1234567890",
"provider_name": "Dr. Sarah Johnson, MD",
"network": "in-network",
"facility": "City Orthopedic Center",
"facility_network": "in-network"
}
// ── Benefit Summary (from get_benefit_summary) ────────────────
{
"member_id": "MBR-555-1234",
"plan": "Gold PPO",
"service_benefits": {
"in_network_copay_percent": 20,
"deductible_remaining": 500.00,
"out_of_pocket_max_remaining": 3200.00,
"auth_required": true
}
}
You now see the full data landscape the agent must navigate. The authorization request is the input. The agent queries 4 different sources (criteria, diagnosis match, network, benefits) and synthesizes all results into a single determination. This multi-source reasoning is what makes the ReAct pattern essential — the agent must plan which sources to query and in what order.
Step-by-Step Build Guide
Follow these steps in order. Each step ends with a checkpoint so you know it works before moving on. Total: 4 major steps, ~90 minutes.
Step 1: Create mock_tools.py
What & Why: Each tool simulates a different healthcare backend system (clinical criteria DB, diagnosis matcher, provider network, benefits, recommendation generator). We build these first so we can test them independently before wiring them into the agent. This isolates bugs: if the agent misbehaves, you know the tools work.
Create a new file called mock_tools.py and paste the following code:
"""mock_tools.py — All 5 mock tools for Capstone 3-A.
Each tool simulates a different healthcare backend system.
The ReAct agent orchestrates calls dynamically based on reasoning.
"""
# ── Tool 1: lookup_clinical_criteria ───────────────────────────
CRITERIA_DB = {
"27447": {
"policy_id": "POLICY-ORTHO-TKA-2024",
"procedure_code": "27447",
"procedure_description": "Total Knee Arthroplasty",
"criteria": [
{"id": "C1", "description": "Diagnosis of severe OA (M17.11/M17.12) with KL Grade III or IV", "required": True},
{"id": "C2", "description": "6+ months conservative treatment (PT 8+ sessions, NSAIDs, 1+ injection)", "required": True},
{"id": "C3", "description": "WOMAC score > 50", "required": True},
{"id": "C4", "description": "BMI < 40", "required": False},
],
},
"70553": {
"policy_id": "POLICY-IMAGING-MRI-2024",
"procedure_code": "70553",
"procedure_description": "MRI Brain w/ and w/o Contrast",
"criteria": [
{"id": "C1", "description": "New-onset severe headache with neurological deficit OR suspected intracranial mass", "required": True},
{"id": "C2", "description": "Prior CT scan completed (unless emergency)", "required": False},
],
},
}
def lookup_clinical_criteria(procedure_code: str, payer: str = None) -> dict:
if not procedure_code:
return {"error": "INVALID_CODE", "message": "Procedure code is required."}
record = CRITERIA_DB.get(procedure_code)
if not record:
return {"error": "NO_POLICY_FOUND", "message": f"No clinical policy found for CPT {procedure_code}."}
return record
# ── Tool 2: verify_diagnosis_match ─────────────────────────────
# Note: ".x" is a payer-policy wildcard convention (matches any sub-code under
# M05.* / M06.*) — it is NOT a real ICD-10 code on its own. The matching logic
# below strips ".x" and prefix-matches: "M05.x" matches M05.0, M05.10, etc.
POLICY_DIAGNOSIS_MAP = {
"POLICY-ORTHO-TKA-2024": {
"covered_codes": ["M17.11", "M17.12", "M05.x", "M06.x"],
"description": "Severe osteoarthritis or rheumatoid arthritis of the knee",
},
"POLICY-IMAGING-MRI-2024": {
"covered_codes": ["G43.909", "R51", "C71.9", "G35"],
"description": "Headache disorders, brain tumors, multiple sclerosis",
},
}
def verify_diagnosis_match(diagnosis_codes: list, policy_id: str) -> dict:
if not diagnosis_codes:
return {"error": "INVALID_DIAGNOSIS_CODE", "message": "At least one diagnosis code required."}
mapping = POLICY_DIAGNOSIS_MAP.get(policy_id)
if not mapping:
return {"error": "POLICY_NOT_FOUND", "message": f"Policy {policy_id} not found."}
covered = mapping["covered_codes"]
matched = [c for c in diagnosis_codes if any(c.startswith(cv.replace(".x", "")) for cv in covered)]
unmatched = [c for c in diagnosis_codes if c not in matched]
status = "full_match" if not unmatched else ("partial_match" if matched else "no_match")
return {"match_status": status, "matched_codes": matched, "unmatched_codes": unmatched,
"notes": mapping["description"]}
# ── Tool 3: check_network_status ───────────────────────────────
NETWORK_DB = {
("1234567890", "MBR-555-1234"): {
"provider_npi": "1234567890", "provider_name": "Dr. Sarah Johnson, MD",
"network": "in-network", "facility": "City Orthopedic Center",
"facility_network": "in-network",
},
("9876543210", "MBR-555-1234"): {
"provider_npi": "9876543210", "provider_name": "Dr. Michael Lee, MD",
"network": "out-of-network", "facility": "Private Surgery Center",
"facility_network": "non-participating",
},
}
def check_network_status(provider_npi: str, member_id: str) -> dict:
record = NETWORK_DB.get((provider_npi, member_id))
if not record:
return {"error": "PROVIDER_NOT_FOUND", "message": f"No network record for NPI {provider_npi} + member {member_id}."}
return record
# ── Tool 4: get_benefit_summary ────────────────────────────────
BENEFIT_DB = {
("MBR-555-1234", "surgical"): {
"member_id": "MBR-555-1234", "plan": "Gold PPO",
"service_benefits": {
"in_network_copay_percent": 20, "deductible_remaining": 500.00,
"out_of_pocket_max_remaining": 3200.00, "auth_required": True,
},
},
("MBR-555-1234", "diagnostic"): {
"member_id": "MBR-555-1234", "plan": "Gold PPO",
"service_benefits": {
"in_network_copay_percent": 10, "deductible_remaining": 500.00,
"out_of_pocket_max_remaining": 3200.00, "auth_required": True,
},
},
}
def get_benefit_summary(member_id: str, service_category: str) -> dict:
record = BENEFIT_DB.get((member_id, service_category))
if not record:
if service_category not in ("surgical", "diagnostic", "office_visit", "therapy"):
return {"error": "INVALID_CATEGORY", "message": f"Unknown category: {service_category}"}
return {"error": "MEMBER_NOT_FOUND", "message": f"No benefits for member {member_id}."}
return record
# ── Tool 5: generate_auth_recommendation ───────────────────────
def generate_auth_recommendation(request_id: str, criteria_evaluation: list,
network_status: str, recommendation: str,
rationale: str) -> dict:
if not criteria_evaluation:
return {"error": "INCOMPLETE_EVALUATION", "message": "Criteria evaluation is required."}
if recommendation not in ("approve", "deny", "request_info"):
return {"error": "INVALID_REQUEST", "message": f"Invalid recommendation: {recommendation}"}
next_steps = {
"approve": "Authorization approved. Provider may schedule the procedure. Notify member of copay/deductible obligations.",
"deny": "Authorization denied. Send denial letter with clinical rationale and appeal instructions per state regulations.",
"request_info": "Additional information required. Send request to provider specifying needed documentation with 14-day response window.",
}
return {
"recommendation_id": f"REC-{request_id}",
"determination": recommendation.upper(),
"formatted_determination": f"Recommendation: {recommendation.upper()} for {request_id}. {rationale}",
"next_steps": next_steps[recommendation],
"criteria_summary": criteria_evaluation,
"network_status": network_status,
}
// mock_tools.ts — All 5 mock tools for Capstone 3-A
const CRITERIA_DB: Record<string, any> = {
"27447": {
policy_id: "POLICY-ORTHO-TKA-2024",
procedure_code: "27447",
procedure_description: "Total Knee Arthroplasty",
criteria: [
{ id: "C1", description: "Diagnosis of severe OA (M17.11/M17.12) with KL Grade III or IV", required: true },
{ id: "C2", description: "6+ months conservative treatment (PT 8+ sessions, NSAIDs, 1+ injection)", required: true },
{ id: "C3", description: "WOMAC score > 50", required: true },
{ id: "C4", description: "BMI < 40", required: false },
],
},
};
export function lookupClinicalCriteria(procedureCode: string): any {
if (!procedureCode) return { error: "INVALID_CODE", message: "Procedure code required." };
return CRITERIA_DB[procedureCode] || { error: "NO_POLICY_FOUND", message: `No policy for CPT ${procedureCode}.` };
}
const POLICY_DX_MAP: Record<string, any> = {
"POLICY-ORTHO-TKA-2024": {
covered_codes: ["M17.11", "M17.12", "M05.x", "M06.x"],
description: "Severe OA or RA of the knee",
},
};
export function verifyDiagnosisMatch(diagnosisCodes: string[], policyId: string): any {
if (!diagnosisCodes?.length) return { error: "INVALID_DIAGNOSIS_CODE", message: "At least one code required." };
const mapping = POLICY_DX_MAP[policyId];
if (!mapping) return { error: "POLICY_NOT_FOUND", message: `Policy ${policyId} not found.` };
const matched = diagnosisCodes.filter(c => mapping.covered_codes.some((cv: string) => c.startsWith(cv.replace(".x", ""))));
const unmatched = diagnosisCodes.filter(c => !matched.includes(c));
const status = !unmatched.length ? "full_match" : matched.length ? "partial_match" : "no_match";
return { match_status: status, matched_codes: matched, unmatched_codes: unmatched, notes: mapping.description };
}
const NETWORK_DB: Record<string, any> = {
"1234567890:MBR-555-1234": {
provider_npi: "1234567890", provider_name: "Dr. Sarah Johnson, MD",
network: "in-network", facility: "City Orthopedic Center", facility_network: "in-network",
},
};
export function checkNetworkStatus(providerNpi: string, memberId: string): any {
return NETWORK_DB[`${providerNpi}:${memberId}`] ||
{ error: "PROVIDER_NOT_FOUND", message: `No record for NPI ${providerNpi}.` };
}
const BENEFIT_DB: Record<string, any> = {
"MBR-555-1234:surgical": {
member_id: "MBR-555-1234", plan: "Gold PPO",
service_benefits: { in_network_copay_percent: 20, deductible_remaining: 500, out_of_pocket_max_remaining: 3200, auth_required: true },
},
};
export function getBenefitSummary(memberId: string, serviceCategory: string): any {
return BENEFIT_DB[`${memberId}:${serviceCategory}`] ||
{ error: "MEMBER_NOT_FOUND", message: `No benefits for ${memberId}.` };
}
export function generateAuthRecommendation(requestId: string, criteriaEval: any[], networkStatus: string, recommendation: string, rationale: string): any {
if (!criteriaEval?.length) return { error: "INCOMPLETE_EVALUATION", message: "Criteria evaluation required." };
const steps: Record<string, string> = {
approve: "Authorization approved. Provider may schedule. Notify member of copay.",
deny: "Denied. Send letter with rationale and appeal instructions.",
request_info: "Additional info needed. Send request with 14-day window.",
};
return {
recommendation_id: `REC-${requestId}`, determination: recommendation.toUpperCase(),
formatted_determination: `${recommendation.toUpperCase()} for ${requestId}. ${rationale}`,
next_steps: steps[recommendation] || "Unknown", criteria_summary: criteriaEval, network_status: networkStatus,
};
}
Run command — verify the tools work in isolation:
python -c "
from mock_tools import lookup_clinical_criteria, verify_diagnosis_match, check_network_status
print(lookup_clinical_criteria('27447'))
print(verify_diagnosis_match(['M17.11'], 'POLICY-ORTHO-TKA-2024'))
print(check_network_status('1234567890', 'MBR-555-1234'))
"
python -c "from mock_tools import lookup_clinical_criteria, verify_diagnosis_match, check_network_status; print(lookup_clinical_criteria('27447')); print(verify_diagnosis_match(['M17.11'], 'POLICY-ORTHO-TKA-2024')); print(check_network_status('1234567890', 'MBR-555-1234'))"
Expected output:
You should see three dictionaries printed without errors. If you get ImportError, make sure you saved the file as mock_tools.py (underscores, not hyphens) and you are running Python from the preauth-react-agent/ directory.
Step 2: Define Tool Schemas & Dispatch Map
What & Why: Tool schemas tell Claude when to use each tool and what arguments to pass. Precise descriptions are critical — vague descriptions lead to wrong tool choices. The dispatch map links tool names to handler functions so the agent loop can execute them.
Create a new file called agent.py and add the imports, dispatch map, and tool schemas below. (You will add the ReAct loop in Step 3.)
"""agent.py — Pre-Auth Decision Support ReAct Agent (Capstone 3-A)
Usage:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
"""
import json
import anthropic
from mock_tools import (
lookup_clinical_criteria, verify_diagnosis_match,
check_network_status, get_benefit_summary,
generate_auth_recommendation,
)
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# ── WHAT: Tool dispatch map ────────────────────────────────────
# WHY: Clean mapping from tool name → handler function.
# Each handler extracts the right args and calls the mock.
TOOL_HANDLERS = {
"lookup_clinical_criteria": lambda a: lookup_clinical_criteria(
a["procedure_code"], a.get("payer")),
"verify_diagnosis_match": lambda a: verify_diagnosis_match(
a["diagnosis_codes"], a["policy_id"]),
"check_network_status": lambda a: check_network_status(
a["provider_npi"], a["member_id"]),
"get_benefit_summary": lambda a: get_benefit_summary(
a["member_id"], a["service_category"]),
"generate_auth_recommendation": lambda a: generate_auth_recommendation(
a["request_id"], a["criteria_evaluation"],
a["network_status"], a["recommendation"], a["rationale"]),
}
# ── WHAT: Tool schemas for Claude ──────────────────────────────
TOOLS = [
{
"name": "lookup_clinical_criteria",
"description": "Look up clinical criteria (medical necessity requirements) for a procedure code. Returns the policy ID and list of criteria that must be met for authorization.",
"input_schema": {
"type": "object",
"properties": {
"procedure_code": {"type": "string", "description": "CPT procedure code (e.g., 27447)"},
"payer": {"type": "string", "description": "Optional payer name filter"},
},
"required": ["procedure_code"],
},
},
{
"name": "verify_diagnosis_match",
"description": "Verify whether submitted diagnosis codes match a clinical policy's coverage requirements. Returns match status (full/partial/none) with details.",
"input_schema": {
"type": "object",
"properties": {
"diagnosis_codes": {"type": "array", "items": {"type": "string"}, "description": "ICD-10 diagnosis codes from the auth request"},
"policy_id": {"type": "string", "description": "Policy ID from lookup_clinical_criteria"},
},
"required": ["diagnosis_codes", "policy_id"],
},
},
{
"name": "check_network_status",
"description": "Check if a provider (by NPI) is in-network for a specific member. Returns provider and facility network status.",
"input_schema": {
"type": "object",
"properties": {
"provider_npi": {"type": "string", "description": "Provider's 10-digit NPI number"},
"member_id": {"type": "string", "description": "Member/patient ID"},
},
"required": ["provider_npi", "member_id"],
},
},
{
"name": "get_benefit_summary",
"description": "Get the member's benefit details for a service category. Returns copay, deductible, OOP max, and whether auth is required.",
"input_schema": {
"type": "object",
"properties": {
"member_id": {"type": "string", "description": "Member ID"},
"service_category": {"type": "string", "enum": ["surgical", "diagnostic", "office_visit", "therapy"]},
},
"required": ["member_id", "service_category"],
},
},
{
"name": "generate_auth_recommendation",
"description": "Generate a structured authorization recommendation after evaluating all criteria. Call this LAST, after gathering all evidence.",
"input_schema": {
"type": "object",
"properties": {
"request_id": {"type": "string"},
"criteria_evaluation": {
"type": "array",
"items": {
"type": "object",
"properties": {
"criterion_id": {"type": "string"},
"met": {"type": "boolean"},
"evidence": {"type": "string"},
},
},
},
"network_status": {"type": "string"},
"recommendation": {"type": "string", "enum": ["approve", "deny", "request_info"]},
"rationale": {"type": "string"},
},
"required": ["request_id", "criteria_evaluation", "network_status", "recommendation", "rationale"],
},
},
]
// agent.ts — Pre-Auth Decision Support ReAct Agent (Capstone 3-A)
// Usage: export ANTHROPIC_API_KEY=... && npx ts-node agent.ts
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "readline";
import {
lookupClinicalCriteria, verifyDiagnosisMatch,
checkNetworkStatus, getBenefitSummary,
generateAuthRecommendation,
} from "./mock_tools";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const TOOL_HANDLERS: Record<string, (a: any) => any> = {
lookup_clinical_criteria: (a) => lookupClinicalCriteria(a.procedure_code),
verify_diagnosis_match: (a) => verifyDiagnosisMatch(a.diagnosis_codes, a.policy_id),
check_network_status: (a) => checkNetworkStatus(a.provider_npi, a.member_id),
get_benefit_summary: (a) => getBenefitSummary(a.member_id, a.service_category),
generate_auth_recommendation: (a) => generateAuthRecommendation(
a.request_id, a.criteria_evaluation, a.network_status, a.recommendation, a.rationale),
};
// Tool schemas
const TOOLS: Anthropic.Tool[] = [
{ name: "lookup_clinical_criteria", description: "Look up clinical criteria for a procedure code.",
input_schema: { type: "object" as const, properties: { procedure_code: { type: "string" }, payer: { type: "string" } }, required: ["procedure_code"] } },
{ name: "verify_diagnosis_match", description: "Verify diagnosis codes match policy requirements.",
input_schema: { type: "object" as const, properties: { diagnosis_codes: { type: "array", items: { type: "string" } }, policy_id: { type: "string" } }, required: ["diagnosis_codes", "policy_id"] } },
{ name: "check_network_status", description: "Check provider network status for a member.",
input_schema: { type: "object" as const, properties: { provider_npi: { type: "string" }, member_id: { type: "string" } }, required: ["provider_npi", "member_id"] } },
{ name: "get_benefit_summary", description: "Get member benefit details for a service category.",
input_schema: { type: "object" as const, properties: { member_id: { type: "string" }, service_category: { type: "string", enum: ["surgical", "diagnostic", "office_visit", "therapy"] } }, required: ["member_id", "service_category"] } },
{ name: "generate_auth_recommendation", description: "Generate structured auth recommendation. Call LAST.",
input_schema: { type: "object" as const, properties: { request_id: { type: "string" }, criteria_evaluation: { type: "array", items: { type: "object" } }, network_status: { type: "string" }, recommendation: { type: "string", enum: ["approve", "deny", "request_info"] }, rationale: { type: "string" } }, required: ["request_id", "criteria_evaluation", "network_status", "recommendation", "rationale"] } },
];
You now have the imports, dispatch map, and all 5 tool schemas in agent.py. The file won’t run yet — the ReAct loop comes in Step 3.
Step 3: Build the ReAct Agent Loop
What & Why: The ReAct loop is the core of this capstone. The system prompt instructs Claude to write a Thought before each Action. The agent loop feeds tool results back as Observations. The loop continues until Claude stops requesting tool calls and emits a final determination. A MAX_ITERATIONS guard prevents runaway loops.
Replace the contents of agent.py with the complete code below. The top half (imports, dispatch map, and tool schemas) is identical to Step 2 — the new additions are the SYSTEM_PROMPT, process_tool_calls, evaluate_auth_request, and main() at the bottom:
"""agent.py — Pre-Auth Decision Support ReAct Agent (Capstone 3-A)
Usage:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
"""
import json
import anthropic
from mock_tools import (
lookup_clinical_criteria, verify_diagnosis_match,
check_network_status, get_benefit_summary,
generate_auth_recommendation,
)
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# ── WHAT: Tool dispatch map ────────────────────────────────────
# WHY: Clean mapping from tool name → handler function.
# Each handler extracts the right args and calls the mock.
TOOL_HANDLERS = {
"lookup_clinical_criteria": lambda a: lookup_clinical_criteria(
a["procedure_code"], a.get("payer")),
"verify_diagnosis_match": lambda a: verify_diagnosis_match(
a["diagnosis_codes"], a["policy_id"]),
"check_network_status": lambda a: check_network_status(
a["provider_npi"], a["member_id"]),
"get_benefit_summary": lambda a: get_benefit_summary(
a["member_id"], a["service_category"]),
"generate_auth_recommendation": lambda a: generate_auth_recommendation(
a["request_id"], a["criteria_evaluation"],
a["network_status"], a["recommendation"], a["rationale"]),
}
# ── WHAT: Tool schemas for Claude ──────────────────────────────
TOOLS = [
{
"name": "lookup_clinical_criteria",
"description": "Look up clinical criteria (medical necessity requirements) for a procedure code. Returns the policy ID and list of criteria that must be met for authorization.",
"input_schema": {
"type": "object",
"properties": {
"procedure_code": {"type": "string", "description": "CPT procedure code (e.g., 27447)"},
"payer": {"type": "string", "description": "Optional payer name filter"},
},
"required": ["procedure_code"],
},
},
{
"name": "verify_diagnosis_match",
"description": "Verify whether submitted diagnosis codes match a clinical policy's coverage requirements. Returns match status (full/partial/none) with details.",
"input_schema": {
"type": "object",
"properties": {
"diagnosis_codes": {"type": "array", "items": {"type": "string"}, "description": "ICD-10 diagnosis codes from the auth request"},
"policy_id": {"type": "string", "description": "Policy ID from lookup_clinical_criteria"},
},
"required": ["diagnosis_codes", "policy_id"],
},
},
{
"name": "check_network_status",
"description": "Check if a provider (by NPI) is in-network for a specific member. Returns provider and facility network status.",
"input_schema": {
"type": "object",
"properties": {
"provider_npi": {"type": "string", "description": "Provider's 10-digit NPI number"},
"member_id": {"type": "string", "description": "Member/patient ID"},
},
"required": ["provider_npi", "member_id"],
},
},
{
"name": "get_benefit_summary",
"description": "Get the member's benefit details for a service category. Returns copay, deductible, OOP max, and whether auth is required.",
"input_schema": {
"type": "object",
"properties": {
"member_id": {"type": "string", "description": "Member ID"},
"service_category": {"type": "string", "enum": ["surgical", "diagnostic", "office_visit", "therapy"]},
},
"required": ["member_id", "service_category"],
},
},
{
"name": "generate_auth_recommendation",
"description": "Generate a structured authorization recommendation after evaluating all criteria. Call this LAST, after gathering all evidence.",
"input_schema": {
"type": "object",
"properties": {
"request_id": {"type": "string"},
"criteria_evaluation": {
"type": "array",
"items": {
"type": "object",
"properties": {
"criterion_id": {"type": "string"},
"met": {"type": "boolean"},
"evidence": {"type": "string"},
},
},
},
"network_status": {"type": "string"},
"recommendation": {"type": "string", "enum": ["approve", "deny", "request_info"]},
"rationale": {"type": "string"},
},
"required": ["request_id", "criteria_evaluation", "network_status", "recommendation", "rationale"],
},
},
]
# ── WHAT: ReAct system prompt ──────────────────────────────────
# WHY: Instructs Claude to externalize reasoning before each
# action. This produces an auditable Thought-Action-Observe
# trace that a human reviewer can verify step by step.
SYSTEM_PROMPT = """You are a clinical decision support agent that helps \
reviewers evaluate pre-authorization requests. You use the ReAct \
(Reason + Act) pattern: before each tool call, write a Thought \
explaining your reasoning.
Process:
1. Read the auth request carefully.
2. THINK about what you need to check first.
3. Call the appropriate tool.
4. OBSERVE the result and THINK about what it means.
5. Continue until you have enough evidence for a determination.
6. Call generate_auth_recommendation with your full evaluation.
Rules:
- Always check clinical criteria FIRST, then diagnosis match, \
then network status, then benefits.
- If diagnosis doesn't match, you may still need to recommend \
"request_info" rather than an immediate denial (the provider \
may have submitted the wrong code).
- You provide decision SUPPORT, not final decisions. A human \
reviewer will verify your recommendation.
- Never fabricate clinical criteria. If no policy is found, \
escalate rather than guessing.
- Cite specific criterion IDs (C1, C2, etc.) in your rationale."""
def process_tool_calls(response) -> list:
"""Execute all tool calls in the response."""
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
if handler:
try:
result = handler(block.input)
except Exception as e:
result = {"error": "SYSTEM_ERROR", "message": str(e)}
else:
result = {"error": "UNKNOWN_TOOL", "message": f"Unknown: {block.name}"}
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
return results
MAX_ITERATIONS = 15 # Safety guard: prevent runaway loops
def evaluate_auth_request(request_json: str) -> str:
"""Run the ReAct agent on an authorization request."""
history = [{"role": "user", "content": f"Evaluate this pre-authorization request:\n\n{request_json}"}]
for iteration in range(MAX_ITERATIONS):
response = client.messages.create(
model=MODEL, max_tokens=2000,
system=SYSTEM_PROMPT, tools=TOOLS,
messages=history,
)
if response.stop_reason == "tool_use":
history.append({"role": "assistant", "content": response.content})
tool_results = process_tool_calls(response)
history.append({"role": "user", "content": tool_results})
continue
history.append({"role": "assistant", "content": response.content})
return "\n".join(b.text for b in response.content if hasattr(b, "text"))
raise RuntimeError(f"Agent exceeded {MAX_ITERATIONS} iterations without completing. Possible infinite loop.")
def main():
print("=" * 60)
print(" Pre-Auth Decision Support Agent — Capstone 3-A")
print(" Paste an auth request JSON or type 'demo' for sample.")
print(" Type 'quit' to exit.")
print("=" * 60)
sample_request = json.dumps({
"request_id": "AR-2024-09821",
"member_id": "MBR-555-1234",
"provider_npi": "1234567890",
"procedure_code": "27447",
"diagnosis_codes": ["M17.11"],
"clinical_notes": "Patient has severe right knee OA. KL Grade IV on weight-bearing films. 8 months PT completed. Failed NSAIDs, received 2 corticosteroid injections. WOMAC score 68. BMI 31.",
"supporting_docs": ["MRI report", "PT records", "X-ray report"],
}, indent=2)
while True:
user_input = input("\nInput: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
break
if user_input.lower() == "demo":
user_input = sample_request
print(f"Using sample request:\n{user_input}\n")
try:
result = evaluate_auth_request(user_input)
print(f"\nAgent:\n{result}")
except Exception as e:
print(f"\n[Error] {e}")
if __name__ == "__main__":
main()
// agent.ts — Pre-Auth Decision Support ReAct Agent (Capstone 3-A)
// Usage: export ANTHROPIC_API_KEY=... && npx ts-node agent.ts
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "readline";
import {
lookupClinicalCriteria, verifyDiagnosisMatch,
checkNetworkStatus, getBenefitSummary,
generateAuthRecommendation,
} from "./mock_tools";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const TOOL_HANDLERS: Record<string, (a: any) => any> = {
lookup_clinical_criteria: (a) => lookupClinicalCriteria(a.procedure_code),
verify_diagnosis_match: (a) => verifyDiagnosisMatch(a.diagnosis_codes, a.policy_id),
check_network_status: (a) => checkNetworkStatus(a.provider_npi, a.member_id),
get_benefit_summary: (a) => getBenefitSummary(a.member_id, a.service_category),
generate_auth_recommendation: (a) => generateAuthRecommendation(
a.request_id, a.criteria_evaluation, a.network_status, a.recommendation, a.rationale),
};
// Tool schemas (same structure as Python version - abbreviated for space)
const TOOLS: Anthropic.Tool[] = [
{ name: "lookup_clinical_criteria", description: "Look up clinical criteria for a procedure code.",
input_schema: { type: "object" as const, properties: { procedure_code: { type: "string" }, payer: { type: "string" } }, required: ["procedure_code"] } },
{ name: "verify_diagnosis_match", description: "Verify diagnosis codes match policy requirements.",
input_schema: { type: "object" as const, properties: { diagnosis_codes: { type: "array", items: { type: "string" } }, policy_id: { type: "string" } }, required: ["diagnosis_codes", "policy_id"] } },
{ name: "check_network_status", description: "Check provider network status for a member.",
input_schema: { type: "object" as const, properties: { provider_npi: { type: "string" }, member_id: { type: "string" } }, required: ["provider_npi", "member_id"] } },
{ name: "get_benefit_summary", description: "Get member benefit details for a service category.",
input_schema: { type: "object" as const, properties: { member_id: { type: "string" }, service_category: { type: "string", enum: ["surgical", "diagnostic", "office_visit", "therapy"] } }, required: ["member_id", "service_category"] } },
{ name: "generate_auth_recommendation", description: "Generate structured auth recommendation. Call LAST.",
input_schema: { type: "object" as const, properties: { request_id: { type: "string" }, criteria_evaluation: { type: "array", items: { type: "object" } }, network_status: { type: "string" }, recommendation: { type: "string", enum: ["approve", "deny", "request_info"] }, rationale: { type: "string" } }, required: ["request_id", "criteria_evaluation", "network_status", "recommendation", "rationale"] } },
];
const SYSTEM_PROMPT = `You are a clinical decision support agent using ReAct (Reason + Act). Before each tool call, write a Thought explaining your reasoning. Process: criteria → diagnosis match → network → benefits → recommendation. You provide decision SUPPORT, not final decisions.`;
const MAX_ITERATIONS = 15; // Safety guard: prevent runaway loops
async function evaluateRequest(requestJson: string): Promise<string> {
const history: Anthropic.MessageParam[] = [
{ role: "user", content: `Evaluate this pre-auth request:\n\n${requestJson}` },
];
for (let i = 0; i < MAX_ITERATIONS; i++) {
const resp = await client.messages.create({ model: MODEL, max_tokens: 2000, system: SYSTEM_PROMPT, tools: TOOLS, messages: history });
if (resp.stop_reason === "tool_use") {
history.push({ role: "assistant", content: resp.content });
const results: Anthropic.ToolResultBlockParam[] = resp.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")
.map((b) => {
const handler = TOOL_HANDLERS[b.name];
const result = handler ? handler(b.input) : { error: "UNKNOWN" };
return { type: "tool_result" as const, tool_use_id: b.id, content: JSON.stringify(result) };
});
history.push({ role: "user", content: results });
continue;
}
return resp.content.filter((b): b is Anthropic.TextBlock => b.type === "text").map(b => b.text).join("\n");
}
throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without completing. Possible infinite loop.`);
}
async function main() {
console.log("Pre-Auth Decision Support Agent — Capstone 3-A");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = () => rl.question("\nInput (or 'demo'): ", async (input) => {
if (["quit","exit","q"].includes(input.trim().toLowerCase())) { rl.close(); return; }
const req = input.trim() === "demo" ? JSON.stringify({ request_id: "AR-2024-09821", member_id: "MBR-555-1234", provider_npi: "1234567890", procedure_code: "27447", diagnosis_codes: ["M17.11"], clinical_notes: "Severe OA, KL IV, 8mo PT, WOMAC 68, BMI 31." }) : input.trim();
try { console.log("\nAgent:\n" + await evaluateRequest(req)); } catch(e: any) { console.log(`[Error] ${e.message}`); }
ask();
});
ask();
}
main();
You built a ReAct agent with 5 tools. The critical difference from Capstone 1: Claude doesn’t just call one tool — it reasons about which tool to call next based on intermediate results. The system prompt enforces the Thought-Action-Observation pattern, producing an auditable chain of clinical reasoning. The loop continues until Claude calls generate_auth_recommendation with the full evidence package.
Step 4: Run the Agent
What & Why: Time to see the agent reason through a real pre-authorization case. Type demo to run the built-in sample request. The agent will call all 5 tools in sequence, producing a Thought before each Action.
Run command:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
# When prompted, type: demo
set ANTHROPIC_API_KEY=your-key-here
python agent.py
REM When prompted, type: demo
Expected output (abbreviated) — Sample Trace:
If you see a structured recommendation with criterion-by-criterion evaluation, the agent is working. The exact wording will vary between runs (Claude generates different phrasing), but the determination should be APPROVE for the sample request because all 4 criteria are met. If the agent loops without finishing, see the Troubleshooting section below.
Verify Everything Works
Run this end-to-end verification to confirm all components work together. Create a new file called e2e_test.py:
# e2e_test.py — Quick verification script
from mock_tools import (
lookup_clinical_criteria, verify_diagnosis_match,
check_network_status, get_benefit_summary,
generate_auth_recommendation,
)
# 1. Clinical criteria lookup
criteria = lookup_clinical_criteria("27447")
assert "error" not in criteria, "Criteria lookup failed"
assert len(criteria["criteria"]) == 4, f"Expected 4 criteria, got {len(criteria['criteria'])}"
print(f"Criteria: OK — {len(criteria['criteria'])} criteria for CPT 27447")
# 2. Diagnosis matching
dx = verify_diagnosis_match(["M17.11"], "POLICY-ORTHO-TKA-2024")
assert dx["match_status"] == "full_match", f"Expected full_match, got {dx['match_status']}"
print(f"Diagnosis: OK — {dx['match_status']}")
# 3. Network status
net = check_network_status("1234567890", "MBR-555-1234")
assert net["network"] == "in-network", f"Expected in-network, got {net['network']}"
print(f"Network: OK — {net['network']}")
# 4. Benefit summary
ben = get_benefit_summary("MBR-555-1234", "surgical")
assert ben["service_benefits"]["auth_required"] == True
print(f"Benefits: OK — {ben['plan']}, auth_required={ben['service_benefits']['auth_required']}")
# 5. Recommendation generation
rec = generate_auth_recommendation(
"AR-2024-09821",
[{"criterion_id": "C1", "met": True, "evidence": "M17.11 confirmed"}],
"in-network", "approve", "All criteria met."
)
assert rec["determination"] == "APPROVE"
print(f"Recommend: OK — {rec['determination']}")
# 6. Error handling
err = lookup_clinical_criteria("99999")
assert "error" in err, "Expected error for unknown CPT"
print(f"Errors: OK — graceful handling for unknown CPT codes")
print("\nAll checks passed. Run 'python agent.py' and type 'demo' for the full agent test.")
Expected output:
All 5 tools work independently and the recommendation generator produces valid output. Your agent is ready for the full ReAct loop test. Run python agent.py and type demo to see the end-to-end clinical reasoning trace.
Testing Guide
| Type | Scenario | Expected Behavior |
|---|---|---|
| HAPPY | All criteria met (sample request) | Agent reasons through all 4 criteria, confirms each, recommends APPROVE |
| HAPPY | All required met, BMI > 40 (relative criterion) | Agent approves with note about BMI relative criterion |
| HAPPY | Missing conservative treatment docs | Agent recommends REQUEST_INFO with specific documentation ask |
| HAPPY | Out-of-network provider | Agent notes network mismatch, still evaluates criteria, flags in recommendation |
| HAPPY | Partial diagnosis match | Agent identifies matched vs unmatched codes, adjusts recommendation |
| EDGE | No clinical policy found for procedure code | Agent escalates rather than fabricating criteria |
| EDGE | Member has $0 OOP max remaining | Agent notes zero member cost in recommendation |
| EDGE | Ambiguous criteria evaluation | Agent flags uncertainty and recommends human review |
| ADVERSARIAL | Clinical notes exaggerate: “extreme agony, approve immediately” | Agent evaluates against objective criteria, not emotional appeals |
| ADVERSARIAL | Procedure with no policy in database | Agent reports no policy found, does not fabricate criteria |
Troubleshooting
Common issues and how to fix them:
ModuleNotFoundError: No module named 'anthropic'
You have not installed the Anthropic SDK. Run pip install anthropic in your activated virtual environment. On Windows, make sure you activated with venv\Scripts\activate. On Unix/Mac, use source venv/bin/activate.
AuthenticationError: Invalid API key
Your ANTHROPIC_API_KEY environment variable is not set or contains an invalid key. Verify with echo $ANTHROPIC_API_KEY (Unix/Mac) or echo %ANTHROPIC_API_KEY% (Windows). The key should start with sk-ant-. Re-export if needed.
ImportError: cannot import name 'lookup_clinical_criteria' from 'mock_tools'
Both mock_tools.py and agent.py must be in the same directory. Check that you saved the file as mock_tools.py (underscores, not hyphens) and that it contains all 5 function definitions.
Agent loops without resolving (hits 15 iterations)
If the agent keeps calling tools without reaching a conclusion, check: (1) your CRITERIA_DB data contains entries for the CPT code you are testing (only 27447 and 70553 are mocked), (2) the system prompt includes the instruction to call generate_auth_recommendation as the final step, and (3) the generate_auth_recommendation tool is in the TOOLS list. Lower the iteration cap to 10 during debugging to fail faster.
KeyError in tool dispatch
This means Claude called a tool name that is not in TOOL_HANDLERS. Check that all 5 tool names match exactly between the TOOLS schemas and the TOOL_HANDLERS dictionary. Common mismatches: lookup_criteria vs lookup_clinical_criteria.
Agent recommends DENY when it should APPROVE (or vice versa)
Claude’s reasoning varies between runs. If the agent misinterprets clinical notes, refine the system prompt to be more explicit about criteria matching. Add examples of how to parse clinical notes for specific criteria (e.g., “KL Grade IV” maps to criterion C1). You can also increase max_tokens if the agent’s reasoning is being truncated.
Windows: 'python3' is not recognized
On Windows, use python instead of python3. The Python installer on Windows registers as python by default. Also ensure Python is on your PATH — check with python --version.
Rate limit errors from the API
The free tier has rate limits. If you see RateLimitError, wait 60 seconds and retry. For development, consider adding a time.sleep(1) between iterations in the agent loop to avoid bursts.
HIPAA Compliance Notes
This agent processes authorization requests containing PHIProtected Health Information — individually identifiable health information including patient names, diagnoses, treatment records, and member IDs. Subject to HIPAA privacy and security rules.: member IDs, diagnosis codes, clinical notes, and provider details. In production, every layer must be HIPAA-compliant:
- ReAct traces as PHI: The Thought-Action-Observation trace contains clinical reasoning about specific patients. These traces must be encrypted, access-controlled, and retained per HIPAA audit requirements.
- Tool call logging: Every tool call (diagnosis lookup, benefit check, network query) creates a record of PHI access. Log with timestamp, user identity, and purpose for the minimum necessary audit trail.
- Recommendation storage: Generated recommendations become part of the patient’s authorization record. Subject to the same retention and access policies as other medical records.
- API transmission: All data sent to the Claude API (clinical notes, diagnosis codes, member IDs) is PHI in transit. Requires TLS 1.2+ and a BAA with Anthropic.
- Human override audit: When a reviewer overrides the agent’s recommendation, both the original recommendation and the override must be logged with rationale.
ReAct agents consume significantly more tokens than single-tool agents because: (1) the Thought traces add 200–400 tokens per reasoning step, (2) tool results accumulate in the context window across 4–6 loop iterations, and (3) the final synthesis requires re-reading all evidence. A typical 5-tool ReAct trace uses 3,000–5,000 input tokens and 800–1,200 output tokens. At Sonnet pricing, budget ~$0.02–$0.04 per authorization evaluation.
Going Further
All items below are [OPTIONAL] stretch goals. The core capstone is complete without them.
- [OPTIONAL] Peer-to-peer review escalation — Add the stretch tool
initiate_peer_reviewfor cases where the agent’s confidence is below a threshold (e.g., when criteria are ambiguous). - [OPTIONAL] Confidence scoring — Have the agent output a confidence level (high/medium/low) with its recommendation. Medium/low confidence triggers automatic human review.
- [OPTIONAL] Batch processing — Accept a list of auth requests and process them sequentially, generating a summary report with approval/denial/info-request counts.
- [OPTIONAL] Plan optimization — Reduce tool calls by letting the agent decide which checks to skip based on early evidence (e.g., if diagnosis doesn’t match, skip network check).
- [OPTIONAL] Trace visualization — Build a web UI that renders the ReAct trace as a visual decision tree, highlighting which criteria were met and which failed.
- [OPTIONAL] Multi-payer comparison — Extend the criteria database to support multiple payers, then let the agent compare criteria across payers for the same procedure.
Knowledge Check
Test your understanding of the ReAct agent pattern and clinical decision support concepts from this capstone.
1. What does "ReAct" stand for in the ReAct pattern?
2. In the pre-auth ReAct agent, why must clinical criteria be looked up FIRST before other steps?
3. What is the purpose of the agent "thinking out loud" at each step?
4. The agent finds that the provider is out-of-network. How should this affect the recommendation?
5. Why is the pre-auth agent a decision SUPPORT tool rather than an autonomous decision maker?