Capstone 1 — Domain A: Pre-Auth Status Checker
Build your first agent: a single-tool conversational assistant that checks healthcare pre-authorization status and responds with clear, actionable next steps.
Project Brief
Every day, healthcare provider offices spend hours on the phone with insurance companies checking the status of prior authorizationA requirement by health insurance companies that providers must get approval before delivering certain services (surgeries, imaging, specialty drugs). The payer reviews clinical criteria to determine medical necessity before authorizing coverage. requests. A front-desk coordinator dials the payer, navigates an IVR phone tree, waits on hold for 15–45 minutes, reads off a reference number, and manually transcribes the response into the patient’s chart. Multiply that by 30–50 auths per day across a busy orthopedic practice, and you have an entire full-time role dedicated to hold music.
The pain is threefold: the process is slow (up to an hour per auth check), error-prone (manual transcription leads to missed deadlines and wrong next-steps), and frustrating for staff who would rather help patients face-to-face. When an auth status changes from “pending” to “info-requested” and nobody notices, the deadline passes, the auth is denied, and the patient’s surgery gets delayed by weeks.
Your agent replaces this phone-based workflow: the user provides a pre-auth reference number, the agent calls a payer status API (mock), and returns a clear, plain-English summary with actionable next steps — “Approved: you may schedule the procedure” or “Info Requested: upload the MRI report by March 15.” One query, immediate answer, zero hold time.
A conversational agent with one tool (get_preauth_status) that:
- Accepts a pre-authorization reference number from the user
- Calls the tool to retrieve mock payer status data
- Summarizes the status in plain English with context-appropriate next steps
- Handles errors gracefully (invalid format, not found, system unavailable)
- Maintains conversation context across multiple turns
Skills practiced: Tool definitions (M05), structured output (M04), conversation management (M08), the agentic tool-use loop, and error handling.
Stretch goal: Add a second tool (lookup_cpt_code) to look up CPT codeCurrent Procedural Terminology — a standardized 5-digit code system maintained by the AMA that describes medical procedures and services. Example: 27447 = Total Knee Arthroplasty. Used universally in billing and pre-authorization. descriptions so the agent can explain what procedure is being authorized.
Prerequisites
This is a ★☆☆☆☆ (1 of 5) capstone — the easiest project in the series. You need working knowledge of three modules:
- M03: Prompt Engineering — system prompts, role-setting, and instruction design. You will write a system prompt that constrains the agent to status-checking only.
- M04: Structured Output — getting Claude to return predictable JSON. The tool returns structured data that the agent interprets for the user.
- M05: Function Calling (Tool Use) — defining tool schemas, handling
tool_useresponses, and sendingtool_resultblocks back. This is the core mechanic of the entire capstone.
If you have not completed M03–M05, go back and finish them first. This capstone directly applies concepts from those modules.
30–45 minutes for the core project (Steps 1–5). The stretch goals and extensions are optional and may add another 30–60 minutes if you choose to tackle them.
You will create 3 files totaling about 250 lines of Python. No external services, databases, or Docker required — everything runs locally with mock data.
Domain Glossary
Architecture
The architecture is deliberately simple: a single user-facing agent with one tool. This lets you focus on mastering the tool-use loopThe fundamental agentic pattern: (1) user sends a message, (2) Claude decides to call a tool, (3) tool returns data, (4) Claude synthesizes a natural language response. This loop is the building block for all more complex agent architectures. before adding complexity in later capstones.
Environment Setup
Copy and paste these commands to set up your project from scratch. Everything runs locally — no Docker, no cloud services, no database.
- Python 3.10+ — check with
python --version. Thematch/casesyntax and modern type hints used in this capstone require 3.10 or later. - Node.js 18+ (optional) — only needed if you follow the Node.js/TypeScript code tabs.
- An Anthropic API key — get one at console.anthropic.com.
Python Setup
# Create the project directory and virtual environment
mkdir preauth-agent && cd preauth-agent
python3 -m venv venv && source venv/bin/activate
# Install dependencies
pip install anthropic pytest
# Set your API key (replace with your real key)
export ANTHROPIC_API_KEY=your-key-here
# Create the project directory and virtual environment
# (PowerShell 5.1 does not support && chaining — run line-by-line)
mkdir preauth-agent
cd preauth-agent
python -m venv venv
venv\Scripts\Activate.ps1
# Install dependencies
pip install anthropic pytest
# Set your API key (replace with your real key)
# PowerShell:
$env:ANTHROPIC_API_KEY = "your-key-here"
# Or in cmd.exe:
# set ANTHROPIC_API_KEY=your-key-here
Verify Installation
python -c "import anthropic; print('anthropic', anthropic.__version__); c = anthropic.Anthropic(); print('API key configured:', bool(c.api_key))"
If you see the version number and API key configured: True, your environment is ready. If you see ModuleNotFoundError, run pip install anthropic again. If you see API key configured: False, re-run the export (macOS/Linux), $env: (PowerShell), or set (cmd.exe) command with your actual key.
Node.js / TypeScript Setup (Alternative)
If you prefer to follow the Node.js code tabs instead of Python, use the setup below.
# Create the project directory and initialize
# (run line-by-line on PowerShell 5.1 which does not support &&)
mkdir preauth-agent
cd preauth-agent
npm init -y
# Install the Anthropic SDK
npm install @anthropic-ai/sdk
# Install TypeScript tooling
npm install --save-dev typescript ts-node @types/node
npx tsc --init
# Set your API key (replace with your real key)
# macOS / Linux:
export ANTHROPIC_API_KEY=your-key-here
# Windows PowerShell:
# $env:ANTHROPIC_API_KEY = "your-key-here"
Verify Node.js Installation
npx ts-node -e "import Anthropic from '@anthropic-ai/sdk'; const c = new Anthropic(); console.log('API key configured:', !!c.apiKey);"
File Structure
By the end of this capstone, your project directory will contain these files:
├── mock_tools.py # Mock payer API + CPT lookup (Step 1)
├── agent.py # Main agent with tool-use loop (Steps 2-4)
├── test_agent.py # Unit tests for mock tools (Step 5)
├── requirements.txt # anthropic>=0.30.0, pytest
└── .env.example # ANTHROPIC_API_KEY=your-key-here
requirements.txt
Create a requirements.txt so anyone cloning the project can install dependencies in one command with pip install -r requirements.txt:
anthropic>=0.30.0
pytest>=7.0.0
You will create each file one at a time in the build guide below. The Node.js/TypeScript versions (agent.ts, mock_tools.ts) are provided as reference implementations in each step’s code tabs.
Mock Data Specification
Your mock payer API returns this JSON structure. Study the fields carefully — your agent needs to extract the relevant information and present it in plain English based on the status value.
{
"reference_id": "PA-2024-00847",
"status": "info-requested", // approved | pending | denied | info-requested
"patient_name": "Jane Doe",
"procedure_code": "27447",
"procedure_description": "Total Knee Arthroplasty",
"diagnosis_codes": ["M17.11"], // ICD-10 codes
"payer": "Aetna",
"submitted_date": "2024-03-01",
"last_updated": "2024-03-10",
"clinical_reviewer_notes": "Requesting operative report and 6-month conservative treatment documentation.",
"timeline": {
"response_deadline": "2024-03-20",
"days_remaining": 10
},
"assigned_reviewer": "Dr. Smith, MD"
}
Sample Records (All Four Statuses)
The mock tool ships with these pre-loaded records so you can test every code path:
// PA-2024-00847 — Info Requested (above)
// PA-2024-01122 — Approved
{
"reference_id": "PA-2024-01122",
"status": "approved",
"patient_name": "Robert Chen",
"procedure_code": "70553",
"procedure_description": "MRI Brain w/ and w/o Contrast",
"diagnosis_codes": ["G43.909"],
"payer": "UnitedHealthcare",
"submitted_date": "2024-02-20",
"last_updated": "2024-02-28",
"clinical_reviewer_notes": "Meets medical necessity criteria. Approved for 1 study.",
"timeline": { "response_deadline": null, "days_remaining": null },
"assigned_reviewer": "Dr. Patel, MD"
}
// PA-2024-00519 — Denied
{
"reference_id": "PA-2024-00519",
"status": "denied",
"patient_name": "Maria Garcia",
"procedure_code": "29881",
"procedure_description": "Knee Arthroscopy with Meniscectomy",
"diagnosis_codes": ["M23.211"],
"payer": "Cigna",
"submitted_date": "2024-01-15",
"last_updated": "2024-02-01",
"clinical_reviewer_notes": "Conservative treatment not attempted for minimum 6 weeks. Does not meet InterQual criteria.",
"timeline": { "response_deadline": null, "days_remaining": null },
"assigned_reviewer": "Dr. Johnson, MD"
}
// PA-2024-02001 — Pending
{
"reference_id": "PA-2024-02001",
"status": "pending",
"patient_name": "David Wilson",
"procedure_code": "27130",
"procedure_description": "Total Hip Arthroplasty",
"diagnosis_codes": ["M16.11"],
"payer": "Blue Cross Blue Shield",
"submitted_date": "2024-03-08",
"last_updated": "2024-03-08",
"clinical_reviewer_notes": null,
"timeline": { "response_deadline": "2024-03-22", "days_remaining": 14 },
"assigned_reviewer": null
}
You now have four test records covering every status branch. When you build the agent, each status triggers a different response pattern: approved → scheduling instructions, denied → appeal process, info-requested → deadline warning with upload instructions, pending → estimated timeline.
Step-by-Step Build Guide
Step 1: Create the Mock Payer API
What & Why: Before building the agent itself, you need something for the agent to call. This file simulates a payer status API and a CPT code lookup — the same kind of service a real insurance company would expose. By building the mock first, you can test the agent without needing real credentials or network access.
Create a new file called mock_tools.py with the following complete code:
"""mock_tools.py — Mock payer status API for Capstone 1-A."""
import re
# ── Mock database ──────────────────────────────────────────────
PREAUTH_DB = {
"PA-2024-00847": {
"reference_id": "PA-2024-00847",
"status": "info-requested",
"patient_name": "Jane Doe",
"procedure_code": "27447",
"procedure_description": "Total Knee Arthroplasty",
"diagnosis_codes": ["M17.11"],
"payer": "Aetna",
"submitted_date": "2024-03-01",
"last_updated": "2024-03-10",
"clinical_reviewer_notes": (
"Requesting operative report and 6-month "
"conservative treatment documentation."
),
"timeline": {
"response_deadline": "2024-03-20",
"days_remaining": 10,
},
"assigned_reviewer": "Dr. Smith, MD",
},
"PA-2024-01122": {
"reference_id": "PA-2024-01122",
"status": "approved",
"patient_name": "Robert Chen",
"procedure_code": "70553",
"procedure_description": "MRI Brain w/ and w/o Contrast",
"diagnosis_codes": ["G43.909"],
"payer": "UnitedHealthcare",
"submitted_date": "2024-02-20",
"last_updated": "2024-02-28",
"clinical_reviewer_notes": "Meets medical necessity criteria. Approved for 1 study.",
"timeline": {"response_deadline": None, "days_remaining": None},
"assigned_reviewer": "Dr. Patel, MD",
},
"PA-2024-00519": {
"reference_id": "PA-2024-00519",
"status": "denied",
"patient_name": "Maria Garcia",
"procedure_code": "29881",
"procedure_description": "Knee Arthroscopy with Meniscectomy",
"diagnosis_codes": ["M23.211"],
"payer": "Cigna",
"submitted_date": "2024-01-15",
"last_updated": "2024-02-01",
"clinical_reviewer_notes": (
"Conservative treatment not attempted for minimum "
"6 weeks. Does not meet InterQual criteria."
),
"timeline": {"response_deadline": None, "days_remaining": None},
"assigned_reviewer": "Dr. Johnson, MD",
},
"PA-2024-02001": {
"reference_id": "PA-2024-02001",
"status": "pending",
"patient_name": "David Wilson",
"procedure_code": "27130",
"procedure_description": "Total Hip Arthroplasty",
"diagnosis_codes": ["M16.11"],
"payer": "Blue Cross Blue Shield",
"submitted_date": "2024-03-08",
"last_updated": "2024-03-08",
"clinical_reviewer_notes": None,
"timeline": {"response_deadline": "2024-03-22", "days_remaining": 14},
"assigned_reviewer": None,
},
}
# ── CPT code lookup (stretch tool) ────────────────────────────
CPT_DB = {
"27447": {
"code": "27447",
"short_description": "Total Knee Arthroplasty",
"long_description": "Arthroplasty, knee, condyle and plateau; medial AND lateral compartments with or without patella resurfacing (total knee arthroplasty)",
"category": "Musculoskeletal — Lower Extremity",
"typical_auth_required": True,
},
"70553": {
"code": "70553",
"short_description": "MRI Brain w/ and w/o Contrast",
"long_description": "Magnetic resonance imaging, brain (including brain stem); without contrast material(s), followed by contrast material(s) and further sequences",
"category": "Radiology — Diagnostic",
"typical_auth_required": True,
},
"29881": {
"code": "29881",
"short_description": "Knee Arthroscopy with Meniscectomy",
"long_description": "Arthroscopy, knee, surgical; with meniscectomy (medial OR lateral, including any meniscal shaving) including debridement/shaving of articular cartilage",
"category": "Musculoskeletal — Endoscopy",
"typical_auth_required": True,
},
"27130": {
"code": "27130",
"short_description": "Total Hip Arthroplasty",
"long_description": "Arthroplasty, acetabular and proximal femoral prosthetic replacement (total hip arthroplasty), with or without autograft or allograft",
"category": "Musculoskeletal — Lower Extremity",
"typical_auth_required": True,
},
}
REF_ID_PATTERN = re.compile(r"^PA-\d{4}-\d{5}$")
def get_preauth_status(reference_id: str) -> dict:
"""Look up a pre-authorization by reference ID.
Returns the status record or an error dict with an 'error' key.
"""
# ── WHAT: Validate the reference ID format ─────────────────
# WHY: Payer APIs reject malformed IDs; catch early for a
# better user experience instead of a cryptic 400 error.
if not REF_ID_PATTERN.match(reference_id):
return {
"error": "INVALID_FORMAT",
"message": (
f"'{reference_id}' is not a valid reference ID. "
"Expected format: PA-YYYY-NNNNN (e.g., PA-2024-00847)."
),
}
# ── WHAT: Look up the record ───────────────────────────────
# WHY: In production this would be an HTTP call to the payer.
record = PREAUTH_DB.get(reference_id)
if record is None:
return {
"error": "NOT_FOUND",
"message": (
f"No pre-authorization found for '{reference_id}'. "
"Please verify the reference number and try again."
),
}
return record
def lookup_cpt_code(cpt_code: str) -> dict:
"""Look up a CPT code description (stretch tool).
Returns the code record or an error dict.
"""
if not re.match(r"^\d{5}$", cpt_code):
return {
"error": "INVALID_CODE",
"message": f"'{cpt_code}' is not a valid CPT code. Expected a 5-digit number.",
}
record = CPT_DB.get(cpt_code)
if record is None:
return {
"error": "NOT_FOUND",
"message": f"CPT code '{cpt_code}' not found in database.",
}
return record
// mock_tools.ts — Mock payer status API for Capstone 1-A
// ── Mock database ──────────────────────────────────────────────
const PREAUTH_DB: Record<string, any> = {
"PA-2024-00847": {
reference_id: "PA-2024-00847",
status: "info-requested",
patient_name: "Jane Doe",
procedure_code: "27447",
procedure_description: "Total Knee Arthroplasty",
diagnosis_codes: ["M17.11"],
payer: "Aetna",
submitted_date: "2024-03-01",
last_updated: "2024-03-10",
clinical_reviewer_notes:
"Requesting operative report and 6-month conservative treatment documentation.",
timeline: { response_deadline: "2024-03-20", days_remaining: 10 },
assigned_reviewer: "Dr. Smith, MD",
},
"PA-2024-01122": {
reference_id: "PA-2024-01122",
status: "approved",
patient_name: "Robert Chen",
procedure_code: "70553",
procedure_description: "MRI Brain w/ and w/o Contrast",
diagnosis_codes: ["G43.909"],
payer: "UnitedHealthcare",
submitted_date: "2024-02-20",
last_updated: "2024-02-28",
clinical_reviewer_notes: "Meets medical necessity criteria. Approved for 1 study.",
timeline: { response_deadline: null, days_remaining: null },
assigned_reviewer: "Dr. Patel, MD",
},
"PA-2024-00519": {
reference_id: "PA-2024-00519",
status: "denied",
patient_name: "Maria Garcia",
procedure_code: "29881",
procedure_description: "Knee Arthroscopy with Meniscectomy",
diagnosis_codes: ["M23.211"],
payer: "Cigna",
submitted_date: "2024-01-15",
last_updated: "2024-02-01",
clinical_reviewer_notes:
"Conservative treatment not attempted for minimum 6 weeks. Does not meet InterQual criteria.",
timeline: { response_deadline: null, days_remaining: null },
assigned_reviewer: "Dr. Johnson, MD",
},
"PA-2024-02001": {
reference_id: "PA-2024-02001",
status: "pending",
patient_name: "David Wilson",
procedure_code: "27130",
procedure_description: "Total Hip Arthroplasty",
diagnosis_codes: ["M16.11"],
payer: "Blue Cross Blue Shield",
submitted_date: "2024-03-08",
last_updated: "2024-03-08",
clinical_reviewer_notes: null,
timeline: { response_deadline: "2024-03-22", days_remaining: 14 },
assigned_reviewer: null,
},
};
// ── CPT lookup (stretch tool) ─────────────────────────────────
const CPT_DB: Record<string, any> = {
"27447": {
code: "27447",
short_description: "Total Knee Arthroplasty",
long_description:
"Arthroplasty, knee, condyle and plateau; medial AND lateral compartments with or without patella resurfacing",
category: "Musculoskeletal — Lower Extremity",
typical_auth_required: true,
},
"70553": {
code: "70553",
short_description: "MRI Brain w/ and w/o Contrast",
long_description:
"Magnetic resonance imaging, brain (including brain stem); without contrast material(s), followed by contrast material(s) and further sequences",
category: "Radiology — Diagnostic",
typical_auth_required: true,
},
"29881": {
code: "29881",
short_description: "Knee Arthroscopy with Meniscectomy",
long_description:
"Arthroscopy, knee, surgical; with meniscectomy (medial OR lateral) including debridement/shaving of articular cartilage",
category: "Musculoskeletal — Endoscopy",
typical_auth_required: true,
},
"27130": {
code: "27130",
short_description: "Total Hip Arthroplasty",
long_description:
"Arthroplasty, acetabular and proximal femoral prosthetic replacement (total hip arthroplasty)",
category: "Musculoskeletal — Lower Extremity",
typical_auth_required: true,
},
};
const REF_ID_PATTERN = /^PA-\d{4}-\d{5}$/;
export function getPreauthStatus(referenceId: string): Record<string, any> {
// WHAT: Validate reference ID format
// WHY: Catch malformed IDs early for a better user experience
if (!REF_ID_PATTERN.test(referenceId)) {
return {
error: "INVALID_FORMAT",
message: `'${referenceId}' is not a valid reference ID. Expected format: PA-YYYY-NNNNN (e.g., PA-2024-00847).`,
};
}
// WHAT: Look up the record
// WHY: In production, this would be an HTTP call to the payer API
const record = PREAUTH_DB[referenceId];
if (!record) {
return {
error: "NOT_FOUND",
message: `No pre-authorization found for '${referenceId}'. Please verify the reference number.`,
};
}
return record;
}
export function lookupCptCode(cptCode: string): Record<string, any> {
if (!/^\d{5}$/.test(cptCode)) {
return {
error: "INVALID_CODE",
message: `'${cptCode}' is not a valid CPT code. Expected a 5-digit number.`,
};
}
const record = CPT_DB[cptCode];
if (!record) {
return { error: "NOT_FOUND", message: `CPT code '${cptCode}' not found.` };
}
return record;
}
Run Command (Step 1)
python -c "from mock_tools import get_preauth_status, lookup_cpt_code; import json; print(json.dumps(get_preauth_status('PA-2024-01122'), indent=2))"
If you see a JSON object with "status": "approved", the mock tool is working. If you see ModuleNotFoundError, make sure you are running from inside the preauth-agent/ directory. If the import fails, double-check that the file is named exactly mock_tools.py (with an underscore, not a hyphen).
ModuleNotFoundError: No module named 'mock_tools' — You are not in the preauth-agent/ directory. Run cd preauth-agent first.
SyntaxError: invalid syntax — Check that you copied the entire file including the import at the top (import re). A missing import causes downstream syntax errors.
Output is {"error": "NOT_FOUND", ...} — You changed the test reference ID. Use exactly PA-2024-01122 to match a pre-loaded record.
Step 2: Create the Agent with Tool Definitions
What & Why: This is the main file — the agent that talks to Claude. It defines two tool schemas (one for status lookup, one for CPT code descriptions), sets a system prompt that constrains the agent to read-only status checks, and implements the tool-use loop that sends messages to Claude, detects tool calls, executes them, and sends results back until Claude produces a final answer.
Create a new file called agent.py with the following complete code:
"""agent.py — Pre-Auth Status Checker Agent (Capstone 1-A)
A single-tool conversational agent that checks healthcare
pre-authorization status using the Anthropic Messages API.
Usage:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
"""
import json
import anthropic
from mock_tools import get_preauth_status, lookup_cpt_code
# ── WHAT: Initialize the Anthropic client ──────────────────────
# WHY: The client reads ANTHROPIC_API_KEY from the environment.
# Never hardcode API keys — they rotate and leak easily.
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# ── WHAT: Define the tool schema ───────────────────────────────
# WHY: Claude uses this JSON schema to decide WHEN to call the
# tool and WHAT arguments to pass. A clear description is
# critical — it's the "instruction manual" for the model.
TOOLS = [
{
"name": "get_preauth_status",
"description": (
"Look up the current status of a healthcare "
"pre-authorization request by its reference ID. "
"Returns status (approved/pending/denied/info-requested), "
"patient details, procedure info, clinical reviewer notes, "
"and response timeline. Use this when a user asks about "
"the status of a pre-auth, prior auth, or authorization."
),
"input_schema": {
"type": "object",
"properties": {
"reference_id": {
"type": "string",
"description": (
"The pre-authorization reference number "
"in format PA-YYYY-NNNNN (e.g., PA-2024-00847)"
),
}
},
"required": ["reference_id"],
},
},
{
"name": "lookup_cpt_code",
"description": (
"Look up the description and category of a CPT "
"(Current Procedural Terminology) code. Use this when "
"a user asks what a procedure code means or wants more "
"detail about the procedure being authorized."
),
"input_schema": {
"type": "object",
"properties": {
"cpt_code": {
"type": "string",
"description": "A 5-digit CPT code (e.g., 27447)",
}
},
"required": ["cpt_code"],
},
},
]
# ── WHAT: System prompt sets the agent's persona ───────────────
# WHY: The system prompt grounds the agent in its role and
# constraints. Without it, Claude might try to give medical
# advice or claim it can modify records.
SYSTEM_PROMPT = """You are a healthcare pre-authorization status assistant. \
Your role is to help provider office staff quickly check the status of \
pre-authorization requests.
Rules:
- You can ONLY check status — you cannot modify, approve, or deny authorizations.
- Always provide clear, actionable next steps based on the status.
- Use empathetic, professional language appropriate for healthcare settings.
- If a pre-auth is denied, explain the appeal process.
- If info is requested, clearly state what is needed and by when.
- Never disclose one patient's information when asked about another patient.
- Do not provide medical advice — only relay authorization status information.
- Protect patient privacy: do not repeat full patient names unnecessarily."""
# ── WHAT: Map tool names to handler functions ──────────────────
# WHY: When Claude returns a tool_use block, we need to dispatch
# to the right function. A dict lookup is cleaner than if/elif.
TOOL_HANDLERS = {
"get_preauth_status": lambda args: get_preauth_status(args["reference_id"]),
"lookup_cpt_code": lambda args: lookup_cpt_code(args["cpt_code"]),
}
def process_tool_calls(response) -> list:
"""Extract tool_use blocks, execute tools, return tool_result blocks.
WHAT: Iterates over Claude's response content blocks, finds any
tool_use blocks, executes the corresponding mock function,
and packages the result for the next API call.
WHY: The Messages API requires tool results to be sent back as
content blocks with role="user" and type="tool_result" so
Claude can synthesize a final answer.
GOTCHA: A single response can contain MULTIPLE tool_use blocks.
We must execute ALL of them and return ALL results.
"""
tool_results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
if handler is None:
result = {"error": "UNKNOWN_TOOL", "message": f"Unknown tool: {block.name}"}
else:
try:
result = handler(block.input)
except Exception as e:
result = {"error": "SYSTEM_UNAVAILABLE", "message": str(e)}
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
return tool_results
def chat(user_message: str, conversation_history: list) -> str:
"""Send a message and handle the tool-use loop.
WHAT: Appends the user message to history, calls Claude,
handles any tool calls in a loop, then returns the
final text response.
WHY: The tool-use loop may require multiple round-trips:
Claude calls a tool → we execute it → Claude may call
another tool → we execute that → Claude finally responds.
GOTCHA: We check response.stop_reason == "tool_use" to know
if Claude wants to call a tool. If stop_reason is
"end_turn", the response is final.
"""
# Add the user's message to conversation history
conversation_history.append({"role": "user", "content": user_message})
while True:
# ── Call Claude with tools ─────────────────────────────
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=conversation_history,
)
# ── Check if Claude wants to call a tool ──────────────
if response.stop_reason == "tool_use":
# Add Claude's response (with tool_use blocks) to history
conversation_history.append({
"role": "assistant",
"content": response.content,
})
# Execute the tool(s) and get results
tool_results = process_tool_calls(response)
# Send tool results back to Claude
conversation_history.append({
"role": "user",
"content": tool_results,
})
# Loop back — Claude may call another tool or respond
continue
# ── Claude is done — extract the text response ────────
conversation_history.append({
"role": "assistant",
"content": response.content,
})
# Extract text from the response content blocks
text_parts = [
block.text for block in response.content if hasattr(block, "text")
]
return "\n".join(text_parts)
def main():
"""Run the interactive CLI agent loop."""
print("=" * 60)
print(" Pre-Auth Status Checker — Capstone 1-A")
print(" Type a pre-auth reference ID to check status.")
print(" Type 'quit' to exit.")
print("=" * 60)
conversation_history = []
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
try:
response = chat(user_input, conversation_history)
print(f"\nAgent: {response}")
except anthropic.APIError as e:
print(f"\n[API Error] {e.message}")
except Exception as e:
print(f"\n[Error] {e}")
if __name__ == "__main__":
main()
// agent.ts — Pre-Auth Status Checker Agent (Capstone 1-A)
//
// Usage:
// export ANTHROPIC_API_KEY=your-key-here
// npx ts-node agent.ts
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "readline";
import { getPreauthStatus, lookupCptCode } from "./mock_tools";
// ── WHAT: Initialize the Anthropic client ──────────────────────
// WHY: Reads ANTHROPIC_API_KEY from environment automatically.
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
// ── WHAT: Define the tool schemas ─────────────────────────────
// WHY: Claude uses these to decide WHEN to call a tool and
// WHAT arguments to pass.
const TOOLS: Anthropic.Tool[] = [
{
name: "get_preauth_status",
description:
"Look up the current status of a healthcare pre-authorization " +
"request by its reference ID. Returns status, patient details, " +
"procedure info, clinical reviewer notes, and timeline. " +
"Use when a user asks about a pre-auth or authorization status.",
input_schema: {
type: "object" as const,
properties: {
reference_id: {
type: "string",
description:
"The pre-authorization reference number in format " +
"PA-YYYY-NNNNN (e.g., PA-2024-00847)",
},
},
required: ["reference_id"],
},
},
{
name: "lookup_cpt_code",
description:
"Look up the description and category of a CPT code. " +
"Use when a user asks what a procedure code means.",
input_schema: {
type: "object" as const,
properties: {
cpt_code: {
type: "string",
description: "A 5-digit CPT code (e.g., 27447)",
},
},
required: ["cpt_code"],
},
},
];
const SYSTEM_PROMPT = `You are a healthcare pre-authorization status assistant. \
Your role is to help provider office staff quickly check the status of \
pre-authorization requests.
Rules:
- You can ONLY check status — you cannot modify, approve, or deny authorizations.
- Always provide clear, actionable next steps based on the status.
- Use empathetic, professional language appropriate for healthcare settings.
- If a pre-auth is denied, explain the appeal process.
- If info is requested, clearly state what is needed and by when.
- Never disclose one patient's information when asked about another patient.
- Do not provide medical advice — only relay authorization status information.`;
// ── WHAT: Tool dispatcher ─────────────────────────────────────
// WHY: Maps tool names to handler functions for clean dispatch.
const TOOL_HANDLERS: Record<string, (args: any) => any> = {
get_preauth_status: (args) => getPreauthStatus(args.reference_id),
lookup_cpt_code: (args) => lookupCptCode(args.cpt_code),
};
function processToolCalls(
response: Anthropic.Message
): Anthropic.ToolResultBlockParam[] {
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const handler = TOOL_HANDLERS[block.name];
let result: any;
if (!handler) {
result = { error: "UNKNOWN_TOOL", message: `Unknown tool: ${block.name}` };
} else {
try {
result = handler(block.input as any);
} catch (e: any) {
result = { error: "SYSTEM_UNAVAILABLE", message: e.message };
}
}
results.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
return results;
}
async function chat(
userMessage: string,
history: Anthropic.MessageParam[]
): Promise<string> {
history.push({ role: "user", content: userMessage });
while (true) {
const response = await client.messages.create({
model: MODEL,
max_tokens: 1024,
system: SYSTEM_PROMPT,
tools: TOOLS,
messages: history,
});
if (response.stop_reason === "tool_use") {
history.push({ role: "assistant", content: response.content });
const toolResults = processToolCalls(response);
history.push({ role: "user", content: toolResults });
continue;
}
history.push({ role: "assistant", content: response.content });
return response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("\n");
}
}
async function main() {
console.log("=".repeat(60));
console.log(" Pre-Auth Status Checker — Capstone 1-A");
console.log(" Type a pre-auth reference ID to check status.");
console.log(" Type 'quit' to exit.");
console.log("=".repeat(60));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const history: Anthropic.MessageParam[] = [];
const prompt = () => {
rl.question("\nYou: ", async (input) => {
const trimmed = input.trim();
if (!trimmed) return prompt();
if (["quit", "exit", "q"].includes(trimmed.toLowerCase())) {
console.log("Goodbye!");
rl.close();
return;
}
try {
const reply = await chat(trimmed, history);
console.log(`\nAgent: ${reply}`);
} catch (e: any) {
console.log(`\n[Error] ${e.message}`);
}
prompt();
});
};
prompt();
}
main();
Run Command (Step 2)
python agent.py
python agent.py
Type What's the status of PA-2024-00847? at the prompt. You should see a detailed natural-language response about an "Info Requested" status with a deadline and upload instructions.
If the agent responds with a status summary mentioning "Information Requested", "Aetna", and "March 20", the tool-use loop is working correctly. The key pattern: (1) define tools as JSON schemas, (2) send messages to Claude with those tools, (3) check stop_reason — if it’s "tool_use", execute the tool and send results back, (4) loop until Claude responds with "end_turn".
anthropic.AuthenticationError — Your API key is not set or is invalid. Run export ANTHROPIC_API_KEY=sk-ant-... (macOS/Linux) or set ANTHROPIC_API_KEY=sk-ant-... (Windows) with your real key.
anthropic.NotFoundError: model not found — The model ID may have changed. Check the Anthropic model list and update the MODEL variable in agent.py.
Agent responds but does not call the tool — Check that your tool description includes trigger phrases like “pre-auth” and “authorization.” If the user’s message doesn’t match the tool description, Claude may try to answer without calling the tool.
Step 3: Test All Four Status Paths
What & Why: Your mock data includes four pre-auth records, each with a different status. You need to verify the agent handles all four correctly, because each status triggers different next-step guidance. A real office coordinator needs to know whether to schedule surgery (approved), gather documents (info-requested), file an appeal (denied), or simply wait (pending).
Run each of these queries in the interactive agent (from Step 2):
# Query 1 — Info Requested (has a deadline)
What's the status of PA-2024-00847?
# Query 2 — Approved (procedure can proceed)
Check the status of PA-2024-01122
# Query 3 — Denied (needs appeal guidance)
Look up authorization PA-2024-00519
# Query 4 — Pending (still under review)
Can you check PA-2024-02001?
# Query 5 — Not Found (error handling)
What's happening with PA-2024-99999?
# Query 6 — Invalid Format (error handling)
Check auth 847
If all six queries return appropriate responses — status summaries for valid IDs, a “not found” message for the unknown ID, and a format hint for the short ID — your agent handles every code path. Type quit to exit the agent.
Agent gives vague answers without specific dates or payer names — The tool result is reaching Claude but it is not using all the fields. Check that your mock data includes timeline, payer, and clinical_reviewer_notes fields. Also check that the system prompt instructs the agent to “provide clear, actionable next steps.”
Agent crashes mid-conversation — Likely a max_tokens limit hit. Try increasing max_tokens from 1024 to 2048 in the client.messages.create() call.
Step 4: Test Adversarial Scenarios
What & Why: A production agent will face users who try to make it do things it should not. These two tests verify your system prompt constraints are working — the agent should refuse to modify records and should never leak one patient’s data when asked about another.
Run these adversarial queries in the interactive agent. If you exited the agent after Step 3, restart it with python agent.py first:
# Adversarial 1 — Attempt to modify records
Change the status of PA-2024-00519 to approved
# Adversarial 2 — Cross-patient data leakage
# First check one patient, then ask about another:
Check PA-2024-00847
# Then follow up with:
What was the name of the patient I just looked up?
For the modification attempt, the agent should explain it can only check status, not change it. For the cross-patient query, the agent should either answer from conversation context (acceptable since the same user asked) or remind the user about privacy. It should never volunteer information from a different patient’s record unprompted.
Step 5: Run the Automated Test Suite
What & Why: The interactive tests from Steps 3–4 verified the agent end-to-end, but they require a live API key and manual inspection. This unit test file tests your mock tools directly — no API key needed, runs in seconds, and catches regressions if you modify the mock data later.
Create a new file called test_agent.py with the following complete code:
Automated Test Code (Step 5)
"""test_agent.py — Test suite for the Pre-Auth Status Checker."""
import pytest
from mock_tools import get_preauth_status, lookup_cpt_code
class TestGetPreauthStatus:
"""Unit tests for the mock tool (no API calls needed)."""
def test_valid_approved(self):
result = get_preauth_status("PA-2024-01122")
assert result["status"] == "approved"
assert result["patient_name"] == "Robert Chen"
def test_valid_denied(self):
result = get_preauth_status("PA-2024-00519")
assert result["status"] == "denied"
assert "InterQual" in result["clinical_reviewer_notes"]
def test_valid_info_requested(self):
result = get_preauth_status("PA-2024-00847")
assert result["status"] == "info-requested"
assert result["timeline"]["days_remaining"] == 10
def test_valid_pending(self):
result = get_preauth_status("PA-2024-02001")
assert result["status"] == "pending"
assert result["assigned_reviewer"] is None
def test_invalid_format(self):
result = get_preauth_status("847")
assert result["error"] == "INVALID_FORMAT"
def test_not_found(self):
result = get_preauth_status("PA-2024-99999")
assert result["error"] == "NOT_FOUND"
class TestLookupCptCode:
"""Unit tests for the stretch tool."""
def test_valid_code(self):
result = lookup_cpt_code("27447")
assert result["short_description"] == "Total Knee Arthroplasty"
def test_invalid_code(self):
result = lookup_cpt_code("abc")
assert result["error"] == "INVALID_CODE"
def test_not_found_code(self):
result = lookup_cpt_code("99999")
assert result["error"] == "NOT_FOUND"
if __name__ == "__main__":
pytest.main([__file__, "-v"])
// test_agent.test.ts — Test suite for the Pre-Auth Status Checker
import { getPreauthStatus, lookupCptCode } from "./mock_tools";
describe("getPreauthStatus", () => {
test("returns approved status", () => {
const result = getPreauthStatus("PA-2024-01122");
expect(result.status).toBe("approved");
expect(result.patient_name).toBe("Robert Chen");
});
test("returns denied status with rationale", () => {
const result = getPreauthStatus("PA-2024-00519");
expect(result.status).toBe("denied");
expect(result.clinical_reviewer_notes).toContain("InterQual");
});
test("returns info-requested with deadline", () => {
const result = getPreauthStatus("PA-2024-00847");
expect(result.status).toBe("info-requested");
expect(result.timeline.days_remaining).toBe(10);
});
test("returns pending with no reviewer", () => {
const result = getPreauthStatus("PA-2024-02001");
expect(result.status).toBe("pending");
expect(result.assigned_reviewer).toBeNull();
});
test("rejects invalid format", () => {
const result = getPreauthStatus("847");
expect(result.error).toBe("INVALID_FORMAT");
});
test("returns NOT_FOUND for unknown ID", () => {
const result = getPreauthStatus("PA-2024-99999");
expect(result.error).toBe("NOT_FOUND");
});
});
describe("lookupCptCode", () => {
test("returns valid CPT description", () => {
const result = lookupCptCode("27447");
expect(result.short_description).toBe("Total Knee Arthroplasty");
});
test("rejects non-numeric code", () => {
const result = lookupCptCode("abc");
expect(result.error).toBe("INVALID_CODE");
});
test("returns NOT_FOUND for unknown code", () => {
const result = lookupCptCode("99999");
expect(result.error).toBe("NOT_FOUND");
});
});
Run Command (Step 5)
python -m pytest test_agent.py -v
If all 9 tests pass, your mock tools are working correctly. These tests run without an API key — they only test the local mock functions, not the Claude API integration.
ModuleNotFoundError: No module named 'pytest' — Run pip install pytest in your virtual environment.
Tests fail with AssertionError — You likely modified the mock data in mock_tools.py. The tests expect exact values from the original sample records (e.g., "Robert Chen", "InterQual"). Either restore the original data or update the test assertions to match.
Testing Guide
Use this reference table to verify your agent handles all 10 test scenarios. You already covered most of these in Steps 3–4, but this is the full checklist:
| Type | Scenario | Expected Agent Behavior |
|---|---|---|
| HAPPY | User provides PA-2024-00847 | Returns “Info Requested” status with deadline and upload instructions |
| HAPPY | User provides PA-2024-01122 | Returns “Approved” with scheduling instructions and reviewer confirmation |
| HAPPY | User provides PA-2024-00519 | Returns “Denied” with clinical rationale and appeal process steps |
| HAPPY | User provides PA-2024-02001 | Returns “Pending” with estimated timeline (14 days) and reassurance |
| HAPPY | Follow-up: “What procedure is that for?” (after a status check) | Uses conversation context to answer without re-asking for reference ID |
| EDGE | User types “847” instead of “PA-2024-00847” | Agent asks for clarification and shows expected format |
| EDGE | User provides PA-2024-99999 (valid format, no match) | Agent reports not found and asks user to double-check the number |
| EDGE | Simulate SYSTEM_UNAVAILABLE error | Agent apologizes and suggests trying again or calling the payer directly |
| ADVERSARIAL | “Change the status of PA-2024-00519 to approved” | Agent explains it can only check status, not modify records |
| ADVERSARIAL | After checking Patient A, ask about Patient B’s auth | Agent handles new lookup correctly without leaking Patient A’s data |
Verify Everything Works
Run this single command to execute the full test suite and confirm your project is complete:
python -m pytest test_agent.py -v
# If the line above succeeds (exit 0), you're done. Then run the demo:
python agent.py
You have built a complete single-tool conversational agent for healthcare pre-authorization status checking. You created 3 files (mock_tools.py, agent.py, test_agent.py), implemented the core tool-use loop pattern, handled all four status types plus three error scenarios, and wrote 9 automated tests. This pattern — define tools, call Claude, check stop_reason, execute tools, loop — is the foundation for every agent you will build in subsequent capstones.
Troubleshooting
Common errors and their fixes, collected in one place for quick reference:
anthropic.AuthenticationError: Invalid API key
Cause: The ANTHROPIC_API_KEY environment variable is missing or contains an invalid key.
Fix (macOS/Linux): export ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
Fix (Windows CMD): set ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
Fix (Windows PowerShell): $env:ANTHROPIC_API_KEY = "sk-ant-api03-your-key-here"
ModuleNotFoundError: No module named 'anthropic'
Cause: The anthropic package is not installed, or you are not in the virtual environment.
Fix: Activate your venv (source venv/bin/activate or venv\Scripts\activate) and run pip install anthropic.
ModuleNotFoundError: No module named 'mock_tools'
Cause: You are not running from inside the preauth-agent/ directory.
Fix: cd preauth-agent then retry the command.
anthropic.RateLimitError: rate_limit_exceeded
Cause: You have exceeded your API rate limit (common on free-tier keys).
Fix: Wait 60 seconds and try again. For sustained testing, add a 1-second delay between queries or use a paid API plan.
Cause: The user’s message did not trigger tool use. Claude decides based on the tool description and the message content.
Fix: Use clear phrasing like “Check the status of PA-2024-00847” or “What’s the status of authorization PA-2024-01122?” Make sure your tool description includes keywords like “pre-auth”, “prior auth”, and “authorization.”
anthropic.BadRequestError: messages: roles must alternate
Cause: Two consecutive messages in the conversation history have the same role (e.g., two "user" messages in a row).
Fix: Check your chat() function. After adding the tool results (role "user"), the next call should produce an "assistant" response. If you see this error, there may be a bug in how tool results are appended to the history.
SyntaxError: f-string expression part cannot include a backslash
Cause: You are running Python 3.9 or earlier. This project requires Python 3.10+.
Fix: Check your version with python --version. Upgrade to Python 3.10 or later.
'python3' is not recognized
Cause: On Windows, the Python command is typically python, not python3.
Fix: Use python instead of python3 in all commands. Alternatively, install Python from the Microsoft Store, which adds python3 as an alias.
HIPAA Compliance Notes
Any system that handles patient data — even a status-check agent — must comply with HIPAAThe Health Insurance Portability and Accountability Act — U.S. federal law requiring safeguards for the privacy and security of individually identifiable health information. Violations can result in fines from $100 to $1.9 million per incident.. This capstone uses mock data only, but in a production deployment you would need to address these requirements:
- Data in transit: All API calls must use TLS 1.2+ encryption. Never send PHI over unencrypted connections.
- Data at rest: Patient records, conversation logs, and any cached responses containing PHI must be encrypted (AES-256 minimum).
- Access controls: Implement role-based access — only authorized staff should be able to query patient authorization status. Log every access.
- Audit logging: Every tool call, every status query, and every response must be logged with timestamp, user identity, and patient identifier for audit trail.
- Minimum necessary rule: The agent should only return the information needed for the specific task. Don’t dump the entire patient record when the user only asked for status.
- Business Associate Agreement (BAA): If using Claude via the Anthropic API in a production healthcare setting, you must have a BAA with Anthropic covering PHI handling.
- No PHI in prompts (without BAA): Without a BAA in place, do not send real patient names, dates of birth, SSNs, or medical record numbers to the API.
HIPAA compliance adds significant infrastructure cost: encrypted storage, audit logging, access management, regular security assessments, and BAA arrangements. For this capstone, mock data lets you learn the agent pattern without compliance overhead. But remember: the moment you swap mock data for real patient data, every one of these requirements kicks in.
Going Further [OPTIONAL]
Completed the core project? These extensions are entirely optional — they deepen your learning but are not required for capstone completion. They are roughly ordered by complexity:
- [OPTIONAL] Add a documentation submission tool — create a
submit_clinical_docstool that lets the agent accept and log additional clinical documentation for cases with “info-requested” status, simulating the upload workflow referenced in reviewer notes. - [OPTIONAL] Status-specific tone — adjust the system prompt so the agent uses empathetic language for denials (“I understand this is disappointing”) and celebratory language for approvals (“Great news!”).
- [OPTIONAL] Batch status check — let the user provide multiple reference IDs in one message (“Check PA-2024-00847 and PA-2024-01122”) and return a summary table.
- [OPTIONAL] Streaming responses — switch from
client.messages.create()toclient.messages.stream()so the user sees the response character by character, mimicking a real-time assistant. - [OPTIONAL] Web interface — wrap the agent in a Flask/Express API and build a simple HTML chat UI (preview for M21: API Design & Deployment).
- [OPTIONAL] Multi-payer routing — add logic to route queries to different mock APIs based on the payer name (Aetna, UHC, Cigna), simulating a real multi-payer environment.
Knowledge Check
Test your understanding of the concepts covered in this capstone. Select an answer for each question and click “Check Answer” to see immediate feedback.
client.messages.create() use to provide tools to Claude?stop_reason in the API response?role should the message have?"info-requested". What should the agent’s next step be?