What You'll Build
If you've ever searched for how to build AI agents tutorial content that actually ships working code, you're in the right place. By the end of this tutorial, you'll have a fully functional lead qualifier agent built in Python using the Anthropic SDK that can validate contact info, check budget fit, and assess timeline — automatically, in under 50 lines of core logic.
This is the exact type of agent we build at Naples AI for local businesses who are drowning in unqualified leads. Sales teams waste hours chasing prospects who were never a good fit to begin with — this agent fixes that.
We're using Claude claude-sonnet-4-6 with tool use, which means the model decides when to call your qualification functions rather than just free-texting an answer. That's what makes it an agent rather than a chatbot.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- The
anthropicPython package installed (pip install anthropic) - Basic comfort reading Python — you don't need to be an expert
- A terminal / command line you can run scripts from
lead_qualifier.py.
Step 1: Set Up Your Claude API Project
First, let's make sure the environment is wired up correctly. Create a new folder, drop in a .env file with your API key, and install the dependency.
Run this in your terminal:
terminalmkdir lead-qualifier-agent cd lead-qualifier-agent pip install anthropic python-dotenv
Now create a .env file in that folder:
ANTHROPIC_API_KEY=sk-ant-your-key-goes-here
Now let's write the top of our agent file — imports, client initialization, and the model constant we'll reuse throughout.
lead_qualifier.pyimport os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
# Initialize the Anthropic client using your API key from .env
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"That's all the setup you need. The anthropic.Anthropic() client handles authentication and HTTP for you — no requests library, no manual headers.
python-dotenv pattern above keeps it out of version control. Add .env to your .gitignore before you push anything.
Step 2: Define Qualification Criteria and Tools
This is where the agent gets its actual intelligence. We're going to define three qualification tools: contact validation, budget check, and timeline assessment. Claude will decide when to call each one based on the lead data you give it.
In the Anthropic SDK, tools are defined as Python dictionaries that describe what the function does and what parameters it expects. The model reads these descriptions at runtime and decides which tool to invoke.
lead_qualifier.py (continued)# Tool definitions tell Claude what functions are available and how to call them
TOOLS = [
{
"name": "validate_contact",
"description": "Validates that a lead has a real email address and phone number. Returns whether each field is valid and a combined contact_valid boolean.",
"input_schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "The lead's email address"
},
"phone": {
"type": "string",
"description": "The lead's phone number, any format"
}
},
"required": ["email", "phone"]
}
},
{
"name": "check_budget",
"description": "Checks whether a lead's stated budget meets the minimum threshold for our services. Returns fit level: high, medium, low, or disqualified.",
"input_schema": {
"type": "object",
"properties": {
"budget": {
"type": "number",
"description": "The lead's stated monthly or project budget in USD"
}
},
"required": ["budget"]
}
},
{
"name": "assess_timeline",
"description": "Evaluates how soon the lead wants to start. Returns urgency level: immediate, near_term, or low_priority.",
"input_schema": {
"type": "object",
"properties": {
"timeline": {
"type": "string",
"description": "The lead's stated timeline, e.g. 'within 30 days', 'Q1 next year', 'just exploring'"
}
},
"required": ["timeline"]
}
}
]Now let's write the actual Python functions that get called when Claude invokes each tool. These are the real business logic — the rules that define a qualified lead for your company.
lead_qualifier.py (continued)import re
def validate_contact(email: str, phone: str) -> dict:
# Basic email format check using regex
email_valid = bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
# Strip non-digits and check length (US numbers = 10 digits)
digits = re.sub(r"\D", "", phone)
phone_valid = len(digits) >= 10
return {
"email_valid": email_valid,
"phone_valid": phone_valid,
"contact_valid": email_valid and phone_valid
}
def check_budget(budget: float) -> dict:
if budget >= 5000:
fit = "high"
elif budget >= 1500:
fit = "medium"
elif budget >= 500:
fit = "low"
else:
fit = "disqualified"
return {"budget": budget, "fit": fit}
def assess_timeline(timeline: str) -> dict:
timeline_lower = timeline.lower()
if any(word in timeline_lower for word in ["immediate", "asap", "now", "this week", "30 days", "next month"]):
urgency = "immediate"
elif any(word in timeline_lower for word in ["quarter", "q1", "q2", "q3", "q4", "3 months", "6 months"]):
urgency = "near_term"
else:
urgency = "low_priority"
return {"timeline": timeline, "urgency": urgency}
# Router that maps tool names to their Python functions
def run_tool(tool_name: str, tool_input: dict) -> dict:
if tool_name == "validate_contact":
return validate_contact(**tool_input)
elif tool_name == "check_budget":
return check_budget(**tool_input)
elif tool_name == "assess_timeline":
return assess_timeline(**tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}Notice that run_tool is just a simple router. It takes the name Claude gives us and calls the right function. This pattern scales cleanly when you add more tools later.
Step 3: Build the Agent with Tool Use
Here's where the agentic loop comes in. An agent isn't a single API call — it's a loop. Claude responds, sometimes calling a tool. You run the tool, feed the result back, and Claude responds again. You keep going until Claude stops calling tools and gives you a final answer.
This is the core mechanic that separates a Claude API tutorial for agents from a basic completion call. Let's build it.
lead_qualifier.py (continued)def run_agent(lead_data: dict) -> str:
"""
Runs the lead qualification agent loop.
Keeps cycling until Claude returns a final text response with no tool calls.
"""
# Build the initial user message with lead details
user_message = f"""
Please qualify the following lead for our AI services agency.
Use the available tools to validate their contact info, check their budget fit,
and assess their timeline urgency. Then give me a final qualification summary.
Lead data:
- Name: {lead_data.get('name')}
- Email: {lead_data.get('email')}
- Phone: {lead_data.get('phone')}
- Budget (USD): {lead_data.get('budget')}
- Timeline: {lead_data.get('timeline')}
- Company: {lead_data.get('company', 'Not provided')}
- Notes: {lead_data.get('notes', 'None')}
"""
messages = [{"role": "user", "content": user_message}]
# Agentic loop — keeps running until Claude stops requesting tools
while True:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=TOOLS,
messages=messages
)
# Append Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
# If Claude is done using tools, extract and return the final text
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
# Otherwise, process every tool call Claude made in this turn
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Feed all tool results back to Claude in a single user message
if tool_results:
messages.append({"role": "user", "content": tool_results})stop_reason == "end_turn". If you forget to return tool results back to Claude, the model will hang waiting for them and your loop will spin forever. Always append tool results before the next iteration.
Step 4: Create the Qualification Workflow
Now let's wire everything together with a realistic lead and actually run it. This is the part you'd swap out with real form data from your CRM or website webhook in production.
lead_qualifier.py (continued)def qualify_lead(lead: dict) -> dict:
"""
Public interface: takes a lead dict, returns a qualification result dict.
"""
print(f"\n{'='*50}")
print(f"Qualifying lead: {lead.get('name')} from {lead.get('company', 'Unknown')}")
print(f"{'='*50}\n")
summary = run_agent(lead)
return {
"lead_name": lead.get("name"),
"company": lead.get("company"),
"qualification_summary": summary
}
if __name__ == "__main__":
# Example lead — replace with real data from your form or CRM
sample_lead = {
"name": "Maria Gonzalez",
"email": "[email protected]",
"phone": "(239) 555-0192",
"budget": 4500,
"timeline": "We want to go live within the next 30 days",
"company": "Suncoast Realty Group",
"notes": "Interested in AI listing automation and chatbot for their website"
}
result = qualify_lead(sample_lead)
print("\n--- QUALIFICATION RESULT ---")
print(f"Lead: {result['lead_name']} | {result['company']}")
print(f"\n{result['qualification_summary']}")Run it with python lead_qualifier.py and you'll see Claude call each tool in sequence, evaluate the results, and hand you back a structured summary.
Here's what the actual output looks like:
sample output================================================== Qualifying lead: Maria Gonzalez from Suncoast Realty Group ================================================== --- QUALIFICATION RESULT --- Lead: Maria Gonzalez | Suncoast Realty Group **Lead Qualification Summary — Maria Gonzalez, Suncoast Realty Group** ✅ Contact Validation: PASSED - Email ([email protected]): Valid format - Phone ((239) 555-0192): Valid — 10-digit US number confirmed 💰 Budget Assessment: MEDIUM FIT - Stated budget: $4,500 - Falls in the medium tier ($1,500–$4,999). Strong enough to proceed. - Recommend discussing a phased engagement starting with the chatbot. ⏱️ Timeline: IMMEDIATE - Wants to go live within 30 days — high urgency, hot lead. - Prioritize scheduling a discovery call this week. 📋 Overall Recommendation: QUALIFY — PRIORITY OUTREACH Maria represents a warm, time-sensitive opportunity in real estate AI automation. Contact info is clean, budget is workable, and urgency is high. Recommend scheduling a 30-minute discovery call within 24 hours.
How It Works
Let me walk you through what's actually happening under the hood in plain English. When you call run_agent(), you're sending Claude a prompt that includes your lead data AND a list of tools it's allowed to call.
Claude reads the lead data and decides it needs more information before it can give you a real answer — so it calls validate_contact, then check_budget, then assess_timeline. Each time it calls a tool, the loop captures that call, runs your Python function locally, and sends the result back to Claude.
Claude never actually runs your Python code — it just tells you what to run and what arguments to use. Your code runs the function and reports back. Once Claude has all three tool results, it synthesizes everything into the final qualification summary and sets stop_reason to end_turn, which breaks the loop.
Common Errors and Fixes
Error 1: AuthenticationError — Invalid API Key
anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}Fix: Your .env file isn't being loaded, or the key is wrong. Double-check that load_dotenv() is called before anthropic.Anthropic(), that your .env file is in the same directory you're running the script from, and that the key starts with sk-ant-.
Error 2: Invalid Tool Result Format Causes Infinite Loop
# The agent never stops — it keeps calling the API in a loop # No error is thrown, but the script never exits
Fix: This happens when your tool result message doesn't match the format Claude expects. Make sure each entry in tool_results has "type": "tool_result", the correct "tool_use_id" from block.id, and that the content is a JSON string (not a raw dict). The json.dumps(result) call in our code handles that last part.
Error 3: KeyError When Accessing Response Content
AttributeError: 'TextBlock' object has no attribute 'input'
Fix: You're iterating over all blocks in response.content and trying to access .input on every one, but only tool_use blocks have that attribute. Always check if block.type == "tool_use" before accessing block.input or block.name. Our loop does this correctly — just make sure you don't simplify it out.
Next Steps
You've got a working lead qualifier agent. Here's how to take it further:
- Connect it to a real form. Use FastAPI or Flask to expose a
/qualifyendpoint. Post your Typeform or HubSpot webhook data directly to it and return the qualification result in real time. - Add a CRM write-back tool. Define a fourth tool called
save_to_crmthat calls the HubSpot or Salesforce API. Claude will call it automatically at the end of qualification and create the contact record for you. - Score leads numerically. Update
check_budgetandassess_timelineto return numeric scores (0–100). Have Claude calculate a weighted composite score and use it to sort leads in a dashboard. - Add industry-specific logic. If you serve real estate, healthcare, and restaurants like we do at Naples AI, branch your qualification criteria by industry. A $2,000 budget means something very different depending on the sector.
FAQ
How do I build an AI agent with the Anthropic Python SDK?
You build an agent by combining the messages.create() API call with a list of tool definitions and an agentic loop. The key is feeding tool results back into the conversation as a user message with "type": "tool_result" blocks, then letting the loop run until stop_reason == "end_turn". This tutorial covers the full pattern from scratch.
What is the difference between Claude tool use and function calling?
They're the same concept under different names. OpenAI calls it function calling; Anthropic calls it tool use. In both cases, the model doesn't execute your code — it outputs a structured request describing which function to call and with what arguments. Your application runs the function and returns the result to the model.
How much does it cost to run a Claude agent like this?
For a typical lead qualification run with three tool calls, you're looking at roughly 800–1,200 input tokens and 200–400 output tokens per lead. At current claude-sonnet-4-6 pricing, that's well under a cent per lead. At scale — say, 1,000 leads per month — you'd spend around $5–$10 in API costs, which is negligible compared to the sales time you save.
Can I run this Claude API tutorial with claude-opus or claude-haiku instead?
Yes — just swap the MODEL constant. Use claude-haiku-4-5 if you want faster, cheaper runs and your qualification logic is simple. Use claude-opus-4-5 if you're adding complex reasoning steps, like analyzing long-form notes or making nuanced industry judgments. For most lead qualification use cases, claude-sonnet-4-6 hits the right balance.
How do I add more qualification tools to the agent?
Add a new dictionary to the TOOLS list with a name, description, and input schema. Then write the Python function that implements it. Finally, add a branch to the run_tool() router. Claude will automatically decide when to call it based on the description you wrote — no other changes needed in the loop logic.
Conclusion
You just built a real, working AI lead qualifier agent with the Anthropic SDK in Python — one that validates contacts, checks budget fit, assesses timeline urgency, and hands you a clean summary ready for your sales team. This is exactly the kind of intelligent