What You'll Build
If you've been searching for a real-world autonomous agent tutorial in Python, this one skips the toy examples and builds something you can actually use. By the end of this guide, you'll have a working real estate lead qualifier agent powered by Claude's API that takes raw lead input, calls structured tools to evaluate budget, timeline, and property type, and returns a scored result with a routing decision. The whole thing runs in Python using the Anthropic SDK, and every line of code here is production-ready.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python functions and dictionaries
- The
anthropicpackage installed (pip install anthropic) - The
python-dotenvpackage for environment variable management (pip install python-dotenv)
Step 1: Set Up the Claude API Client and Define Your Agent Tools
The first thing we need is a working Anthropic client and a clear picture of what tools our agent will have access to. Tools in the Claude API are just structured function definitions that tell the model what it can call and what parameters those calls expect.
Here I'm defining three tools: one to check budget qualification, one to assess purchase timeline urgency, and one to validate the property type against what our fictional brokerage actually handles. Together, these three tools give the agent everything it needs to make a real qualification decision.
agent_setup.py
import os
import anthropic
from dotenv import load_dotenv
load_dotenv()
# Initialize the Anthropic client using your API key from environment
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# These tool definitions tell Claude what functions it can invoke during a conversation
TOOLS = [
{
"name": "check_budget",
"description": (
"Evaluates whether the lead's stated budget falls within the brokerage's "
"serviceable range. Returns a qualification tier: 'high', 'mid', or 'low'."
),
"input_schema": {
"type": "object",
"properties": {
"budget_usd": {
"type": "number",
"description": "The lead's stated maximum purchase budget in US dollars."
}
},
"required": ["budget_usd"]
}
},
{
"name": "check_timeline",
"description": (
"Assesses how urgently the lead wants to purchase. "
"Returns 'immediate', 'near_term', or 'exploratory' based on their timeframe."
),
"input_schema": {
"type": "object",
"properties": {
"months_to_purchase": {
"type": "integer",
"description": "How many months until the lead intends to make a purchase."
}
},
"required": ["months_to_purchase"]
}
},
{
"name": "check_property_type",
"description": (
"Validates whether the lead's desired property type is one the brokerage represents. "
"Returns 'supported' or 'unsupported'."
),
"input_schema": {
"type": "object",
"properties": {
"property_type": {
"type": "string",
"description": (
"The type of property the lead wants. "
"Examples: 'single_family', 'condo', 'townhouse', 'land', 'commercial'."
)
}
},
"required": ["property_type"]
}
}
]
if __name__ == "__main__":
print(f"Client initialized. Using model: {MODEL}")
print(f"Tools registered: {[t['name'] for t in TOOLS]}")
Running this file should give you clean output with no errors. If you see an AuthenticationError, double-check that your .env file has ANTHROPIC_API_KEY=your_key_here on its own line.
Step 2: Create the Lead Qualification Tool with Structured Criteria
Now we write the actual Python functions that execute when Claude calls those tools. These are the functions that contain your business logic. In a real brokerage setup, these might query a CRM or a property database, but here we're keeping the logic clean and readable so the pattern is easy to adapt.
The key thing to notice is that each function returns a plain dictionary. Claude reads that return value and uses it to decide what to do next, whether to call another tool or wrap up with a final answer.
qualification_tools.py
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
def check_budget(budget_usd: float) -> dict:
"""
Returns a qualification tier based on budget thresholds
set for a Southwest Florida luxury brokerage.
"""
if budget_usd >= 1_000_000:
tier = "high"
label = "Luxury buyer. Priority client."
elif budget_usd >= 400_000:
tier = "mid"
label = "Strong buyer. Standard follow-up."
else:
tier = "low"
label = "Below typical listing range. Refer to partner agency."
return {
"budget_tier": tier,
"budget_label": label,
"budget_usd": budget_usd
}
def check_timeline(months_to_purchase: int) -> dict:
"""
Returns urgency level based on how many months until the lead wants to buy.
"""
if months_to_purchase <= 2:
urgency = "immediate"
note = "Hot lead. Contact within 24 hours."
elif months_to_purchase <= 6:
urgency = "near_term"
note = "Warm lead. Add to nurture sequence."
else:
urgency = "exploratory"
note = "Early-stage. Add to long-term drip campaign."
return {
"urgency": urgency,
"urgency_note": note,
"months_to_purchase": months_to_purchase
}
def check_property_type(property_type: str) -> dict:
"""
Checks whether the brokerage handles the requested property type.
"""
supported_types = {"single_family", "condo", "townhouse", "waterfront"}
normalized = property_type.lower().replace(" ", "_").replace("-", "_")
status = "supported" if normalized in supported_types else "unsupported"
return {
"property_type": normalized,
"status": status,
"note": (
"We handle this property type."
if status == "supported"
else f"We do not currently list {property_type} properties. Refer out."
)
}
def execute_tool(tool_name: str, tool_input: dict) -> dict:
"""
Routes a tool call from Claude to the correct Python function.
"""
if tool_name == "check_budget":
return check_budget(**tool_input)
elif tool_name == "check_timeline":
return check_timeline(**tool_input)
elif tool_name == "check_property_type":
return check_property_type(**tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}
if __name__ == "__main__":
# Quick sanity check on all three tool functions
print(check_budget(1_250_000))
print(check_timeline(1))
print(check_property_type("Waterfront"))
Here's what you should see when you run this file directly:
sample output
{'budget_tier': 'high', 'budget_label': 'Luxury buyer. Priority client.', 'budget_usd': 1250000}
{'urgency': 'immediate', 'urgency_note': 'Hot lead. Contact within 24 hours.', 'months_to_purchase': 1}
{'property_type': 'waterfront', 'status': 'supported', 'note': 'We handle this property type.'}
Step 3: Build the Agentic Loop with Tool Use and Responses
This is where the agent behavior actually comes to life. The agentic loop is a while loop that keeps sending messages to Claude, handling any tool calls it makes, and continuing until Claude stops requesting tools and returns a final text response.
Most Claude tutorials skip this part or show it as pseudocode, so I want to walk through every line here. The stop condition is when Claude returns a message with stop_reason == "end_turn", which means it's done reasoning and has a final answer ready.
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# Reuse tool definitions and execute_tool from earlier steps
TOOLS = [
{
"name": "check_budget",
"description": (
"Evaluates whether the lead's stated budget falls within the brokerage's "
"serviceable range. Returns a qualification tier: 'high', 'mid', or 'low'."
),
"input_schema": {
"type": "object",
"properties": {
"budget_usd": {
"type": "number",
"description": "The lead's stated maximum purchase budget in US dollars."
}
},
"required": ["budget_usd"]
}
},
{
"name": "check_timeline",
"description": (
"Assesses how urgently the lead wants to purchase. "
"Returns 'immediate', 'near_term', or 'exploratory' based on their timeframe."
),
"input_schema": {
"type": "object",
"properties": {
"months_to_purchase": {
"type": "integer",
"description": "How many months until the lead intends to make a purchase."
}
},
"required": ["months_to_purchase"]
}
},
{
"name": "check_property_type",
"description": (
"Validates whether the lead's desired property type is one the brokerage represents. "
"Returns 'supported' or 'unsupported'."
),
"input_schema": {
"type": "object",
"properties": {
"property_type": {
"type": "string",
"description": (
"The type of property the lead wants. "
"Examples: 'single_family', 'condo', 'townhouse', 'land', 'commercial'."
)
}
},
"required": ["property_type"]
}
}
]
def check_budget(budget_usd: float) -> dict:
if budget_usd >= 1_000_000:
tier, label = "high", "Luxury buyer. Priority client."
elif budget_usd >= 400_000:
tier, label = "mid", "Strong buyer. Standard follow-up."
else:
tier, label = "low", "Below typical listing range. Refer to partner agency."
return {"budget_tier": tier, "budget_label": label, "budget_usd": budget_usd}
def check_timeline(months_to_purchase: int) -> dict:
if months_to_purchase <= 2:
urgency, note = "immediate", "Hot lead. Contact within 24 hours."
elif months_to_purchase <= 6:
urgency, note = "near_term", "Warm lead. Add to nurture sequence."
else:
urgency, note = "exploratory", "Early-stage. Add to long-term drip campaign."
return {"urgency": urgency, "urgency_note": note, "months_to_purchase": months_to_purchase}
def check_property_type(property_type: str) -> dict:
supported_types = {"single_family", "condo", "townhouse", "waterfront"}
normalized = property_type.lower().replace(" ", "_").replace("-", "_")
status = "supported" if normalized in supported_types else "unsupported"
return {
"property_type": normalized,
"status": status,
"note": (
"We handle this property type."
if status == "supported"
else f"We do not currently list {property_type} properties. Refer out."
)
}
def execute_tool(tool_name: str, tool_input: dict) -> dict:
if tool_name == "check_budget":
return check_budget(**tool_input)
elif tool_name == "check_timeline":
return check_timeline(**tool_input)
elif tool_name == "check_property_type":
return check_property_type(**tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}
def run_lead_qualifier(lead_info: str) -> str:
"""
Runs the full agentic loop for a single lead description.
Returns Claude's final qualification summary as a string.
"""
system_prompt = (
"You are a lead qualification agent for a real estate brokerage in Naples, Florida. "
"When given information about a potential buyer, use your available tools to check their "
"budget, purchase timeline, and desired property type. After calling all relevant tools, "
"summarize the qualification result clearly. Always call all three tools before responding."
)
# Start the conversation with the lead information
messages = [{"role": "user", "content": lead_info}]
print(f"\n--- Starting qualification for lead ---")
print(f"Input: {lead_info}\n")
# The agentic loop continues until Claude stops requesting tools
while True:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt,
tools=TOOLS,
messages=messages
)
# If Claude is done reasoning, extract the final text response
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return "No final response generated."
# If Claude wants to use tools, process each tool call
if response.stop_reason == "tool_use":
# Add Claude's response (including tool use requests) to message history
messages.append({"role": "assistant", "content": response.content})
# Build a list of tool results to send back in a single user message
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"Tool called: {block.name} with input {block.input}")
result = execute_tool(block.name, block.input)
print(f"Tool result: {result}\n")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # Must match the id Claude sent
"content": json.dumps(result)
})
# Send all tool results back to Claude in one message
messages.append({"role": "user", "content": tool_results})
else:
# Handle unexpected stop reasons gracefully
print(f"Unexpected stop_reason: {response.stop_reason}")
break
return "Agent loop ended without a final response."
if __name__ == "__main__":
lead = (
"Hi, my name is Sandra. I'm looking to buy a waterfront condo in Naples. "
"My budget is around $1.3 million and I'd like to be in something within the next 6 weeks."
)
result = run_lead_qualifier(lead)
print("=== FINAL QUALIFICATION RESULT ===")
print(result)
Here's the console output you should see when you run this:
sample output
--- Starting qualification for lead ---
Input: Hi, my name is Sandra. I'm looking to buy a waterfront condo in Naples.
My budget is around $1.3 million and I'd like to be in something within the next 6 weeks.
Tool called: check_budget with input {'budget_usd': 1300000}
Tool result: {'budget_tier': 'high', 'budget_label': 'Luxury buyer. Priority client.', 'budget_usd': 1300000}
Tool called: check_timeline with input {'months_to_purchase': 1}
Tool result: {'urgency': 'immediate', 'urgency_note': 'Hot lead. Contact within 24 hours.', 'months_to_purchase': 1}
Tool called: check_property_type with input {'property_type': 'waterfront condo'}
Tool result: {'property_type': 'waterfront_condo', 'status': 'unsupported', 'note': 'We do not currently list waterfront condo properties. Refer out.'}
=== FINAL QUALIFICATION RESULT ===
Sandra is a high-budget buyer ($1.3M) with an immediate purchase timeline (within 6 weeks),
which makes her a top-priority lead. However, her desired property type (waterfront condo)
is not currently in our supported listings portfolio. Recommend referring her to a partner
agency that specializes in waterfront condos, or checking if any upcoming listings match her needs.
tool_use_id in each tool result must exactly match the id Claude sent in its tool use block. If those IDs don't match, the API will return a validation error.
Step 4: Implement Scoring Logic and Lead Routing
The agentic loop gives us a narrative summary, but in a real system you also want a machine-readable score you can pipe into a CRM or a routing table. This final piece parses the tool results we already collected and computes a numeric score from 0 to 100 with a clear routing action.
I'm keeping the scoring weights simple and explicit so you can tune them without digging through logic. Budget accounts for 40 points, timeline for 35, and property type fit for 25.
lead_scorer.py
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
TOOLS = [
{
"name": "check_budget",
"description": (
"Evaluates whether the lead's stated budget falls within the brokerage's "
"serviceable range. Returns a qualification tier: 'high', 'mid', or 'low'."
),
"input_schema": {
"type": "object",
"properties": {
"budget_usd": {
"type": "number",
"description": "The lead's stated maximum purchase budget in US dollars."
}
},
"required": ["budget_usd"]
}
},
{
"name": "check_timeline",
"description": (
"Assesses how urgently the lead wants to purchase. "
"Returns 'immediate', 'near_term', or 'exploratory' based on their timeframe."
),
"input_schema": {
"type": "object",
"properties": {
"months_to_purchase": {
"type": "integer",
"description": "How many months until the lead intends to make a purchase."
}
},
"required": ["months_to_purchase"]
}
},
{
"name": "check_property_type",
"description": (
"Validates whether the lead's desired property type is one the brokerage represents. "
"Returns 'supported' or 'unsupported'."
),
"input_schema": {
"type": "object",
"properties": {
"property_type": {
"type": "string",
"description": (
"The type of property the lead wants. "
"Examples: 'single_family', 'condo', 'townhouse', 'land', 'commercial'."
)
}
},
"required": ["property_type"]
}
}
]
def check_budget(budget_usd: float) -> dict:
if budget_usd >= 1_000_000:
tier, label = "high", "Luxury buyer. Priority client."
elif budget_usd >= 400_000:
tier, label = "mid", "Strong buyer. Standard follow-up."
else:
tier, label = "low", "Below typical listing range. Refer to partner agency."
Naples AI