Capstone 1 — Domain B: Order Status Bot
Build your first agent: a single-tool conversational assistant that retrieves B2B purchase order status from a mock ERP and provides natural language summaries with ETAs.
Project Brief
In B2B ecommerce, a customer success representative fields 50–80 “Where’s my order?” calls per day from buyers at manufacturing companies, distributors, and procurement offices. Each call requires logging into the ERPEnterprise Resource Planning — the central business system that tracks orders, inventory, financials, and logistics. Examples: SAP, Oracle NetSuite, Microsoft Dynamics. In B2B, the ERP is the single source of truth for order status., navigating to the order screen, finding the PO numberPurchase Order number — the unique identifier assigned by the buyer when placing an order. In B2B, the PO number is the primary key for all order inquiries, not a customer name or email., cross-referencing shipment tracking, and manually composing a status summary. Each inquiry takes 5–10 minutes.
The pain is amplified by B2B complexity: orders have multiple line items, each potentially at different fulfillment stages. A single PO might have 3 SKUs — one shipped, one in production, one backordered. The rep must check each line separately, then combine the information into a coherent update. When they get it wrong, the buyer loses trust and the account is at risk.
Your agent replaces the ERP-lookup step: the user provides a PO number, the agent calls a mock ERP tool, and returns a clear, structured summary with per-line-item status, tracking numbers, and estimated delivery dates. One query, immediate answer, no ERP navigation required.
A conversational agent with one tool (get_order_status) that:
- Accepts a purchase order number from the user
- Calls the tool to retrieve mock ERP order data
- Summarizes the order in plain English with per-line-item status and ETAs
- Handles errors gracefully (invalid format, not found, access denied)
- 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 (track_shipment) to look up carrier-level tracking detail for individual shipments.
Environment Setup
Complete these modules before starting: M03 (Prompts & System Messages), M04 (Structured Output), M05 (Function Calling), and M08 (Conversation Management). These modules teach the core concepts (tool definitions, stop_reason handling, system prompts, multi-turn history) that this capstone combines into a working agent. This is a ★☆☆☆☆ capstone — the easiest in the series.
Version Requirements
- Python 3.10+ (for
list | Noneunion syntax andmatchstatements) - Node.js 18+ (for native
fetchand top-levelawaitsupport)
Python Setup
mkdir order-status-bot && cd order-status-bot
python -m venv venv && source venv/bin/activate
pip install "anthropic>=0.30.0" pytest
export ANTHROPIC_API_KEY=your-key-here
mkdir order-status-bot && cd order-status-bot
python -m venv venv && venv\Scripts\activate
pip install "anthropic>=0.30.0" pytest
set ANTHROPIC_API_KEY=your-key-here
Node.js / TypeScript Setup
mkdir order-status-bot && cd order-status-bot
npm init -y
npm install @anthropic-ai/sdk typescript ts-node
export ANTHROPIC_API_KEY=your-key-here
mkdir order-status-bot && cd order-status-bot
npm init -y
npm install @anthropic-ai/sdk typescript ts-node
set ANTHROPIC_API_KEY=your-key-here
Verify Installation
python -c "import anthropic; print('anthropic', anthropic.__version__)"
File Structure
You will create the following files. All files live in the same directory so imports resolve correctly.
order-status-bot/
├── mock_tools.py # Mock ERP order data + tool functions
├── agent.py # Main agent with tool-use loop
├── mock_tools.ts # TypeScript mock data
├── agent.ts # TypeScript agent
├── test_agent.py # Test suite (pytest)
├── .env.example # ANTHROPIC_API_KEY=
└── requirements.txt # anthropic>=0.30.0, pytest>=7.0.0
requirements.txt
anthropic>=0.30.0
pytest>=7.0.0
Domain Glossary
Architecture
Mock Data Specification
// PO-2024-11234 — Shipped (single shipment)
{
"po_number": "PO-2024-11234",
"customer": "Acme Manufacturing",
"order_date": "2024-02-15",
"status": "shipped",
"total_value": 24750.00,
"currency": "USD",
"line_items": [
{ "line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 50, "unit_price": 495.00, "status": "shipped",
"tracking_number": "1Z999AA10123456784" }
],
"shipments": [
{ "tracking_number": "1Z999AA10123456784", "carrier": "UPS",
"ship_date": "2024-03-05", "estimated_delivery": "2024-03-12",
"status": "in-transit" }
],
"payment_terms": "Net 45",
"invoice_number": null,
"notes": "Customer requested consolidated shipment."
}
// PO-2024-11567 — Partial (multi-line, mixed status)
{
"po_number": "PO-2024-11567",
"customer": "Beta Industrial",
"order_date": "2024-02-20",
"status": "in-production",
"total_value": 61250.00,
"currency": "USD",
"line_items": [
{ "line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 100, "unit_price": 465.00, "status": "shipped",
"tracking_number": "783412345678" },
{ "line": 2, "sku": "SKU-7721", "description": "Pressure Gauge - Digital, 0-300 PSI",
"quantity": 50, "unit_price": 125.00, "status": "in-production",
"tracking_number": null },
{ "line": 3, "sku": "SKU-2210", "description": "Stainless Steel Fitting Kit - 4 inch",
"quantity": 100, "unit_price": 85.00, "status": "confirmed",
"tracking_number": null }
],
"shipments": [
{ "tracking_number": "783412345678", "carrier": "FedEx",
"ship_date": "2024-03-08", "estimated_delivery": "2024-03-15",
"status": "in-transit" }
],
"payment_terms": "Net 30",
"invoice_number": null,
"notes": "Split shipment authorized. Line 1 shipped first."
}
// PO-2024-10890 — Delivered and invoiced
{
"po_number": "PO-2024-10890",
"customer": "Acme Manufacturing",
"order_date": "2024-01-10",
"status": "invoiced",
"total_value": 9900.00,
"currency": "USD",
"line_items": [
{ "line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 20, "unit_price": 495.00, "status": "delivered",
"tracking_number": "1Z999CC30345678901" }
],
"shipments": [
{ "tracking_number": "1Z999CC30345678901", "carrier": "UPS",
"ship_date": "2024-01-18", "estimated_delivery": "2024-01-25",
"status": "delivered" }
],
"payment_terms": "Net 45",
"invoice_number": "INV-2024-5567",
"notes": null
}
// PO-2024-99998 — Restricted (ACCESS_DENIED test case)
// Looking up this PO returns an ACCESS_DENIED error.
// Use this to test your agent's error handling for permission errors.
{
"po_number": "PO-2024-99998",
"access": "RESTRICTED"
}
You have 3 test orders covering the main scenarios: a single-line shipped order, a multi-line order with mixed statuses (one shipped, one in production, one confirmed), and a completed order that’s been delivered and invoiced. There is also a restricted PO (PO-2024-99998) that triggers an ACCESS_DENIED error for testing permission handling. The agent must handle all these patterns and present each clearly.
Step-by-Step Build
Step 1: Create the Mock ERP Data
What: Build a mock ERP database that returns order records for test PO numbers, including multi-line orders and error cases. Why: Using mock data lets you develop and test the full agent loop without needing a real ERP system (SAP, NetSuite, etc.), API credentials, or rate limits. This is a standard pattern for agent development — build the agent logic first, swap in real APIs later.
Create a new file called mock_tools.py (Python) or mock_tools.ts (TypeScript) and paste the complete code below.
"""mock_tools.py — Mock ERP order status API for Capstone 1-B."""
import re
PO_PATTERN = re.compile(r"^PO-\d{4}-\d{5}$")
ORDER_DB = {
"PO-2024-11234": {
"po_number": "PO-2024-11234", "customer": "Acme Manufacturing",
"order_date": "2024-02-15", "status": "shipped", "total_value": 24750.00,
"currency": "USD",
"line_items": [
{"line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 50, "unit_price": 495.00, "status": "shipped",
"tracking_number": "1Z999AA10123456784"},
],
"shipments": [
{"tracking_number": "1Z999AA10123456784", "carrier": "UPS",
"ship_date": "2024-03-05", "estimated_delivery": "2024-03-12",
"status": "in-transit"},
],
"payment_terms": "Net 45", "invoice_number": None,
"notes": "Customer requested consolidated shipment.",
},
"PO-2024-11567": {
"po_number": "PO-2024-11567", "customer": "Beta Industrial",
"order_date": "2024-02-20", "status": "in-production", "total_value": 61250.00,
"currency": "USD",
"line_items": [
{"line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 100, "unit_price": 465.00, "status": "shipped",
"tracking_number": "783412345678"},
{"line": 2, "sku": "SKU-7721", "description": "Pressure Gauge - Digital, 0-300 PSI",
"quantity": 50, "unit_price": 125.00, "status": "in-production",
"tracking_number": None},
{"line": 3, "sku": "SKU-2210", "description": "Stainless Steel Fitting Kit - 4 inch",
"quantity": 100, "unit_price": 85.00, "status": "confirmed",
"tracking_number": None},
],
"shipments": [
{"tracking_number": "783412345678", "carrier": "FedEx",
"ship_date": "2024-03-08", "estimated_delivery": "2024-03-15",
"status": "in-transit"},
],
"payment_terms": "Net 30", "invoice_number": None,
"notes": "Split shipment authorized. Line 1 shipped first.",
},
"PO-2024-10890": {
"po_number": "PO-2024-10890", "customer": "Acme Manufacturing",
"order_date": "2024-01-10", "status": "invoiced", "total_value": 9900.00,
"currency": "USD",
"line_items": [
{"line": 1, "sku": "SKU-4892", "description": "Industrial Valve Assembly - 4 inch",
"quantity": 20, "unit_price": 495.00, "status": "delivered",
"tracking_number": "1Z999CC30345678901"},
],
"shipments": [
{"tracking_number": "1Z999CC30345678901", "carrier": "UPS",
"ship_date": "2024-01-18", "estimated_delivery": "2024-01-25",
"status": "delivered"},
],
"payment_terms": "Net 45", "invoice_number": "INV-2024-5567",
"notes": None,
},
}
# PO numbers that require elevated permissions (ACCESS_DENIED test)
RESTRICTED_POS = {"PO-2024-99998"}
# ── Stretch tool: carrier tracking ─────────────────────────────
TRACKING_DB = {
"1Z999AA10123456784": {
"tracking_number": "1Z999AA10123456784", "carrier": "UPS",
"status": "in-transit", "current_location": "Louisville, KY",
"estimated_delivery": "2024-03-12",
"delivery_history": [
{"timestamp": "2024-03-05T14:00:00Z", "location": "Chicago, IL", "event": "Picked up"},
{"timestamp": "2024-03-06T08:30:00Z", "location": "Louisville, KY", "event": "In transit"},
],
},
"783412345678": {
"tracking_number": "783412345678", "carrier": "FedEx",
"status": "in-transit", "current_location": "Memphis, TN",
"estimated_delivery": "2024-03-15",
"delivery_history": [
{"timestamp": "2024-03-08T10:00:00Z", "location": "Detroit, MI", "event": "Picked up"},
{"timestamp": "2024-03-09T06:15:00Z", "location": "Memphis, TN", "event": "In transit — hub scan"},
],
},
"1Z999CC30345678901": {
"tracking_number": "1Z999CC30345678901", "carrier": "UPS",
"status": "delivered", "current_location": "Acme Manufacturing, Cleveland, OH",
"estimated_delivery": "2024-01-25",
"delivery_history": [
{"timestamp": "2024-01-18T09:00:00Z", "location": "Chicago, IL", "event": "Picked up"},
{"timestamp": "2024-01-20T11:30:00Z", "location": "Toledo, OH", "event": "In transit"},
{"timestamp": "2024-01-22T14:45:00Z", "location": "Cleveland, OH", "event": "Delivered — signed by J. Martinez"},
],
},
}
def get_order_status(po_number: str) -> dict:
"""Look up a B2B order by PO number."""
# Normalize: accept lowercase
po_number = po_number.upper().strip()
if not PO_PATTERN.match(po_number):
return {"error": "INVALID_FORMAT",
"message": f"'{po_number}' is not valid. Expected: PO-YYYY-NNNNN"}
if po_number in RESTRICTED_POS:
return {"error": "ACCESS_DENIED",
"message": "You do not have permission to access this order. Contact your account manager."}
record = ORDER_DB.get(po_number)
if not record:
return {"error": "NOT_FOUND",
"message": f"No order found for '{po_number}'."}
return record
def track_shipment(tracking_number: str, carrier: str) -> dict:
"""Look up carrier-level tracking detail (stretch tool)."""
record = TRACKING_DB.get(tracking_number)
if not record:
return {"error": "INVALID_TRACKING",
"message": f"Tracking number '{tracking_number}' not found."}
return record
// mock_tools.ts — Mock ERP order status API for Capstone 1-B
const PO_PATTERN = /^PO-\d{4}-\d{5}$/;
const ORDER_DB: Record<string, any> = {
"PO-2024-11234": {
po_number: "PO-2024-11234", customer: "Acme Manufacturing",
order_date: "2024-02-15", status: "shipped", total_value: 24750.00,
currency: "USD",
line_items: [
{ line: 1, sku: "SKU-4892", description: "Industrial Valve Assembly - 4 inch",
quantity: 50, unit_price: 495.00, status: "shipped",
tracking_number: "1Z999AA10123456784" },
],
shipments: [
{ tracking_number: "1Z999AA10123456784", carrier: "UPS",
ship_date: "2024-03-05", estimated_delivery: "2024-03-12",
status: "in-transit" },
],
payment_terms: "Net 45", invoice_number: null,
notes: "Customer requested consolidated shipment.",
},
"PO-2024-11567": {
po_number: "PO-2024-11567", customer: "Beta Industrial",
order_date: "2024-02-20", status: "in-production", total_value: 61250.00,
currency: "USD",
line_items: [
{ line: 1, sku: "SKU-4892", description: "Industrial Valve Assembly - 4 inch",
quantity: 100, unit_price: 465.00, status: "shipped",
tracking_number: "783412345678" },
{ line: 2, sku: "SKU-7721", description: "Pressure Gauge - Digital, 0-300 PSI",
quantity: 50, unit_price: 125.00, status: "in-production",
tracking_number: null },
{ line: 3, sku: "SKU-2210", description: "Stainless Steel Fitting Kit - 4 inch",
quantity: 100, unit_price: 85.00, status: "confirmed",
tracking_number: null },
],
shipments: [
{ tracking_number: "783412345678", carrier: "FedEx",
ship_date: "2024-03-08", estimated_delivery: "2024-03-15",
status: "in-transit" },
],
payment_terms: "Net 30", invoice_number: null,
notes: "Split shipment authorized.",
},
"PO-2024-10890": {
po_number: "PO-2024-10890", customer: "Acme Manufacturing",
order_date: "2024-01-10", status: "invoiced", total_value: 9900.00,
currency: "USD",
line_items: [
{ line: 1, sku: "SKU-4892", description: "Industrial Valve Assembly - 4 inch",
quantity: 20, unit_price: 495.00, status: "delivered",
tracking_number: "1Z999CC30345678901" },
],
shipments: [
{ tracking_number: "1Z999CC30345678901", carrier: "UPS",
ship_date: "2024-01-18", estimated_delivery: "2024-01-25",
status: "delivered" },
],
payment_terms: "Net 45", invoice_number: "INV-2024-5567", notes: null,
},
};
// PO numbers that require elevated permissions (ACCESS_DENIED test)
const RESTRICTED_POS = new Set(["PO-2024-99998"]);
const TRACKING_DB: Record<string, any> = {
"1Z999AA10123456784": {
tracking_number: "1Z999AA10123456784", carrier: "UPS",
status: "in-transit", current_location: "Louisville, KY",
estimated_delivery: "2024-03-12",
delivery_history: [
{ timestamp: "2024-03-05T14:00:00Z", location: "Chicago, IL", event: "Picked up" },
{ timestamp: "2024-03-06T08:30:00Z", location: "Louisville, KY", event: "In transit" },
],
},
"783412345678": {
tracking_number: "783412345678", carrier: "FedEx",
status: "in-transit", current_location: "Memphis, TN",
estimated_delivery: "2024-03-15",
delivery_history: [
{ timestamp: "2024-03-08T10:00:00Z", location: "Detroit, MI", event: "Picked up" },
{ timestamp: "2024-03-09T06:15:00Z", location: "Memphis, TN", event: "In transit — hub scan" },
],
},
"1Z999CC30345678901": {
tracking_number: "1Z999CC30345678901", carrier: "UPS",
status: "delivered", current_location: "Acme Manufacturing, Cleveland, OH",
estimated_delivery: "2024-01-25",
delivery_history: [
{ timestamp: "2024-01-18T09:00:00Z", location: "Chicago, IL", event: "Picked up" },
{ timestamp: "2024-01-20T11:30:00Z", location: "Toledo, OH", event: "In transit" },
{ timestamp: "2024-01-22T14:45:00Z", location: "Cleveland, OH", event: "Delivered — signed by J. Martinez" },
],
},
};
export function getOrderStatus(poNumber: string): any {
const normalized = poNumber.toUpperCase().trim();
if (!PO_PATTERN.test(normalized))
return { error: "INVALID_FORMAT", message: `'${poNumber}' is not valid. Expected: PO-YYYY-NNNNN` };
if (RESTRICTED_POS.has(normalized))
return { error: "ACCESS_DENIED", message: "You do not have permission to access this order. Contact your account manager." };
return ORDER_DB[normalized] || { error: "NOT_FOUND", message: `No order for '${normalized}'.` };
}
export function trackShipment(trackingNumber: string, carrier: string): any {
const record = TRACKING_DB[trackingNumber];
if (!record)
return { error: "INVALID_TRACKING", message: `Tracking number '${trackingNumber}' not found.` };
return record;
}
Run command (verify the mock data loads):
python -c "from mock_tools import get_order_status; import json; print(json.dumps(get_order_status('PO-2024-11234'), indent=2))"
python -c "from mock_tools import get_order_status; import json; print(json.dumps(get_order_status('PO-2024-11234'), indent=2))"
You should see the full JSON order record for PO-2024-11234 with status "shipped" and one line item. If you see an ImportError, make sure you are running from the directory where mock_tools.py is saved.
ModuleNotFoundError: No module named 'mock_tools'— Make sure you are running the command from the same directory wheremock_tools.pyis saved.SyntaxError: invalid syntax— Confirm you are using Python 3.10+. Runpython --versionto check.- Output shows
None— Check that you used the exact PO numberPO-2024-11234(case-insensitive, but format matters).
Step 2: Build the Agent
What: Create the main agent file with tool definitions, a system prompt, and the agentic tool-use loop. Why: This is where the core pattern from M05 (Function Calling) comes together — you define what tools Claude can use, send messages, check stop_reason, dispatch tool calls, feed results back, and loop until Claude produces a final text response.
Create a new file called agent.py (Python) or agent.ts (TypeScript) and paste the complete code below.
"""agent.py — B2B Order Status Bot (Capstone 1-B)
Usage:
export ANTHROPIC_API_KEY=your-key-here
python agent.py
"""
import json
import anthropic
from mock_tools import get_order_status, track_shipment
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
TOOLS = [
{
"name": "get_order_status",
"description": (
"Look up the current status of a B2B purchase order by PO number. "
"Returns order details including line items, shipment tracking, "
"delivery ETAs, and invoice status. Use when a user asks about "
"an order, PO, or shipment."
),
"input_schema": {
"type": "object",
"properties": {
"po_number": {
"type": "string",
"description": "Purchase order number (format: PO-YYYY-NNNNN)",
}
},
"required": ["po_number"],
},
},
{
"name": "track_shipment",
"description": (
"Get carrier-level tracking detail for a shipment. "
"Use when the user asks for detailed tracking info "
"like current location or delivery history."
),
"input_schema": {
"type": "object",
"properties": {
"tracking_number": {"type": "string", "description": "Carrier tracking number"},
"carrier": {"type": "string", "description": "Carrier name (UPS, FedEx, DHL)"},
},
"required": ["tracking_number", "carrier"],
},
},
]
SYSTEM_PROMPT = """You are a B2B order status assistant for an industrial \
supply company. You help customer success reps quickly check order status.
Rules:
- You can ONLY check status — you cannot modify, cancel, or create orders.
- For multi-line orders, clearly separate each line item's status.
- Always include tracking numbers and estimated delivery dates when available.
- If an order has been invoiced, mention the invoice number.
- Use professional but friendly language appropriate for B2B.
- Format monetary values with dollar signs and two decimal places.
- When a line item is backordered or in production, say so clearly."""
TOOL_HANDLERS = {
"get_order_status": lambda a: get_order_status(a["po_number"]),
"track_shipment": lambda a: track_shipment(a["tracking_number"], a["carrier"]),
}
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 chat(user_message: str, history: list) -> str:
history.append({"role": "user", "content": user_message})
while True:
response = client.messages.create(
model=MODEL, max_tokens=1024,
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"))
def main():
print("=" * 60)
print(" B2B Order Status Bot — Capstone 1-B")
print(" Type a PO number to check status. Type 'quit' to exit.")
print("=" * 60)
history = []
while True:
user_input = input("\nYou: ").strip()
if not user_input: continue
if user_input.lower() in ("quit", "exit", "q"): break
try:
print(f"\nAgent: {chat(user_input, history)}")
except anthropic.APIError as e:
print(f"\n[API Error] {e.message}")
if __name__ == "__main__":
main()
// agent.ts — B2B Order Status Bot (Capstone 1-B)
// Usage: export ANTHROPIC_API_KEY=... && npx ts-node agent.ts
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "readline";
import { getOrderStatus, trackShipment } from "./mock_tools";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const TOOLS: Anthropic.Tool[] = [
{
name: "get_order_status",
description: "Look up B2B purchase order status by PO number. Returns line items, tracking, ETAs.",
input_schema: {
type: "object" as const,
properties: { po_number: { type: "string", description: "PO number (PO-YYYY-NNNNN)" } },
required: ["po_number"],
},
},
{
name: "track_shipment",
description: "Get carrier tracking detail for a shipment.",
input_schema: {
type: "object" as const,
properties: {
tracking_number: { type: "string" },
carrier: { type: "string" },
},
required: ["tracking_number", "carrier"],
},
},
];
const SYSTEM_PROMPT = `You are a B2B order status assistant. Help reps check order status.
Rules: Only check status, never modify orders. For multi-line orders, separate each line item.
Include tracking numbers and ETAs. Use professional B2B language.`;
const HANDLERS: Record<string, (a: any) => any> = {
get_order_status: (a) => getOrderStatus(a.po_number),
track_shipment: (a) => trackShipment(a.tracking_number, a.carrier),
};
async function chat(msg: string, history: Anthropic.MessageParam[]): Promise<string> {
history.push({ role: "user", content: msg });
while (true) {
const resp = await client.messages.create({ model: MODEL, max_tokens: 1024, 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;
}
history.push({ role: "assistant", content: resp.content });
return resp.content.filter((b): b is Anthropic.TextBlock => b.type === "text").map(b => b.text).join("\n");
}
}
async function main() {
console.log("B2B Order Status Bot — Capstone 1-B\nType 'quit' to exit.");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const history: Anthropic.MessageParam[] = [];
const ask = () => rl.question("\nYou: ", async (input) => {
if (["quit","exit","q"].includes(input.trim().toLowerCase())) { rl.close(); return; }
try { console.log(`\nAgent: ${await chat(input.trim(), history)}`); } catch (e: any) { console.log(`[Error] ${e.message}`); }
ask();
});
ask();
}
main();
You should see a natural language summary that separates each line item’s status. The agent clearly identifies which lines are shipped, in production, and confirmed. If the agent does not call the tool, verify the tools parameter is being passed to client.messages.create().
AuthenticationError— YourANTHROPIC_API_KEYis not set or is invalid. On Unix:export ANTHROPIC_API_KEY=sk-ant-...On Windows:set ANTHROPIC_API_KEY=sk-ant-...- Agent does not call the tool — Verify
tools=TOOLSis passed toclient.messages.create(). Without it, Claude cannot see any tools. - Agent produces garbled multi-line output — Check the system prompt instructs Claude to separate line items. The
SYSTEM_PROMPTmust mention handling multi-line orders.
Step 3: Run the Automated Tests
What: Run the test suite to verify the mock tools handle all error paths correctly. Why: Automated tests ensure your mock ERP returns correct data for shipped orders, multi-line orders, invalid formats, not-found POs, and access-denied cases. This gives you confidence before testing the full agent interactively.
Create a new file called test_agent.py in the same directory as mock_tools.py and paste the complete test code from the Testing Guide → Automated Tests section below, then run it with the command shown there. The tests import from the mock_tools module created in Step 1.
Testing Guide
| Type | Scenario | Expected Behavior |
|---|---|---|
| HAPPY | PO-2024-11234 (shipped, single line) | Returns shipped status with UPS tracking and March 12 ETA |
| HAPPY | PO-2024-11567 (multi-line, mixed status) | Clearly separates 3 lines: shipped, in-production, confirmed |
| HAPPY | PO-2024-10890 (delivered + invoiced) | Confirms delivery, mentions invoice INV-2024-5567 |
| HAPPY | Ask about a specific line item after status check | Uses conversation context, no re-query needed |
| HAPPY | Follow-up about tracking for the same order | Agent references prior result or calls track_shipment |
| EDGE | User types “po-2024-11234” (lowercase) | Agent normalizes and retrieves successfully |
| EDGE | PO-2024-99999 (valid format, no match) | Agent reports not found, asks to double-check |
| EDGE | PO-2024-99998 (restricted, access denied) | Agent reports access denied, suggests contacting account manager |
| EDGE | User provides just “11234” | Agent asks for full PO number format |
| ADVERSARIAL | “Cancel PO-2024-11234” | Agent explains it can only check status, not modify orders |
| ADVERSARIAL | Competitor’s PO format “ORD-12345” | Agent explains expected format and asks for correction |
Automated Tests
"""test_agent.py — Tests for the Order Status Bot."""
import pytest
from mock_tools import get_order_status, track_shipment
class TestGetOrderStatus:
def test_shipped_order(self):
result = get_order_status("PO-2024-11234")
assert result["status"] == "shipped"
assert result["customer"] == "Acme Manufacturing"
assert len(result["line_items"]) == 1
def test_multi_line_order(self):
result = get_order_status("PO-2024-11567")
assert len(result["line_items"]) == 3
statuses = [li["status"] for li in result["line_items"]]
assert "shipped" in statuses
assert "in-production" in statuses
def test_invoiced_order(self):
result = get_order_status("PO-2024-10890")
assert result["status"] == "invoiced"
assert result["invoice_number"] == "INV-2024-5567"
def test_invalid_format(self):
result = get_order_status("12345")
assert result["error"] == "INVALID_FORMAT"
def test_not_found(self):
result = get_order_status("PO-2024-99999")
assert result["error"] == "NOT_FOUND"
def test_access_denied(self):
result = get_order_status("PO-2024-99998")
assert result["error"] == "ACCESS_DENIED"
def test_case_insensitive(self):
result = get_order_status("po-2024-11234")
assert result["po_number"] == "PO-2024-11234"
class TestTrackShipment:
def test_valid_tracking(self):
result = track_shipment("1Z999AA10123456784", "UPS")
assert result["status"] == "in-transit"
def test_fedex_tracking(self):
result = track_shipment("783412345678", "FedEx")
assert result["status"] == "in-transit"
def test_delivered_tracking(self):
result = track_shipment("1Z999CC30345678901", "UPS")
assert result["status"] == "delivered"
def test_invalid_tracking(self):
result = track_shipment("INVALID", "UPS")
assert result["error"] == "INVALID_TRACKING"
if __name__ == "__main__":
pytest.main([__file__, "-v"])
Run command:
pip install pytest # if not already installed
python -m pytest test_agent.py -v
All 11 tests should pass. If any fail, check that mock_tools.py from Step 1 is unchanged and in the same directory as test_agent.py.
Verify Everything Works
Run this single end-to-end smoke test to confirm the full pipeline is functional. This non-interactive test sends one query and prints the agent’s response — no manual input needed.
python -c "
from agent import chat
history = []
response = chat('What is the status of PO-2024-11234?', history)
print(response)
assert 'shipped' in response.lower() or 'Shipped' in response, 'Missing shipped status'
assert 'UPS' in response or '1Z999AA' in response, 'Missing carrier info'
print('\n--- ALL CHECKS PASSED ---')
"
python -c "from agent import chat; history = []; response = chat('What is the status of PO-2024-11234?', history); print(response); assert 'shipped' in response.lower() or 'Shipped' in response, 'Missing shipped status'; assert 'UPS' in response or '1Z999AA' in response, 'Missing carrier info'; print('\n--- ALL CHECKS PASSED ---')"
If you see “ALL CHECKS PASSED” your agent is fully operational. You have built a working single-tool B2B Order Status Bot that retrieves PO data from a mock ERP and summarizes it in natural language. The interactive mode (python agent.py) lets you test multi-turn conversations — try asking about PO-2024-11567 to see multi-line order handling. Continue to the Testing Guide below to exercise edge cases and adversarial inputs.
Troubleshooting
Common errors you may encounter and how to fix them:
Cause: The Anthropic SDK is not installed, or you are running Python outside the virtual environment.
Fix: Activate your venv first, then install: pip install "anthropic>=0.30.0"
Cause: The ANTHROPIC_API_KEY environment variable is not set or contains an invalid key.
Fix (Unix): export ANTHROPIC_API_KEY=sk-ant-api03-...
Fix (Windows): set ANTHROPIC_API_KEY=sk-ant-api03-...
Cause: You are running the command from a different directory than where mock_tools.py is saved.
Fix: cd order-status-bot (or wherever you saved the files) before running.
Cause: The tools parameter is missing from client.messages.create(), so Claude does not know any tools exist.
Fix: Verify your chat() function passes tools=TOOLS in the API call.
Cause: The agent loop does not check for stop_reason == "tool_use" and tries to extract text from a tool-use response.
Fix: Make sure the while True loop checks response.stop_reason and only extracts text when it equals "end_turn".
Cause: The tool result is being passed as a raw string instead of being JSON-serialized. Claude expects tool_result.content to be a string.
Fix: Wrap the tool return value with json.dumps(result) before setting it as the content of the tool_result message.
Cause: You are sending too many requests to the Anthropic API in a short time window.
Fix: Wait 30–60 seconds and try again. For production use, add exponential backoff retry logic.
Cause: The input() call in main() is waiting for terminal input. If you are running in an environment without a terminal (e.g., some IDEs), it may appear to hang.
Fix: Run from a real terminal: python agent.py. Alternatively, use the Verify command above for non-interactive testing.
Compliance Notes
B2B order data can contain sensitive commercial information: contract pricing, volume discounts, customer credit terms, and payment data. While not as strictly regulated as HIPAA, there are compliance considerations:
- Contract pricing confidentiality: Never expose one customer’s contract pricing to another customer’s rep. The agent must verify the querying user has access to the specific PO.
- PCI-DSS: If the order system handles credit card data (rare in B2B Net terms, common in B2B prepay), payment fields must be masked. Never return full card numbers or CVVs in tool responses.
- EDI compliance: Many B2B orders arrive via EDIElectronic Data Interchange — standardized formats (X12 850 for POs, 856 for ship notices) used for machine-to-machine B2B commerce. EDI data has specific formatting and audit requirements. (X12 850 format). If the agent ingests EDI data, it must respect the transactional integrity and audit trail requirements of EDI standards.
- Data retention: B2B order records typically must be retained for 7 years for tax and audit purposes. Conversation logs containing order data inherit this retention requirement.
B2B status queries are typically short (one tool call, 500–800 tokens). At Sonnet pricing, each query costs ~$0.003–$0.005. For a team handling 200 queries/day, that’s ~$0.60–$1.00/day. The ROI is clear: replacing 5–10 minutes of manual ERP lookup per query saves ~$2–$4 in labor cost per query.
Going Further
These extensions are all [OPTIONAL]. The capstone is complete without them. They are listed in order of increasing complexity.
- [OPTIONAL] Add the carrier tracking stretch tool — Wire up
track_shipmentso the agent can show package location and delivery history. The mock data is already inmock_tools.py. - [OPTIONAL] Multi-order comparison — Let the user ask “Show all orders for Acme Manufacturing” and return a summary table of multiple POs.
- [OPTIONAL] SLA monitoring — Add logic to flag orders that are approaching or have missed their SLA delivery window.
- [OPTIONAL] Email draft generation — After checking status, offer to draft a customer-facing status update email that the rep can review and send.
- [OPTIONAL] Streaming responses — Switch to
client.messages.stream()for real-time output as the agent processes multi-line orders. - [OPTIONAL] Webhook integration — Add a mock webhook endpoint that receives carrier tracking updates and proactively notifies the agent of delays.
Knowledge Check
Test your understanding of the concepts covered in this capstone. Select an answer and click Check Answer for immediate feedback.
What parameter in client.messages.create() provides tools to Claude?
When Claude wants to call a tool, what value does stop_reason have?
In the tool result message sent back to Claude, what role value is used?
A B2B customer asks about PO-2024-11234 but the PO doesn’t exist in the system. What should the agent do?
Why is conversation history (message array) important for multi-turn B2B order inquiries?