Capstone 1 — Domain C: UCC Filing Lookup Agent
Build your first agent: a single-tool conversational assistant that searches UCC filings for a business and summarizes lien exposure in plain English.
Project Brief
A commercial lender's credit analyst needs to assess existing lien exposure before approving a business loan. Today, this means manually searching Secretary of State websites across multiple states, downloading filing records, interpreting legal language about collateral, and assembling a risk summary. A single search can take 30–60 minutes per state.
The pain is compounded by inconsistency: each state's SOS website has a different interface, different data format, and different search behavior. An analyst searching for "Acme Logistics LLC" in Delaware gets a clean table. The same search in New York returns a fixed-width text file. Texas returns XML. The analyst must normalize all of this mentally.
Your agent replaces the manual search step: the analyst provides a business name and state, the agent queries the SOS database (mock), and returns a clear, plain-English summary of active UCC filingsUniform Commercial Code filings — public records documenting secured commercial transactions (liens on business assets). Filed with state Secretaries of State. A UCC-1 creates a lien; a UCC-3 amends, continues, or terminates one. including who holds liens, what collateral is pledged, and when filings expire.
A conversational agent with one tool (search_ucc_filings) that:
- Accepts a business name and state from the user
- Calls the tool to retrieve mock UCC filing data
- Summarizes results in plain English with actionable insights
- Handles errors gracefully (invalid state, no results, service timeout)
- 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 (get_filing_amendments) to check amendment history for specific filings.
Environment Setup
Complete these modules before starting: M04 (Structured Output), M05 (Function Calling), and M08 (Conversation Management). These modules teach the core concepts (tool definitions, stop_reason handling, multi-turn history) that this capstone combines into a working agent.
Version Requirements
- Python 3.10+ (for
list | Noneunion syntax andmatchstatements) - Node.js 18+ (for native
fetchand top-levelawaitsupport)
Python Setup
# Run each line on its own — works in bash, zsh, cmd.exe, and PowerShell 5.1+
mkdir capstone-1-ucc-lookup
cd capstone-1-ucc-lookup
python -m venv venv
# macOS/Linux: source venv/bin/activate
# Windows PowerShell: venv\Scripts\Activate.ps1
# Windows cmd.exe: venv\Scripts\activate.bat
# Save dependencies — keeps the env reproducible
echo "anthropic>=0.40.0" > requirements.txt
pip install -r requirements.txt
# API key (use the form for your shell)
# bash/zsh: export ANTHROPIC_API_KEY=your-key-here
# Windows PowerShell: $env:ANTHROPIC_API_KEY = "your-key-here"
# Windows cmd.exe: set ANTHROPIC_API_KEY=your-key-here
Node.js / TypeScript Setup
# Run each line on its own — works in bash, zsh, cmd.exe, and PowerShell 5.1+
mkdir capstone-1-ucc-lookup
cd capstone-1-ucc-lookup
npm init -y
npm install @anthropic-ai/sdk typescript ts-node
# API key (use the form for your shell)
# bash/zsh: export ANTHROPIC_API_KEY=your-key-here
# Windows PowerShell: $env:ANTHROPIC_API_KEY = "your-key-here"
# Windows cmd.exe: set ANTHROPIC_API_KEY=your-key-here
File Structure
You will create the following files. All files live in the same directory so imports resolve correctly.
capstone-1-ucc-lookup/
├── mock_sos.py # Mock Secretary of State data
├── ucc_agent.py # Main agent with tool-use loop
├── mock_sos.ts # TypeScript mock data
├── ucc_agent.ts # TypeScript agent
└── requirements.txt # Dependencies
Domain Glossary
Architecture Diagram
Mock Data Specification
The mock tool returns data in this structure. Your agent will receive this JSON and must synthesize it into a readable summary.
{
"business_name": "string — the searched entity name",
"state": "string — 2-letter state code",
"search_timestamp": "string — ISO 8601 datetime",
"filings": [
{
"filing_number": "string — state filing ID",
"filing_type": "string — UCC-1 | UCC-3",
"filing_date": "string — YYYY-MM-DD",
"lapse_date": "string — YYYY-MM-DD",
"status": "string — active | lapsed | terminated",
"secured_party": {
"name": "string — lender/creditor name",
"address": "string — full mailing address"
},
"debtor": {
"name": "string — business entity name",
"address": "string — full mailing address"
},
"collateral_description": "string — free-text description",
"amendments": ["array of amendment objects"]
}
],
"total_filings": "number — count of filings returned"
}
{
"business_name": "Acme Logistics LLC",
"state": "DE",
"search_timestamp": "2024-03-10T14:30:00Z",
"filings": [
{
"filing_number": "2023-1234567",
"filing_type": "UCC-1",
"filing_date": "2023-06-15",
"lapse_date": "2028-06-15",
"status": "active",
"secured_party": {
"name": "First National Bank",
"address": "123 Main St, Wilmington, DE 19801"
},
"debtor": {
"name": "Acme Logistics LLC",
"address": "456 Commerce Blvd, Dover, DE 19901"
},
"collateral_description": "All inventory, accounts receivable, and equipment now owned or hereafter acquired.",
"amendments": []
},
{
"filing_number": "2022-9876543",
"filing_type": "UCC-1",
"filing_date": "2022-01-20",
"lapse_date": "2027-01-20",
"status": "active",
"secured_party": {
"name": "Delaware Capital Partners",
"address": "789 Finance Row, Wilmington, DE 19801"
},
"debtor": {
"name": "Acme Logistics LLC",
"address": "456 Commerce Blvd, Dover, DE 19901"
},
"collateral_description": "All vehicles and transportation equipment.",
"amendments": []
}
],
"total_filings": 2
}
Implementation Phases
- Phase 1 — Mock Tool (10 min): Create the
search_ucc_filingsfunction with mock data for 5 business entities across 4 states. Include error cases (invalid state, no results, timeout). - Phase 2 — Tool Definition (5 min): Define the tool schema for the Claude API with accurate
name,description,input_schema(including property descriptions and required fields). - Phase 3 — System Prompt (5 min): Write a focused system prompt that defines the agent's role, capabilities, constraints, and response format.
- Phase 4 — Agentic Loop (10 min): Implement the tool-use loop: send message → check
stop_reason→ dispatch tool → send result → repeat untilend_turn. - Phase 5 — Error Handling (5 min): Handle all three error types: invalid input, not found, and service timeout. Return user-friendly messages.
- Phase 6 — Testing (10 min): Run the 10 test cases (happy path + edge cases + adversarial). Fix any failures.
- Phase 7 — Stretch (optional, +30 min): Add the
get_filing_amendmentstool for amendment history lookup.
Step-by-Step Build
Step 1: Create the Mock SOS Data
What: Build a mock Secretary of State database that returns UCC filing records for test business entities. Why: Using mock data lets you develop and test the full agent loop without needing a real SOS API account, API keys, 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_sos.py (Python) or mock_sos.ts (TypeScript) and paste the complete code below.
# mock_sos.py — Mock Secretary of State UCC filing database
# Simulates UCC filing search across multiple states.
VALID_STATES = {"DE", "NY", "CA", "TX", "FL", "IL", "OH", "PA", "NJ", "GA"}
MOCK_FILINGS = {
("acme logistics llc", "DE"): {
"business_name": "Acme Logistics LLC",
"state": "DE",
"search_timestamp": "2024-03-10T14:30:00Z",
"filings": [
{
"filing_number": "2023-1234567",
"filing_type": "UCC-1",
"filing_date": "2023-06-15",
"lapse_date": "2028-06-15",
"status": "active",
"secured_party": {"name": "First National Bank", "address": "123 Main St, Wilmington, DE 19801"},
"debtor": {"name": "Acme Logistics LLC", "address": "456 Commerce Blvd, Dover, DE 19901"},
"collateral_description": "All inventory, accounts receivable, and equipment now owned or hereafter acquired.",
"amendments": [],
},
{
"filing_number": "2022-9876543",
"filing_type": "UCC-1",
"filing_date": "2022-01-20",
"lapse_date": "2027-01-20",
"status": "active",
"secured_party": {"name": "Delaware Capital Partners", "address": "789 Finance Row, Wilmington, DE 19801"},
"debtor": {"name": "Acme Logistics LLC", "address": "456 Commerce Blvd, Dover, DE 19901"},
"collateral_description": "All vehicles and transportation equipment.",
"amendments": [],
},
],
"total_filings": 2,
},
("buildright construction", "NY"): {
"business_name": "BuildRight Construction",
"state": "NY",
"search_timestamp": "2024-03-10T14:31:00Z",
"filings": [
{
"filing_number": "NY-2024-0055123",
"filing_type": "UCC-1",
"filing_date": "2024-01-10",
"lapse_date": "2029-01-10",
"status": "active",
"secured_party": {"name": "Empire State Lending Corp", "address": "100 Wall St, New York, NY 10005"},
"debtor": {"name": "BuildRight Construction", "address": "88 Builder Ave, Albany, NY 12207"},
"collateral_description": "All construction equipment, heavy machinery, and related parts inventory.",
"amendments": [],
},
],
"total_filings": 1,
},
("metro holdings inc", "CA"): {
"business_name": "Metro Holdings Inc",
"state": "CA",
"search_timestamp": "2024-03-10T14:32:00Z",
"filings": [],
"total_filings": 0,
},
("riverside trucking llc", "TX"): {
"business_name": "Riverside Trucking LLC",
"state": "TX",
"search_timestamp": "2024-03-10T14:32:00Z",
"filings": [
{
"filing_number": "TX-2023-89441",
"filing_type": "UCC-1",
"filing_date": "2023-09-12",
"lapse_date": "2028-09-12",
"status": "active",
"secured_party": {"name": "First National Bank", "address": "123 Main St, Dallas, TX 75201"},
"debtor": {"name": "Riverside Trucking LLC", "address": "200 River Rd, Houston, TX 77002"},
"collateral_description": "All trucks, trailers, and titled rolling stock; specifically 6 Peterbilt 579 model year 2022.",
"amendments": [],
},
],
"total_filings": 1,
},
("metalworks incorporated", "DE"): {
"business_name": "MetalWorks Incorporated",
"state": "DE",
"search_timestamp": "2024-03-10T14:32:00Z",
"filings": [
{
"filing_number": "DE-2022-44102",
"filing_type": "UCC-1",
"filing_date": "2022-04-18",
"lapse_date": "2029-04-18",
"status": "active",
"secured_party": {"name": "Industrial Lenders LLC", "address": "55 Capital Way, Wilmington, DE 19801"},
"debtor": {"name": "MetalWorks Incorporated", "address": "12 Foundry Ln, Newark, DE 19711"},
"collateral_description": "Equipment, inventory, and accounts receivable now owned or hereafter acquired.",
"amendments": [
{"filing_number": "DE-2024-00811", "amendment_type": "continuation", "filing_date": "2024-01-22", "new_lapse_date": "2029-04-18"},
],
},
],
"total_filings": 1,
},
}
def search_ucc_filings(business_name: str, state: str) -> dict:
"""Search UCC filings for a business in a specific state."""
try:
# Validate state code
if not state or len(state) != 2:
return {
"is_error": True,
"error_category": "INVALID_STATE",
"is_retryable": False,
"context": f"Invalid state code: '{state}'. Provide a 2-letter US state code.",
}
state_upper = state.upper()
if state_upper not in VALID_STATES:
return {
"is_error": True,
"error_category": "INVALID_STATE",
"is_retryable": False,
"context": f"State '{state_upper}' not in supported states: {', '.join(sorted(VALID_STATES))}",
}
# Simulate timeout for TX (for testing)
if state_upper == "TX" and business_name.lower() == "timeout test":
return {
"is_error": True,
"error_category": "SERVICE_TIMEOUT",
"is_retryable": True,
"context": "Texas SOS service timed out after 30 seconds.",
}
# Look up filings
key = (business_name.lower().strip(), state_upper)
result = MOCK_FILINGS.get(key)
if result:
return {"is_error": False, **result}
# No results found (valid search, just no matches)
return {
"is_error": False,
"business_name": business_name,
"state": state_upper,
"search_timestamp": "2024-03-10T14:35:00Z",
"filings": [],
"total_filings": 0,
}
except Exception as e:
return {
"is_error": True,
"error_category": "INTERNAL_ERROR",
"is_retryable": True,
"context": str(e),
}
// mock_sos.ts — Mock Secretary of State UCC filing database
interface ToolResult { is_error: boolean; [key: string]: unknown; }
const VALID_STATES = new Set(["DE","NY","CA","TX","FL","IL","OH","PA","NJ","GA"]);
const MOCK_FILINGS: Record<string, object> = {
"acme logistics llc|DE": {
business_name: "Acme Logistics LLC", state: "DE",
search_timestamp: "2024-03-10T14:30:00Z",
filings: [
{
filing_number: "2023-1234567", filing_type: "UCC-1",
filing_date: "2023-06-15", lapse_date: "2028-06-15", status: "active",
secured_party: { name: "First National Bank", address: "123 Main St, Wilmington, DE 19801" },
debtor: { name: "Acme Logistics LLC", address: "456 Commerce Blvd, Dover, DE 19901" },
collateral_description: "All inventory, accounts receivable, and equipment now owned or hereafter acquired.",
amendments: [],
},
{
filing_number: "2022-9876543", filing_type: "UCC-1",
filing_date: "2022-01-20", lapse_date: "2027-01-20", status: "active",
secured_party: { name: "Delaware Capital Partners", address: "789 Finance Row, Wilmington, DE 19801" },
debtor: { name: "Acme Logistics LLC", address: "456 Commerce Blvd, Dover, DE 19901" },
collateral_description: "All vehicles and transportation equipment.",
amendments: [],
},
],
total_filings: 2,
},
"buildright construction|NY": {
business_name: "BuildRight Construction", state: "NY",
search_timestamp: "2024-03-10T14:31:00Z",
filings: [
{
filing_number: "NY-2024-0055123", filing_type: "UCC-1",
filing_date: "2024-01-10", lapse_date: "2029-01-10", status: "active",
secured_party: { name: "Empire State Lending Corp", address: "100 Wall St, New York, NY 10005" },
debtor: { name: "BuildRight Construction", address: "88 Builder Ave, Albany, NY 12207" },
collateral_description: "All construction equipment, heavy machinery, and related parts inventory.",
amendments: [],
},
],
total_filings: 1,
},
"metro holdings inc|CA": {
business_name: "Metro Holdings Inc", state: "CA",
search_timestamp: "2024-03-10T14:32:00Z",
filings: [], total_filings: 0,
},
"riverside trucking llc|TX": {
business_name: "Riverside Trucking LLC", state: "TX",
search_timestamp: "2024-03-10T14:32:00Z",
filings: [
{
filing_number: "TX-2023-89441", filing_type: "UCC-1",
filing_date: "2023-09-12", lapse_date: "2028-09-12", status: "active",
secured_party: { name: "First National Bank", address: "123 Main St, Dallas, TX 75201" },
debtor: { name: "Riverside Trucking LLC", address: "200 River Rd, Houston, TX 77002" },
collateral_description: "All trucks, trailers, and titled rolling stock; specifically 6 Peterbilt 579 model year 2022.",
amendments: [],
},
],
total_filings: 1,
},
"metalworks incorporated|DE": {
business_name: "MetalWorks Incorporated", state: "DE",
search_timestamp: "2024-03-10T14:32:00Z",
filings: [
{
filing_number: "DE-2022-44102", filing_type: "UCC-1",
filing_date: "2022-04-18", lapse_date: "2029-04-18", status: "active",
secured_party: { name: "Industrial Lenders LLC", address: "55 Capital Way, Wilmington, DE 19801" },
debtor: { name: "MetalWorks Incorporated", address: "12 Foundry Ln, Newark, DE 19711" },
collateral_description: "Equipment, inventory, and accounts receivable now owned or hereafter acquired.",
amendments: [
{ filing_number: "DE-2024-00811", amendment_type: "continuation", filing_date: "2024-01-22", new_lapse_date: "2029-04-18" },
],
},
],
total_filings: 1,
},
};
export function searchUccFilings(businessName: string, state: string): ToolResult {
try {
if (!state || state.length !== 2) {
return { is_error: true, error_category: "INVALID_STATE", is_retryable: false,
context: `Invalid state code: '${state}'. Provide a 2-letter US state code.` };
}
const stateUpper = state.toUpperCase();
if (!VALID_STATES.has(stateUpper)) {
return { is_error: true, error_category: "INVALID_STATE", is_retryable: false,
context: `State '${stateUpper}' not supported.` };
}
if (stateUpper === "TX" && businessName.toLowerCase() === "timeout test") {
return { is_error: true, error_category: "SERVICE_TIMEOUT", is_retryable: true,
context: "Texas SOS service timed out after 30 seconds." };
}
const key = `${businessName.toLowerCase().trim()}|${stateUpper}`;
const result = MOCK_FILINGS[key];
if (result) return { is_error: false, ...result };
return { is_error: false, business_name: businessName, state: stateUpper,
search_timestamp: "2024-03-10T14:35:00Z", filings: [], total_filings: 0 };
} catch (error) {
return { is_error: true, error_category: "INTERNAL_ERROR", is_retryable: true, context: String(error) };
}
}
Run Command
python -c "from mock_sos import search_ucc_filings; print(search_ucc_filings('Acme Logistics LLC', 'DE'))"
npx ts-node -e "import {searchUccFilings} from './mock_sos'; console.log(searchUccFilings('Acme Logistics LLC', 'DE'))"
You built a mock SOS database with 5 business entities across 4 states: Acme Logistics (DE, 2 filings), BuildRight Construction (NY, 1 filing), Metro Holdings (CA, 0 filings), Riverside Trucking (TX, 1 filing), and MetalWorks Incorporated (DE, 1 filing with a continuation amendment). The function handles three error types: invalid state code, service timeout (triggered by a test entity in TX), and internal errors. Every response uses structured error fields (is_error, error_category, is_retryable) so your agent can make intelligent decisions about retries and user messages.
ModuleNotFoundError: No module named 'mock_sos'— Make sure you are running the command from the same directory wheremock_sos.pyis saved.SyntaxError: invalid syntax— Confirm you are using Python 3.10+. Runpython --versionto check.- Output shows
None— Check that the business name and state match exactly (case-insensitive). The mock data uses lowercase keys internally.
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 ucc_agent.py (Python) or ucc_agent.ts (TypeScript) and paste the complete code below.
# ucc_agent.py — UCC Filing Lookup Agent (Capstone 1, Domain C)
# A single-tool conversational agent that searches UCC filings
# and summarizes lien exposure in plain English.
import anthropic
import json
from mock_sos import search_ucc_filings
SYSTEM_PROMPT = """You are a UCC Filing Lookup Agent for commercial lenders.
Your job is to help credit analysts assess lien exposure on prospective
borrowers by searching Secretary of State UCC filing records.
You have access to one tool:
- search_ucc_filings: Search UCC filings by business name and state
Rules:
- Always ask for the state if the user doesn't provide one.
- Summarize filings in plain English — not raw JSON.
- For each active filing, clearly state: secured party, collateral,
filing date, and lapse date.
- If there are no filings, report a clean lien status.
- Warn the user that results may be incomplete (name variations,
filings in other states).
- You can ONLY search filings. You cannot file, modify, or delete them.
- Never reveal raw error details to the user.
"""
TOOLS = [
{
"name": "search_ucc_filings",
"description": (
"Search the Secretary of State UCC filing database for a "
"business entity in a specific state. Returns all active "
"and lapsed filings with secured party, collateral, and dates."
),
"input_schema": {
"type": "object",
"properties": {
"business_name": {
"type": "string",
"description": "The business entity name to search for",
},
"state": {
"type": "string",
"description": "2-letter US state code (e.g., DE, NY, CA)",
},
},
"required": ["business_name", "state"],
},
},
]
TOOL_HANDLERS = {
"search_ucc_filings": lambda args: search_ucc_filings(
args["business_name"], args["state"]
),
}
def run_agent(user_message: str, history: list | None = None) -> tuple[str, list]:
"""
Run the UCC lookup agent. Supports multi-turn conversation
by accepting and returning message history.
"""
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY env var
messages = history or []
messages.append({"role": "user", "content": user_message})
tools_called = []
for _ in range(5): # safety net
try:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=messages,
)
except anthropic.APIError as e:
return f"Sorry, I encountered a technical issue: {e}", messages
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
tools_called.append(block.name)
handler = TOOL_HANDLERS.get(block.name)
result = handler(block.input) if handler else {
"is_error": True,
"error_category": "UNKNOWN_TOOL",
"context": block.name,
}
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
elif response.stop_reason == "end_turn":
text = " ".join(
b.text for b in response.content if hasattr(b, "text")
)
messages.append({"role": "assistant", "content": response.content})
return text, messages
return "I was unable to complete your request.", messages
if __name__ == "__main__":
print("UCC Filing Lookup Agent — Type 'quit' to exit.\n")
history = []
while True:
query = input("You: ").strip()
if query.lower() in ("quit", "exit", "q"):
break
response, history = run_agent(query, history)
print(f"\nAgent: {response}\n")
// ucc_agent.ts — UCC Filing Lookup Agent (Capstone 1, Domain C)
import Anthropic from "@anthropic-ai/sdk";
import { searchUccFilings } from "./mock_sos.js";
const SYSTEM_PROMPT = `You are a UCC Filing Lookup Agent for commercial lenders.
Your job is to help credit analysts assess lien exposure on prospective
borrowers by searching Secretary of State UCC filing records.
You have access to one tool:
- search_ucc_filings: Search UCC filings by business name and state
Rules:
- Always ask for the state if the user doesn't provide one.
- Summarize filings in plain English — not raw JSON.
- For each active filing, clearly state: secured party, collateral,
filing date, and lapse date.
- If there are no filings, report a clean lien status.
- Warn the user that results may be incomplete (name variations,
filings in other states).
- You can ONLY search filings. You cannot file, modify, or delete them.
- Never reveal raw error details to the user.`;
const TOOLS: Anthropic.Tool[] = [
{
name: "search_ucc_filings",
description:
"Search the Secretary of State UCC filing database for a " +
"business entity in a specific state. Returns all active " +
"and lapsed filings with secured party, collateral, and dates.",
input_schema: {
type: "object" as const,
properties: {
business_name: { type: "string", description: "Business entity name" },
state: { type: "string", description: "2-letter US state code" },
},
required: ["business_name", "state"],
},
},
];
const TOOL_HANDLERS: Record<string, (args: Record<string, string>) => unknown> = {
search_ucc_filings: (args) => searchUccFilings(args.business_name, args.state),
};
export async function runAgent(
userMessage: string,
history: Anthropic.MessageParam[] = [],
): Promise<[string, Anthropic.MessageParam[]]> {
const client = new Anthropic();
const messages = [...history, { role: "user" as const, content: userMessage }];
for (let i = 0; i < 5; i++) {
let response: Anthropic.Message;
try {
response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: SYSTEM_PROMPT,
tools: TOOLS,
messages,
});
} catch (error) {
return [`Sorry, I encountered a technical issue: ${error}`, messages];
}
if (response.stop_reason === "tool_use") {
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const handler = TOOL_HANDLERS[block.name];
const result = handler
? handler(block.input as Record<string, string>)
: { is_error: true, error_category: "UNKNOWN_TOOL", context: block.name };
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
} else if (response.stop_reason === "end_turn") {
const text = response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join(" ");
messages.push({ role: "assistant", content: response.content });
return [text, messages];
}
}
return ["I was unable to complete your request.", messages];
}
// --- Main entry point (run with: npx ts-node ucc_agent.ts) ---
import * as readline from "readline";
async function main() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = (q: string): Promise<string> =>
new Promise((resolve) => rl.question(q, resolve));
console.log("UCC Filing Lookup Agent — Type 'quit' to exit.\n");
let history: Anthropic.MessageParam[] = [];
while (true) {
const query = (await prompt("You: ")).trim();
if (["quit", "exit", "q"].includes(query.toLowerCase())) break;
const [response, newHistory] = await runAgent(query, history);
history = newHistory;
console.log(`\nAgent: ${response}\n`);
}
rl.close();
}
main().catch(console.error);
You built a complete UCC Filing Lookup Agent. Key design decisions: (1) The system prompt defines clear constraints — search only, no modifications, ask for state if missing. (2) The agentic loop uses stop_reason for termination, not text parsing. (3) The history parameter enables multi-turn conversations — the agent remembers previous searches within a session. (4) Error handling catches API errors and returns user-friendly messages. (5) A safety cap of 5 iterations prevents infinite loops.
AuthenticationError— YourANTHROPIC_API_KEYenvironment variable is missing or invalid. Runecho $ANTHROPIC_API_KEY(orecho %ANTHROPIC_API_KEY%on Windows) to check.ImportError: cannot import name 'search_ucc_filings'— Make suremock_sos.pyis in the same directory asucc_agent.py.- Agent loops forever / hits the 5-iteration cap — This usually means the tool result is malformed. Verify
search_ucc_filingsreturns a dict, notNone.
Step 3: Test the Complete Agent
What: Run the agent in interactive mode and test it with real queries. Why: This is where you see the agentic loop working end-to-end: your message goes to Claude, Claude decides to call the tool, your code dispatches the call, feeds the result back, and Claude synthesizes a plain-English response.
Run the agent with the following command:
python ucc_agent.py
npx ts-node ucc_agent.ts
At the prompt, type: Search for Acme Logistics LLC in Delaware
If you see a structured, plain-English summary like the one above, your agent is working correctly. Claude received the user query, decided to call search_ucc_filings, your code dispatched the call and returned mock data, and Claude synthesized the results into an actionable summary. Try follow-up queries like "Now check New York" or "What about Metro Holdings Inc in CA" to test multi-turn context.
- Agent prints raw JSON instead of a summary — Check your system prompt. The instruction "Summarize filings in plain English — not raw JSON" should be present.
- Agent does not call the tool — Verify the
toolsparameter is being passed toclient.messages.create(). Without it, Claude cannot see or use any tools. - Agent asks for the state even when you provide it — This can happen if the tool's
input_schemadescription is unclear. Make sure thestateproperty description says "2-letter US state code."
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.
python -c "
from ucc_agent import run_agent
response, _ = run_agent('Search for Acme Logistics LLC in Delaware')
print(response)
assert 'First National Bank' in response, 'Missing secured party'
assert 'active' in response.lower(), 'Missing filing status'
print('\n--- ALL CHECKS PASSED ---')
"
npx ts-node -e "
import { runAgent } from './ucc_agent';
(async () => {
const [response] = await runAgent('Search for Acme Logistics LLC in Delaware');
console.log(response);
console.assert(response.includes('First National Bank'), 'Missing secured party');
console.assert(response.toLowerCase().includes('active'), 'Missing filing status');
console.log('\n--- ALL CHECKS PASSED ---');
})();
"
If you see "ALL CHECKS PASSED" your agent is fully operational. You have built a working single-tool conversational agent that searches UCC filings and summarizes lien exposure. Continue to the Testing Guide below to exercise edge cases and adversarial inputs.
Testing Guide
| Type | Input | Expected Behavior |
|---|---|---|
| Happy | "Acme Logistics LLC in DE" | Returns 2 active filings with clear summary |
| Happy | "BuildRight Construction in NY" | Returns 1 active filing |
| Happy | "Metro Holdings Inc in CA" | Reports clean lien status (no filings) |
| Happy | Follow-up: "Tell me more about the first filing's collateral" | References previous results from context |
| Happy | "Search for Acme Logistics in DE" then "Now check NY" | Handles sequential searches correctly |
| Edge | "Search for Acme Lgistics" (misspelled) | Proceeds but warns results may be incomplete |
| Edge | "Search for Acme Logistics" (no state) | Asks which state to search |
| Edge | "timeout test in TX" | Reports service timeout, suggests retry |
| Adversarial | "File a new UCC lien against Acme" | Explains it can only search, not file |
| Adversarial | "'; DROP TABLE filings; --" | Treats as literal business name, handles gracefully |
Compliance & Regulatory Notes
Public records, private obligations: UCC filings are public records — anyone can search them. However, the analysis and risk assessments built from filing data may be subject to regulatory requirements under the Fair Credit Reporting Act (FCRA)A US federal law that regulates the collection, dissemination, and use of consumer credit information. If your UCC analysis tool is used for credit decisions, it may need to comply with FCRA requirements including accuracy, dispute resolution, and permissible purpose. if used for credit decisions.
Data freshness: SOS databases are NOT real-time. Filing data may be 24–72 hours behind actual filings. Always disclose the search_timestamp and recommend the user verify critical filings directly with the SOS office.
State variations: Each state has its own filing rules, fee structures, and data formats. A "blanket lien" in one state may have different legal implications than in another. Your agent should never provide legal advice — only factual summaries of filing records.
Going Further
- [OPTIONAL] Stretch: Amendment history — Add the
get_filing_amendmentstool to check whether filings have been continued, assigned, or terminated. - Multi-state search — Allow the user to search across multiple states in a single request (requires multi-tool orchestration from M06).
- Entity resolution — Add fuzzy name matching to find filings under name variations (e.g., "Acme Logistics" vs "ACME LOGISTICS LLC" vs "Acme Logistics Co"). This leads to Capstone 3.
- Risk scoring — Calculate a lien exposure score based on collateral breadth, number of secured parties, and time to lapse.
- RAG on regulations — Add a RAG pipeline over UCC Article 9 reference materials so the agent can answer questions like "What happens when this filing lapses?" This leads to Capstone 2.
Knowledge Check
Test your understanding of the concepts covered in this capstone project.
Q1: What parameter in client.messages.create() provides tools to Claude?
Q2: When Claude wants to call a tool, what value does stop_reason have?
Q3: What is a UCC-1 financing statement?
Q4: A user searches for "Acme" in Texas but no filings exist. What should the agent do?
Q5: Why does the mock tool validate state codes before searching?
References & Resources
- Claude Tool Use Documentation — Official guide to function calling
- Claude Model Overview — Model capabilities and pricing
- Anthropic Cookbook — Production-ready agent examples
- UCC Article 9 (Cornell Law) — Legal reference for secured transactions