Capstone 3 — Domain B: Order Exception Resolution Agent
Build a ReAct agent that investigates B2B order exceptions — shipment delays, pricing discrepancies, partial deliveries, quality holds — by reasoning across ERP, warehouse, carrier, and contract data to propose resolutions with customer communication drafts.
Complete M03 (Prompt Engineering), M04 (Structured Output), M05 (Function Calling), M06 (Multi-Tool Orchestration), 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, orchestrating multiple tools, and building agentic loops that check stop_reason.
Difficulty: ★★★☆☆ — Time estimate: 90 minutes – 2 hours — Steps: 8–12 numbered steps with code/run/output/checkpoint
Project Brief
A B2B operations analyst receives 20–30 order exceptionsAny deviation from the expected order lifecycle: a shipment that didn't arrive on time, an invoice with the wrong price, a delivery missing items, or a product put on quality hold. Exceptions require investigation across multiple systems to find root cause and resolution. per day. Each exception is a puzzle: the customer reports “my order didn’t arrive,” but the root cause could be a carrier weather delay in Memphis, an inventory shortfall at the warehouse, a quality hold on the SKU, or a pricing discrepancy that held the shipment. The analyst must check 3–4 different systems (ERP, WMS, carrier tracking, contract database) to diagnose the problem.
The pain isn’t just the investigation time (15–30 minutes per exception) — it’s the branching logic. A shipment delay requires checking the carrier API. But if the carrier says “delivered,” the analyst pivots to warehouse confirmation. If the warehouse says “quality hold,” the analyst pivots again to the quality system. Each intermediate finding redirects the investigation. A linear tool-calling agent can’t handle this — it needs to reason about what to check next.
Your agent uses the ReAct patternReason + Act — the agent writes a Thought explaining its reasoning before each tool call, observes the result, and adapts its investigation plan dynamically. This produces an auditable trail showing why the agent checked each system and how it reached its conclusion. to investigate exceptions dynamically: it reads the exception description, reasons about which system to check first, adapts based on what it finds, and ultimately proposes a resolution with a drafted customer notification.
A ReAct agent with 6 tools that:
- Accepts an order exception (type, PO number, customer description)
- Pulls order details from the ERP (
get_order_details) - Checks warehouse inventory levels (
query_warehouse_inventory) - Tracks shipments via carrier API (
track_shipment) - Verifies contract pricing (
get_contract_pricing) - Checks for quality holds (
check_quality_hold_status) - Drafts a customer notification (
draft_customer_notification)
Skills practiced: ReAct loop (M12), multi-tool orchestration (M06), planning (M13), branching logic, error recovery, structured output.
Optional stretch goal: Add a calculate_cost_impact tool that estimates the financial impact of the resolution (e.g., expedited shipping cost, discount credit). See the Going Further section for the scaffolded version.
Domain Glossary
Architecture
Mock Data Specification
// ── Exception Input ────────────────────────────────────────────
{
"exception_id": "EXC-2024-0456",
"po_number": "PO-2024-11234",
"exception_type": "shipment_delay",
"reported_by": "customer",
"reported_date": "2024-03-10",
"description": "Customer reports order not received by expected delivery date of 2024-03-08."
}
// ── Order Details (from ERP) ──────────────────────────────────
{
"po_number": "PO-2024-11234",
"customer": "Acme Manufacturing",
"line_items": [
{"line": 1, "sku": "SKU-4892", "qty_ordered": 50, "qty_shipped": 50,
"qty_delivered": 0, "unit_price": 495.00}
],
"total_value": 24750.00
}
// ── Carrier Tracking ──────────────────────────────────────────
{
"tracking_number": "1Z999AA10123456784",
"carrier": "UPS",
"status": "exception",
"exception_detail": "Weather delay — Memphis hub",
"original_eta": "2024-03-08",
"revised_eta": "2024-03-13",
"last_scan": "2024-03-09T18:45:00Z",
"last_location": "Memphis, TN"
}
// ── Warehouse Inventory ───────────────────────────────────────
{
"sku": "SKU-4892",
"available": 120,
"reserved": 50,
"location": "Warehouse-East",
"last_updated": "2024-03-10"
}
// ── Quality Hold ──────────────────────────────────────────────
{
"sku": "SKU-4892",
"hold_status": "none",
"hold_reason": null,
"hold_date": null,
"expected_resolution": null
}
// ── Contract Pricing ──────────────────────────────────────────
{
"sku": "SKU-4892",
"customer": "Acme Manufacturing",
"unit_price": 495.00,
"tier": "tier_2",
"contract_id": "CONTRACT-ACME-2024",
"contract_expiry": "2024-12-31"
}
The agent must navigate 5 different data sources to investigate one exception. The key insight: the investigation path is dynamic. A shipment delay starts with carrier tracking. A pricing discrepancy starts with contract verification. A quality hold starts with the QA system. The ReAct agent reasons about which tool to call next based on intermediate findings.
Step-by-Step Build Guide
A ReAct agent that investigates B2B order exceptions end-to-end. By the end of this guide, you will have a working multi-tool agent that diagnoses shipment delays, pricing discrepancies, partial deliveries, and quality holds — then drafts customer notifications.
Time estimate: 90 minutes – 2 hours | Steps: 8 numbered steps with code, run commands, expected output, and checkpoints
Step 1: Create the Project and Install Dependencies
What & Why: We create an isolated Python virtual environment so our dependencies do not conflict with other projects. We install the Anthropic SDK, which provides the client.messages.create() method for calling Claude with tools.
mkdir exception-resolver && cd exception-resolver
python3 -m venv venv && source venv/bin/activate
pip install "anthropic>=0.30.0"
export ANTHROPIC_API_KEY=your-key-here
# Run each line on its own — works in cmd.exe and PowerShell 5.1+
mkdir exception-resolver
cd exception-resolver
python -m venv venv
# PowerShell: venv\Scripts\Activate.ps1
# cmd.exe: venv\Scripts\activate.bat
venv\Scripts\Activate.ps1
pip install "anthropic>=0.30.0"
# PowerShell: $env:ANTHROPIC_API_KEY = "your-key-here"
# cmd.exe: set ANTHROPIC_API_KEY=your-key-here
$env:ANTHROPIC_API_KEY = "your-key-here"
Node.js / TypeScript Setup (optional)
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
Run command (Python):
Expected output (any 0.30+ version is fine):
If you see a version number (0.30+ is fine), Step 1 is complete. If you see ModuleNotFoundError, make sure your virtual environment is activated and re-run pip install "anthropic>=0.30.0".
File Structure
Here is the complete directory tree you will create over the next steps:
mock_tools.py # All 6 mock tools (ERP, WMS, carrier, pricing, QA, comms)
agent.py # ReAct agent loop with system prompt
mock_tools.ts # Node.js version of mock tools
agent.ts # Node.js version of agent
e2e_test.py # End-to-end verification script
data/
exceptions.json # Sample exception inputs
requirements.txt # anthropic
.env.example # ANTHROPIC_API_KEY=
Step 2: Create the Mock Data
What & Why: Real B2B integrations talk to ERP, WMS, and carrier APIs that require credentials and complex setup. Instead, we create an in-memory mock database with realistic order data, tracking events, inventory levels, contract pricing, and quality hold records. This lets you build and test the full agent without any external dependencies.
Create/Edit: Create a new file called data/exceptions.json.
[
{
"exception_id": "EXC-2024-0456",
"po_number": "PO-2024-11234",
"exception_type": "shipment_delay",
"reported_by": "customer",
"reported_date": "2024-03-10",
"description": "Customer reports order not received by expected delivery date of 2024-03-08."
},
{
"exception_id": "EXC-2024-0501",
"po_number": "PO-2024-11234",
"exception_type": "pricing_discrepancy",
"reported_by": "accounts_payable",
"reported_date": "2024-03-12",
"description": "Invoice shows $520/unit but contract says $495/unit for SKU-4892."
},
{
"exception_id": "EXC-2024-0523",
"po_number": "PO-2024-11890",
"exception_type": "quality_hold",
"reported_by": "warehouse",
"reported_date": "2024-03-06",
"description": "SKU-7721 on quality hold, cannot ship order PO-2024-11890."
}
]
Run command (verify JSON is valid):
Expected output:
If you see “3 exceptions loaded,” your data file is valid. If you see a JSONDecodeError, check for trailing commas or missing brackets in the JSON file.
Step 3: Build the 6 Mock Tools
What & Why: Each tool simulates a different backend system: ERP (order details), WMS (inventory), carrier tracking, contract pricing, quality hold status, and customer notification drafting. Each tool returns either a data record or an error dictionary. This is the same pattern you would use in production, except you would replace the mock dictionaries with real API calls.
Create/Edit: Create a new file called mock_tools.py. This is the complete file — paste the entire contents.
GOTCHA: The PRICING_DB uses a tuple (sku, customer) as the dictionary key. This is the lookup pattern for customer-specific contract pricing — the same SKU can have different prices for different customers.
Note: See the complete mock_tools.py code block below in the Mock Tool Implementations section — scroll down to copy it, then return here to verify the import.
Run command (verify all 6 tools import correctly):
Expected output:
If you see “All 6 tools imported successfully,” Step 3 is done. If you see an ImportError, check that mock_tools.py is in the same directory and has no syntax errors.
Step 4: Verify Each Tool Returns Correct Data
What & Why: Before building the agent, we verify each tool works independently. This isolates tool bugs from agent logic bugs. If the agent behaves unexpectedly later, you can rule out tool issues because you already verified them here.
Run command:
Expected output (truncated):
Each tool returns data for known inputs and an error dictionary for unknown inputs. The “Not Found” line confirms error handling works. If any tool throws an exception instead of returning a dictionary, check the function signature in mock_tools.py.
Step 5: Build the ReAct Agent Loop
What & Why: This is the core of the capstone. The agent loop sends a message to Claude, checks if Claude wants to call a tool (stop_reason == "tool_use"), executes the tool, returns the result, and repeats until Claude produces a final text response. The system prompt tells Claude to use the ReAct pattern: write a Thought before each tool call explaining its reasoning.
Create/Edit: Create a new file called agent.py. See the complete code block below in the Complete Solution section — scroll down to copy it, then return here to verify the import.
GOTCHA: The while True loop relies on stop_reason == "tool_use" to continue and "end_turn" to break. If the system prompt does not guide Claude to eventually stop calling tools and produce a final answer, the loop could run indefinitely. In production, add an iteration cap (e.g., max_iterations = 15).
You should now have two files: mock_tools.py (from Step 3) and agent.py. Both must be in the same directory. Run python -c "from agent import investigate_exception; print('Agent module OK')" to confirm the import works.
Step 6: Run the Agent on a Shipment Delay
What & Why: This is the moment of truth — we run the agent on a real exception input and see it reason through the investigation dynamically. The agent should pull order details, check carrier tracking, find the weather delay, verify no quality hold exists, and draft a customer notification.
Step dependency: This step uses mock_tools.py from Step 3 and agent.py from Step 5. Both files must exist in the same directory.
Run command:
When prompted, type demo to run the built-in sample exception (shipment delay for PO-2024-11234).
Step 7: Test Additional Exception Types
What & Why: A ReAct agent must handle different investigation paths for different exception types. We test a pricing discrepancy (which should start with contract verification) and a quality hold (which should start with the QA system). This verifies the agent’s branching logic, not just one happy path.
Run python agent.py and paste each exception as JSON input (one at a time):
{"exception_id":"EXC-2024-0501","po_number":"PO-2024-11234","exception_type":"pricing_discrepancy","description":"Invoice shows $520/unit but contract says $495/unit for SKU-4892."}
{"exception_id":"EXC-2024-0523","po_number":"PO-2024-11890","exception_type":"quality_hold","description":"SKU-7721 on quality hold, cannot ship order PO-2024-11890."}
For pricing, the agent should: (1) pull order details, (2) check contract pricing, (3) identify the $25/unit mismatch, (4) draft a pricing correction notification. For quality hold, the agent should: (1) check quality hold status for SKU-7721, (2) read order details, (3) report the expected resolution date, (4) draft a customer notification.
The agent should produce different tool call sequences for different exception types. A shipment delay triggers track_shipment; a pricing discrepancy triggers get_contract_pricing. If the agent always calls tools in the same order regardless of exception type, review your system prompt — the investigation strategies may need to be more explicit.
Step 8: Verify Error Recovery and Iteration Cap
What & Why: In production, carrier APIs go down, ERP systems timeout, and data can be missing. The agent must handle tool failures gracefully — note the failure, continue investigating with other tools, and flag the gap in its resolution. The agent.py you copied in Step 5 already includes an iteration cap (MAX_ITERATIONS = 15) to prevent infinite loops — in this step you confirm it works.
Inspect: Open agent.py and locate the investigate_exception function. Confirm you see the iteration counter and the bounded while iteration < MAX_ITERATIONS loop shown below.
# Inside investigate_exception (already present in agent.py):
MAX_ITERATIONS = 15
iteration = 0
while iteration < MAX_ITERATIONS:
iteration += 1
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})
history.append({"role": "user", "content": process_tool_calls(response)})
continue
history.append({"role": "assistant", "content": response.content})
return "\n".join(b.text for b in response.content if hasattr(b, "text"))
# If we exit the loop, the agent hit the iteration cap
return "[WARN] Agent reached maximum iterations without resolving. Manual review required."
Try testing with a non-existent PO number (e.g., "po_number": "PO-FAKE-999"). The agent should get a NOT_FOUND error from the tool, reason about the failure, and stop gracefully instead of looping forever.
- If
agent.pycannot importmock_tools, ensure both files are in the same directory and your terminal iscd’d into that directory. - If the agent returns empty output, check that your
ANTHROPIC_API_KEYis set:echo $ANTHROPIC_API_KEY(Unix) orecho %ANTHROPIC_API_KEY%(Windows). - If the agent calls the same tool repeatedly, review the system prompt investigation strategies — ensure each exception type has a clear resolution path.
Mock Tool Implementations
"""mock_tools.py — 6 tools for order exception investigation."""
ORDER_DB = {
"PO-2024-11234": {
"po_number": "PO-2024-11234", "customer": "Acme Manufacturing",
"order_date": "2024-02-15", "status": "shipped", "total_value": 24750.00,
"line_items": [
{"line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"qty_ordered": 50, "qty_shipped": 50, "qty_delivered": 0,
"unit_price": 495.00, "tracking_number": "1Z999AA10123456784"},
],
},
"PO-2024-11890": {
"po_number": "PO-2024-11890", "customer": "Summit Industrial Supply",
"order_date": "2024-03-01", "status": "quality_hold", "total_value": 25000.00,
"line_items": [
{"line": 1, "sku": "SKU-7721", "description": "Precision Pressure Gauge - Class A",
"qty_ordered": 200, "qty_shipped": 0, "qty_delivered": 0,
"unit_price": 125.00, "tracking_number": None},
],
},
}
TRACKING_DB = {
# UPS — 18-char "1Z" prefix
"1Z999AA10123456784": {
"tracking_number": "1Z999AA10123456784", "carrier": "UPS",
"status": "exception",
"exception_detail": "Weather delay — Memphis hub",
"original_eta": "2024-03-08", "revised_eta": "2024-03-13",
"last_scan": "2024-03-09T18:45:00Z", "last_location": "Memphis, TN",
},
# FedEx — 12-digit numeric
"794611205847": {
"tracking_number": "794611205847", "carrier": "FedEx",
"status": "in_transit",
"exception_detail": None,
"original_eta": "2024-03-11", "revised_eta": "2024-03-11",
"last_scan": "2024-03-10T09:12:00Z",
"last_location": "Indianapolis, IN",
},
# DHL — 10-digit numeric
"1234567890": {
"tracking_number": "1234567890", "carrier": "DHL",
"status": "exception",
"exception_detail": "Customs hold — missing commercial invoice",
"original_eta": "2024-03-09", "revised_eta": "2024-03-15",
"last_scan": "2024-03-09T22:08:00Z",
"last_location": "Cincinnati, OH (CVG hub)",
},
}
INVENTORY_DB = {
"SKU-4892": {"sku": "SKU-4892", "available": 120, "reserved": 50,
"location": "Warehouse-East", "last_updated": "2024-03-10"},
}
PRICING_DB = {
("SKU-4892", "Acme Manufacturing"): {
"sku": "SKU-4892", "customer": "Acme Manufacturing",
"unit_price": 495.00, "tier": "tier_2",
"contract_id": "CONTRACT-ACME-2024", "contract_expiry": "2024-12-31",
},
}
INVOICE_DB = {
"PO-2024-11234": {
"invoice_id": "INV-2024-8834", "po_number": "PO-2024-11234",
"customer": "Acme Manufacturing", "invoice_date": "2024-03-01",
"line_items": [
{"sku": "SKU-4892", "qty": 50, "unit_price": 520.00, "total": 26000.00},
],
"total_value": 26000.00, "status": "pending_review",
},
}
QUALITY_DB = {
"SKU-4892": {"sku": "SKU-4892", "hold_status": "none",
"hold_reason": None, "hold_date": None,
"expected_resolution": None},
"SKU-7721": {"sku": "SKU-7721", "hold_status": "active",
"hold_reason": "Supplier calibration issue — batch B2024-03",
"hold_date": "2024-03-05",
"expected_resolution": "2024-03-15"},
}
def get_order_details(po_number: str) -> dict:
po = po_number.upper().strip()
record = ORDER_DB.get(po)
if not record:
return {"error": "NOT_FOUND", "message": f"No order for '{po}'."}
return record
def query_warehouse_inventory(sku: str, warehouse: str = None) -> dict:
record = INVENTORY_DB.get(sku)
if not record:
return {"error": "SKU_NOT_FOUND", "message": f"SKU '{sku}' not in WMS."}
return record
def track_shipment(tracking_number: str, carrier: str) -> dict:
record = TRACKING_DB.get(tracking_number)
if not record:
return {"error": "INVALID_TRACKING",
"message": f"Tracking '{tracking_number}' not found."}
return record
def get_contract_pricing(customer: str, sku: str, quantity: int = 1) -> dict:
record = PRICING_DB.get((sku, customer))
if not record:
return {"error": "NO_CONTRACT",
"message": f"No contract for {customer} + {sku}."}
return record
def check_quality_hold_status(sku: str, lot_number: str = None) -> dict:
record = QUALITY_DB.get(sku)
if not record:
return {"error": "SKU_NOT_FOUND", "message": f"SKU '{sku}' not found."}
return record
def draft_customer_notification(customer: str, exception_type: str,
summary: str, resolution: str,
new_eta: str = None) -> dict:
subject_map = {
"shipment_delay": f"Update on Your Order — Revised Delivery Date",
"pricing_discrepancy": f"Pricing Correction Notice",
"partial_delivery": f"Partial Shipment Update",
"quality_hold": f"Product Availability Update",
}
subject = subject_map.get(exception_type, "Order Update")
eta_line = f"\n\nRevised delivery date: {new_eta}" if new_eta else ""
body = (f"Dear {customer},\n\n{summary}\n\n"
f"Resolution: {resolution}{eta_line}\n\n"
f"We apologize for any inconvenience. Please contact your account "
f"manager if you have questions.\n\nBest regards,\nOperations Team")
return {"notification_id": f"NOT-{exception_type[:3].upper()}-001",
"draft_text": body, "suggested_subject_line": subject}
// mock_tools.ts — 6 tools for order exception investigation
const ORDER_DB: Record<string, any> = {
"PO-2024-11234": {
po_number: "PO-2024-11234", customer: "Acme Manufacturing",
status: "shipped", total_value: 24750.00,
line_items: [{ line: 1, sku: "SKU-4892", qty_ordered: 50,
qty_shipped: 50, qty_delivered: 0, unit_price: 495.00,
tracking_number: "1Z999AA10123456784" }],
},
"PO-2024-11890": {
po_number: "PO-2024-11890", customer: "Summit Industrial Supply",
status: "quality_hold", total_value: 25000.00,
line_items: [{ line: 1, sku: "SKU-7721", qty_ordered: 200,
qty_shipped: 0, qty_delivered: 0, unit_price: 125.00,
tracking_number: null }],
},
};
const TRACKING_DB: Record<string, any> = {
// UPS — 18-char "1Z" prefix
"1Z999AA10123456784": {
tracking_number: "1Z999AA10123456784", carrier: "UPS",
status: "exception", exception_detail: "Weather delay — Memphis hub",
original_eta: "2024-03-08", revised_eta: "2024-03-13",
last_location: "Memphis, TN",
},
// FedEx — 12-digit numeric
"794611205847": {
tracking_number: "794611205847", carrier: "FedEx",
status: "in_transit", exception_detail: null,
original_eta: "2024-03-11", revised_eta: "2024-03-11",
last_location: "Indianapolis, IN",
},
// DHL — 10-digit numeric
"1234567890": {
tracking_number: "1234567890", carrier: "DHL",
status: "exception",
exception_detail: "Customs hold — missing commercial invoice",
original_eta: "2024-03-09", revised_eta: "2024-03-15",
last_location: "Cincinnati, OH (CVG hub)",
},
};
const QUALITY_DB: Record<string, any> = {
"SKU-4892": { sku: "SKU-4892", hold_status: "none" },
"SKU-7721": { sku: "SKU-7721", hold_status: "active",
hold_reason: "Supplier calibration issue", expected_resolution: "2024-03-15" },
};
export function getOrderDetails(poNumber: string): any {
return ORDER_DB[poNumber.toUpperCase()] || { error: "NOT_FOUND" };
}
export function queryWarehouseInventory(sku: string): any {
return { sku, available: 120, reserved: 50, location: "Warehouse-East" };
}
export function trackShipment(trackingNumber: string, carrier: string): any {
return TRACKING_DB[trackingNumber] || { error: "INVALID_TRACKING" };
}
export function getContractPricing(customer: string, sku: string, qty = 1): any {
if (customer === "Acme Manufacturing" && sku === "SKU-4892")
return { sku, customer, unit_price: 495.00, tier: "tier_2", contract_id: "CONTRACT-ACME-2024" };
return { error: "NO_CONTRACT" };
}
export function checkQualityHoldStatus(sku: string): any {
return QUALITY_DB[sku] || { error: "SKU_NOT_FOUND" };
}
export function draftCustomerNotification(customer: string, excType: string, summary: string, resolution: string, newEta?: string): any {
return { notification_id: "NOT-001",
draft_text: `Dear ${customer},\n\n${summary}\n\nResolution: ${resolution}${newEta ? `\n\nRevised ETA: ${newEta}` : ""}\n\nBest regards,\nOperations Team`,
suggested_subject_line: `Order Update — ${excType.replace("_", " ")}` };
}
Complete Solution
"""agent.py — Order Exception Resolution ReAct Agent (Capstone 3-B)
Usage:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
"""
import json
import anthropic
from mock_tools import (
get_order_details, query_warehouse_inventory, track_shipment,
get_contract_pricing, check_quality_hold_status,
draft_customer_notification,
)
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
TOOL_HANDLERS = {
"get_order_details": lambda a: get_order_details(a["po_number"]),
"query_warehouse_inventory": lambda a: query_warehouse_inventory(
a["sku"], a.get("warehouse")),
"track_shipment": lambda a: track_shipment(
a["tracking_number"], a["carrier"]),
"get_contract_pricing": lambda a: get_contract_pricing(
a["customer"], a["sku"], a.get("quantity", 1)),
"check_quality_hold_status": lambda a: check_quality_hold_status(
a["sku"], a.get("lot_number")),
"draft_customer_notification": lambda a: draft_customer_notification(
a["customer"], a["exception_type"], a["summary"],
a["resolution"], a.get("new_eta")),
}
TOOLS = [
{"name": "get_order_details", "description": "Get order details from ERP by PO number.",
"input_schema": {"type": "object", "properties": {"po_number": {"type": "string"}}, "required": ["po_number"]}},
{"name": "query_warehouse_inventory", "description": "Check warehouse inventory for a SKU.",
"input_schema": {"type": "object", "properties": {"sku": {"type": "string"}, "warehouse": {"type": "string"}}, "required": ["sku"]}},
{"name": "track_shipment", "description": "Get carrier tracking status.",
"input_schema": {"type": "object", "properties": {"tracking_number": {"type": "string"}, "carrier": {"type": "string"}}, "required": ["tracking_number", "carrier"]}},
{"name": "get_contract_pricing", "description": "Verify contract pricing for customer+SKU.",
"input_schema": {"type": "object", "properties": {"customer": {"type": "string"}, "sku": {"type": "string"}, "quantity": {"type": "integer"}}, "required": ["customer", "sku"]}},
{"name": "check_quality_hold_status", "description": "Check if a SKU has a quality hold.",
"input_schema": {"type": "object", "properties": {"sku": {"type": "string"}, "lot_number": {"type": "string"}}, "required": ["sku"]}},
{"name": "draft_customer_notification", "description": "Draft a customer notification email about the exception resolution.",
"input_schema": {"type": "object", "properties": {
"customer": {"type": "string"}, "exception_type": {"type": "string"},
"summary": {"type": "string"}, "resolution": {"type": "string"},
"new_eta": {"type": "string"}}, "required": ["customer", "exception_type", "summary", "resolution"]}},
]
SYSTEM_PROMPT = """You are a B2B order exception resolution agent using the \
ReAct (Reason + Act) pattern. Before each tool call, write a Thought \
explaining your reasoning.
Investigation Strategy by Exception Type:
- SHIPMENT DELAY: Start with order details → carrier tracking → check quality hold → draft notification
- PRICING DISCREPANCY: Start with order details → contract pricing → compare and calculate difference → draft notification
- PARTIAL DELIVERY: Start with order details → check each line item → warehouse inventory for missing items → draft notification
- QUALITY HOLD: Start with quality hold status → order details → warehouse for alternatives → draft notification
Rules:
- Always start by getting the order details to understand what was ordered.
- Adapt your investigation based on what you find — if carrier says "delivered" but customer says not received, check warehouse records.
- If a tool returns an error (e.g., CARRIER_UNAVAILABLE), note the failure and continue investigating with other tools.
- After diagnosing root cause, ALWAYS draft a customer notification.
- Stay professional and neutral — investigate facts, not blame.
- For pricing discrepancies, always verify against the contract before concluding."""
def process_tool_calls(response) -> list:
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
try:
result = handler(block.input) if handler else {"error": "UNKNOWN_TOOL"}
except Exception as e:
result = {"error": "SYSTEM_ERROR", "message": str(e)}
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": json.dumps(result)})
return results
def investigate_exception(exception_json: str) -> str:
history = [{"role": "user",
"content": f"Investigate this order exception:\n\n{exception_json}"}]
MAX_ITERATIONS = 15
iteration = 0
while iteration < MAX_ITERATIONS:
iteration += 1
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})
history.append({"role": "user", "content": process_tool_calls(response)})
continue
history.append({"role": "assistant", "content": response.content})
return "\n".join(b.text for b in response.content if hasattr(b, "text"))
if iteration >= MAX_ITERATIONS:
print(f"Warning: Reached maximum {MAX_ITERATIONS} iterations")
return "[WARN] Agent reached maximum iterations without resolving. Manual review required."
def main():
print("=" * 60)
print(" Order Exception Resolution Agent — Capstone 3-B")
print(" Type 'demo' for sample exception, or 'quit' to exit.")
print("=" * 60)
sample = json.dumps({
"exception_id": "EXC-2024-0456",
"po_number": "PO-2024-11234",
"exception_type": "shipment_delay",
"reported_by": "customer",
"reported_date": "2024-03-10",
"description": "Customer reports order not received by expected delivery date of 2024-03-08.",
}, indent=2)
while True:
cmd = input("\nInput: ").strip()
if not cmd: continue
if cmd.lower() in ("quit", "exit", "q"): break
if cmd.lower() == "demo":
cmd = sample
print(f"Using sample exception:\n{cmd}\n")
try:
print(f"\nAgent:\n{investigate_exception(cmd)}")
except Exception as e:
print(f"\n[Error] {e}")
if __name__ == "__main__":
main()
// agent.ts — Order Exception Resolution ReAct Agent (Capstone 3-B)
// Usage: export ANTHROPIC_API_KEY=... && npx ts-node agent.ts
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "readline";
import {
getOrderDetails, queryWarehouseInventory, trackShipment,
getContractPricing, checkQualityHoldStatus, draftCustomerNotification,
} from "./mock_tools";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const HANDLERS: Record<string, (a: any) => any> = {
get_order_details: (a) => getOrderDetails(a.po_number),
query_warehouse_inventory: (a) => queryWarehouseInventory(a.sku),
track_shipment: (a) => trackShipment(a.tracking_number, a.carrier),
get_contract_pricing: (a) => getContractPricing(a.customer, a.sku, a.quantity),
check_quality_hold_status: (a) => checkQualityHoldStatus(a.sku),
draft_customer_notification: (a) => draftCustomerNotification(
a.customer, a.exception_type, a.summary, a.resolution, a.new_eta),
};
const TOOLS: Anthropic.Tool[] = [
{ name: "get_order_details", description: "Get order details from ERP.",
input_schema: { type: "object" as const, properties: { po_number: { type: "string" } }, required: ["po_number"] } },
{ name: "query_warehouse_inventory", description: "Check inventory for SKU.",
input_schema: { type: "object" as const, properties: { sku: { type: "string" } }, required: ["sku"] } },
{ name: "track_shipment", description: "Get carrier tracking.",
input_schema: { type: "object" as const, properties: { tracking_number: { type: "string" }, carrier: { type: "string" } }, required: ["tracking_number", "carrier"] } },
{ name: "get_contract_pricing", description: "Verify contract pricing.",
input_schema: { type: "object" as const, properties: { customer: { type: "string" }, sku: { type: "string" }, quantity: { type: "integer" } }, required: ["customer", "sku"] } },
{ name: "check_quality_hold_status", description: "Check quality holds.",
input_schema: { type: "object" as const, properties: { sku: { type: "string" } }, required: ["sku"] } },
{ name: "draft_customer_notification", description: "Draft customer email.",
input_schema: { type: "object" as const, properties: { customer: { type: "string" }, exception_type: { type: "string" }, summary: { type: "string" }, resolution: { type: "string" }, new_eta: { type: "string" } }, required: ["customer", "exception_type", "summary", "resolution"] } },
];
const SYSTEM_PROMPT = `You are a B2B order exception resolution agent using the \
ReAct (Reason + Act) pattern. Before each tool call, write a Thought \
explaining your reasoning.
Investigation Strategy by Exception Type:
- SHIPMENT DELAY: Start with order details → carrier tracking → check quality hold → draft notification
- PRICING DISCREPANCY: Start with order details → contract pricing → compare and calculate difference → draft notification
- PARTIAL DELIVERY: Start with order details → check each line item → warehouse inventory for missing items → draft notification
- QUALITY HOLD: Start with quality hold status → order details → warehouse for alternatives → draft notification
Rules:
- Always start by getting the order details to understand what was ordered.
- Adapt your investigation based on what you find — if carrier says "delivered" but customer says not received, check warehouse records.
- If a tool returns an error (e.g., CARRIER_UNAVAILABLE), note the failure and continue investigating with other tools.
- After diagnosing root cause, ALWAYS draft a customer notification.
- Stay professional and neutral — investigate facts, not blame.
- For pricing discrepancies, always verify against the contract before concluding.`;
async function investigate(exceptionJson: string): Promise<string> {
const history: Anthropic.MessageParam[] = [{ role: "user", content: `Investigate:\n\n${exceptionJson}` }];
const MAX_ITERATIONS = 15;
let iteration = 0;
while (iteration < MAX_ITERATIONS) {
iteration++;
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 => ({ type: "tool_result" as const, tool_use_id: b.id,
content: JSON.stringify(HANDLERS[b.name]?.(b.input) || { error: "UNKNOWN" }) }));
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");
}
if (iteration >= MAX_ITERATIONS) {
console.warn(`Warning: Reached maximum ${MAX_ITERATIONS} iterations`);
}
return "[WARN] Agent reached maximum iterations without resolving. Manual review required.";
}
async function main() {
console.log("Order Exception Resolution Agent — Capstone 3-B");
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 exc = input.trim() === "demo" ? JSON.stringify({
exception_id: "EXC-2024-0456", po_number: "PO-2024-11234",
exception_type: "shipment_delay", description: "Order not received by expected date." }) : input.trim();
try { console.log("\nAgent:\n" + await investigate(exc)); } catch(e: any) { console.log(`[Error] ${e.message}`); }
ask();
});
ask();
}
main();
You built a ReAct agent with 6 tools that dynamically investigates order exceptions. The system prompt provides investigation strategies per exception type, but the agent adapts in real time: if the carrier tracking reveals a quality hold, the agent pivots to the QA system. After diagnosis, it always drafts a customer notification with the root cause and proposed resolution.
Verify Everything Works
Run this end-to-end verification script to confirm all components work together. Create a file called e2e_test.py:
# e2e_test.py — Verify all tools and agent components
from mock_tools import (get_order_details, query_warehouse_inventory,
track_shipment, get_contract_pricing, check_quality_hold_status,
draft_customer_notification)
# 1. ERP order lookup works
order = get_order_details("PO-2024-11234")
assert "error" not in order, "Order lookup failed"
assert order["customer"] == "Acme Manufacturing"
print("ERP: OK — order found for Acme Manufacturing")
# 2. Carrier tracking works
tracking = track_shipment("1Z999AA10123456784", "UPS")
assert tracking["status"] == "exception"
print(f"Carrier: OK — status: {tracking['status']} ({tracking['exception_detail']})")
# 3. Warehouse inventory works
inv = query_warehouse_inventory("SKU-4892")
assert inv["available"] == 120
print(f"WMS: OK — {inv['available']} units available at {inv['location']}")
# 4. Contract pricing works
pricing = get_contract_pricing("Acme Manufacturing", "SKU-4892")
assert pricing["unit_price"] == 495.00
print(f"Pricing: OK — ${pricing['unit_price']}/unit (tier: {pricing['tier']})")
# 5. Quality hold check works
qa = check_quality_hold_status("SKU-4892")
assert qa["hold_status"] == "none"
print(f"QA Hold: OK — hold_status: {qa['hold_status']}")
# 6. Customer notification draft works
notif = draft_customer_notification("Acme Manufacturing", "shipment_delay",
"Weather delay at Memphis hub", "Revised ETA: Mar 13", "2024-03-13")
assert "notification_id" in notif
print(f"Comms: OK — draft created ({notif['notification_id']})")
# 7. Error handling works
not_found = get_order_details("PO-FAKE-999")
assert "error" in not_found
print(f"Errors: OK — graceful error for unknown PO")
print("\nAll checks passed. Run 'python agent.py' and type 'demo' to test the full agent.")
Run command:
Expected output:
All 7 checks passed! Your Order Exception Resolution Agent is fully functional. You have a ReAct agent that dynamically investigates shipment delays, pricing discrepancies, partial deliveries, and quality holds across 6 different mock backend systems.
Testing Guide
| Type | Scenario | Expected Behavior |
|---|---|---|
| HAPPY | Shipment delay (weather) | Traces shipment, finds carrier exception, drafts notification with revised ETA |
| HAPPY | Pricing discrepancy on invoice | Compares order price vs contract, identifies mismatch, recommends credit |
| HAPPY | Partial delivery reported | Checks line items, finds backordered items, checks inventory for reshipment |
| HAPPY | Quality hold on SKU-7721 | Discovers active hold, reports expected resolution date, notifies customer |
| HAPPY | Confirmed but not shipped order | Checks inventory availability, provides expected ship date |
| EDGE | Carrier API returns CARRIER_UNAVAILABLE | Notes failure, continues with ERP and warehouse data, flags for manual follow-up |
| EDGE | Exception has both pricing AND delay issues | Investigates both branches and addresses each in the resolution |
| EDGE | Customer has no contract on file | Falls back to list pricing comparison |
| ADVERSARIAL | Exception description blames company aggressively | Stays neutral, investigates facts, drafts professional response |
| ADVERSARIAL | Exception for non-existent PO | Reports NOT_FOUND, does not fabricate order details |
Troubleshooting
Common issues and how to fix them:
ModuleNotFoundError: No module named 'anthropic'
You have not installed the Anthropic SDK. Make sure your virtual environment is activated (source venv/bin/activate on Unix, venv\Scripts\activate on Windows), then run pip install "anthropic>=0.30.0".
AuthenticationError: Invalid API key
Your ANTHROPIC_API_KEY environment variable is not set or contains an invalid key. Verify with echo $ANTHROPIC_API_KEY (Linux/Mac) or echo %ANTHROPIC_API_KEY% (Windows). The key should start with sk-ant-. Re-export it: export ANTHROPIC_API_KEY=sk-ant-... (Unix) or set ANTHROPIC_API_KEY=sk-ant-... (Windows).
ImportError: cannot import name 'get_order_details' 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 6 function definitions. Also verify your terminal is cd’d into the project directory.
Agent loops without resolving (hits 15 iterations)
If the agent keeps calling tools without reaching a conclusion: (1) check that your ORDER_DB in mock_tools.py contains entries for the PO number you are testing, (2) verify the system prompt includes investigation strategies for the exception type, and (3) confirm the draft_customer_notification tool is in the TOOLS list so the agent can finalize its investigation. Lower the iteration cap to 10 during debugging to fail faster.
JSONDecodeError when pasting exception input
The agent expects valid JSON input. Make sure your exception JSON has no trailing commas, uses double quotes (not single quotes), and is pasted as a single line. Use the demo command first to verify the agent works before trying custom inputs.
Agent calls the same tool repeatedly
This usually means the system prompt is not guiding the agent to a conclusion. Check that the investigation strategy section includes explicit resolution paths (e.g., “After diagnosing root cause, ALWAYS draft a customer notification”). Also verify that tool results are being returned in the message history correctly — if tool results are missing, Claude will re-request the same tool.
Windows: 'python3' is not recognized
On Windows, use python instead of python3. If that also fails, verify Python is installed and added to your PATH: python --version should show 3.10 or higher. You can download Python from python.org.
Compliance Notes
Exception investigations may expose sensitive commercial data: contract pricing, customer credit terms, inventory levels, and supplier quality issues. Production considerations:
- Customer data isolation: Exception investigations for Customer A must never expose Customer B’s contract pricing or order data. Enforce per-customer access controls at the tool level.
- Quality hold confidentiality: Quality hold information (supplier issues, defect details) is competitively sensitive. Customer notifications should state “product availability issue” rather than exposing supplier names or defect specifics.
- Draft notification review: All customer-facing drafts should be reviewed by a human before sending. The agent drafts; a human approves and sends.
- Audit trail: Every tool call in the investigation trace is an access event. Log with timestamp, user identity, and data accessed for compliance and SLA dispute resolution.
Each exception investigation involves 4–6 tool calls with Thought traces. Typical consumption: 4,000–6,000 tokens per investigation. At Sonnet pricing, budget ~$0.03–$0.05 per exception. For 25 exceptions/day, that’s ~$0.75–$1.25/day. Compared to 20 minutes of analyst time per exception ($8–$12 in labor), the ROI is immediate.
Going Further
These extensions are all [OPTIONAL]. The core capstone is complete once you pass the Verify Everything section above. Try these if you have extra time or want to deepen your skills:
- [OPTIONAL] Cost impact calculator — Add a
calculate_cost_impactstretch tool that estimates financial impact: expedited shipping cost, discount credits, SLA penalties. This exercises adding a 7th tool to an existing agent. - [OPTIONAL] Multi-exception batch processing — Accept a list of exceptions and process them in sequence, generating a summary report. Uses the Message Batches API (M25) for cost savings.
- [OPTIONAL] Exception pattern detection — Track exceptions over time and surface patterns (e.g., “Memphis hub delays are 3x more common in March”). Introduces episodic memory concepts from M11.
- [OPTIONAL] Automatic SLA check — Compare actual delivery against contract SLA commitments and automatically flag violations for credit processing.
- [OPTIONAL] Slack/email integration — Instead of drafting notifications for human review, send them via a real channel integration.
- [OPTIONAL] Resolution template library — Build a library of resolution templates by exception type and severity, so the agent produces consistent messaging.