What You'll Build
By the end of this tutorial, you'll have a working Python agent that takes raw real estate inquiry data — think name, budget, property type, timeline — and automatically scores, classifies, and routes each lead without a human touching it. The agent uses Claude's tool-use API to pull property data, run scoring logic, and decide whether a lead goes to a senior agent, a drip campaign, or gets flagged as unqualified. This is the exact same pattern we use at Naples AI when we build lead automation pipelines for Southwest Florida real estate teams.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (console.anthropic.com)
anthropicPython SDK installed:pip install anthropic- Basic understanding of Python classes and dictionaries
- Familiarity with how REST APIs work (you don't need to be an expert)
Step 1: Set Up Your Claude API Client and Authentication
First things first — let's get the Anthropic client wired up and make sure authentication works before we write a single line of agent logic. A failed auth call at the start will save you debugging headaches later.
Create a file called lead_qualifier.py and drop in the setup below. We're using claude-sonnet-4-5 as the model because it handles tool use reliably and is fast enough for a qualification loop that needs to feel snappy.
import os
import json
import anthropic
# Load your API key from an environment variable — never hardcode it
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
if not ANTHROPIC_API_KEY:
raise EnvironmentError("ANTHROPIC_API_KEY environment variable is not set.")
# Initialize the Anthropic client
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
# Model constant — swap here if you want to test other models
MODEL = "claude-sonnet-4-5"
print("✅ Anthropic client initialized successfully.")Set your key in the terminal before running: export ANTHROPIC_API_KEY="sk-ant-...". Run this snippet alone first to confirm the client initializes without errors. Once you see the success message, move on.
Step 2: Define Tool Schemas for Property Data and Lead Scoring
This is where the real power of Claude's tool-use API comes in. We're going to define two tools: one that looks up property market data for a given area, and one that computes a lead score based on the inquiry details. Claude will decide when to call each tool during the qualification loop.
Think of the tool schema like a job description — you tell Claude what the tool does, what inputs it needs, and what type those inputs should be. Claude handles the rest.
lead_qualifier.py — Part 2: Tool Definitionsimport os
import json
import anthropic
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
MODEL = "claude-sonnet-4-5"
# Tool schema definitions — Claude reads these to decide when and how to call each tool
TOOLS = [
{
"name": "get_property_market_data",
"description": (
"Retrieves current market data for a given city or zip code, "
"including median home price, average days on market, and inventory level. "
"Use this before scoring to understand if the lead's budget is realistic."
),
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or ZIP code, e.g. 'Naples, FL' or '34102'"
},
"property_type": {
"type": "string",
"enum": ["single_family", "condo", "townhouse", "land", "multi_family"],
"description": "The type of property the lead is inquiring about"
}
},
"required": ["location", "property_type"]
}
},
{
"name": "calculate_lead_score",
"description": (
"Scores a real estate lead from 0 to 100 based on budget alignment, "
"timeline urgency, pre-approval status, and engagement signals. "
"Returns a score, a tier label (hot/warm/cold), and a recommended routing action."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {
"type": "number",
"description": "Lead's stated max budget in USD"
},
"median_market_price": {
"type": "number",
"description": "Median price for the requested property type in their area"
},
"timeline_months": {
"type": "integer",
"description": "How many months until the lead wants to purchase"
},
"pre_approved": {
"type": "boolean",
"description": "Whether the lead has mortgage pre-approval"
},
"inquiry_source": {
"type": "string",
"enum": ["website_form", "zillow", "referral", "cold_outreach", "open_house"],
"description": "Where this lead originated"
}
},
"required": ["budget", "median_market_price", "timeline_months", "pre_approved", "inquiry_source"]
}
}
]Notice that both tools have explicit required arrays. If you leave those out, Claude may try to call a tool without all the data it needs and you'll get incomplete results. Always define required fields.
Step 3: Build the Lead Qualifier Agent Class with Tool Use
Now we build the agent class itself. This class holds the client, the tools, and the functions that actually execute when Claude decides to call a tool. The key insight here: Claude doesn't run your Python functions — it just tells you which tool to call and with what arguments. You run the function yourself and pass the result back.
lead_qualifier.py — Part 3: Agent Classimport os
import json
import anthropic
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
MODEL = "claude-sonnet-4-5"
TOOLS = [
{
"name": "get_property_market_data",
"description": (
"Retrieves current market data for a given city or zip code, "
"including median home price, average days on market, and inventory level. "
"Use this before scoring to understand if the lead's budget is realistic."
),
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name or ZIP code"},
"property_type": {
"type": "string",
"enum": ["single_family", "condo", "townhouse", "land", "multi_family"]
}
},
"required": ["location", "property_type"]
}
},
{
"name": "calculate_lead_score",
"description": (
"Scores a real estate lead from 0 to 100 based on budget alignment, "
"timeline urgency, pre-approval status, and engagement signals."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {"type": "number", "description": "Lead's stated max budget in USD"},
"median_market_price": {"type": "number", "description": "Median price for the area"},
"timeline_months": {"type": "integer", "description": "Months until purchase"},
"pre_approved": {"type": "boolean", "description": "Has mortgage pre-approval"},
"inquiry_source": {
"type": "string",
"enum": ["website_form", "zillow", "referral", "cold_outreach", "open_house"]
}
},
"required": ["budget", "median_market_price", "timeline_months", "pre_approved", "inquiry_source"]
}
}
]
class RealEstateLeadQualifier:
"""Agent that qualifies real estate leads using Claude's tool-use API."""
def __init__(self):
self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
self.model = MODEL
self.tools = TOOLS
# Simulated market data — in production, replace with an MLS API call
self.market_data = {
("Naples, FL", "single_family"): {"median_price": 850000, "days_on_market": 45, "inventory": "low"},
("Naples, FL", "condo"): {"median_price": 420000, "days_on_market": 60, "inventory": "moderate"},
("Bonita Springs, FL", "single_family"): {"median_price": 620000, "days_on_market": 38, "inventory": "low"},
("Marco Island, FL", "single_family"): {"median_price": 1200000, "days_on_market": 72, "inventory": "low"},
("Fort Myers, FL", "townhouse"): {"median_price": 310000, "days_on_market": 30, "inventory": "moderate"},
}
def get_property_market_data(self, location: str, property_type: str) -> dict:
"""Returns market data for the given location and property type."""
key = (location, property_type)
data = self.market_data.get(key, {
"median_price": 500000,
"days_on_market": 50,
"inventory": "unknown"
})
return {
"location": location,
"property_type": property_type,
"median_price": data["median_price"],
"days_on_market": data["days_on_market"],
"inventory_level": data["inventory"]
}
def calculate_lead_score(
self,
budget: float,
median_market_price: float,
timeline_months: int,
pre_approved: bool,
inquiry_source: str
) -> dict:
"""Scores a lead and returns tier + routing recommendation."""
score = 0
# Budget alignment — max 40 points
budget_ratio = budget / median_market_price if median_market_price > 0 else 0
if budget_ratio >= 0.95:
score += 40
elif budget_ratio >= 0.80:
score += 25
elif budget_ratio >= 0.60:
score += 10
# Timeline urgency — max 25 points
if timeline_months <= 3:
score += 25
elif timeline_months <= 6:
score += 15
elif timeline_months <= 12:
score += 8
# Pre-approval — max 20 points
if pre_approved:
score += 20
# Source quality — max 15 points
source_scores = {
"referral": 15,
"open_house": 12,
"website_form": 10,
"zillow": 6,
"cold_outreach": 2
}
score += source_scores.get(inquiry_source, 5)
# Determine tier and routing
if score >= 70:
tier = "hot"
routing = "assign_senior_agent"
elif score >= 40:
tier = "warm"
routing = "drip_campaign_plus_followup"
else:
tier = "cold"
routing = "automated_nurture_sequence"
return {
"score": score,
"tier": tier,
"routing_action": routing,
"budget_ratio": round(budget_ratio, 2)
}
def execute_tool(self, tool_name: str, tool_input: dict) -> str:
"""Dispatches tool calls from Claude to the right Python function."""
if tool_name == "get_property_market_data":
result = self.get_property_market_data(
location=tool_input["location"],
property_type=tool_input["property_type"]
)
elif tool_name == "calculate_lead_score":
result = self.calculate_lead_score(
budget=tool_input["budget"],
median_market_price=tool_input["median_market_price"],
timeline_months=tool_input["timeline_months"],
pre_approved=tool_input["pre_approved"],
inquiry_source=tool_input["inquiry_source"]
)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Claude expects tool results as strings
return json.dumps(result)Step 4: Implement the Qualification Loop and Decision Logic
The agentic loop is the heart of this whole thing. We send Claude the lead data and let it decide which tools to call, in what order, until it has enough information to produce a final qualification summary. The loop runs until Claude returns a stop_reason of "end_turn", which means it's done reasoning and is ready to give us an answer.
We also cap the loop at 10 iterations to prevent runaway API calls — a simple but important guardrail.
lead_qualifier.py — Part 4: Agentic Loop def qualify_lead(self, lead: dict) -> dict:
"""
Runs the full qualification loop for a single lead.
Returns a dict with the lead's score, tier, routing, and Claude's summary.
"""
# Build the initial prompt with the lead's raw inquiry data
system_prompt = (
"You are a real estate lead qualification specialist. "
"Your job is to analyze incoming property inquiries, use the available tools "
"to gather market data and compute a lead score, then produce a clear qualification "
"summary with a routing recommendation. Always call get_property_market_data first, "
"then calculate_lead_score with the market data you retrieved."
)
user_message = (
f"Please qualify the following real estate lead:\n\n"
f"Name: {lead['name']}\n"
f"Email: {lead['email']}\n"
f"Interested In: {lead['property_type']} in {lead['location']}\n"
f"Budget: ${lead['budget']:,}\n"
f"Timeline: {lead['timeline_months']} months\n"
f"Pre-Approved: {'Yes' if lead['pre_approved'] else 'No'}\n"
f"Inquiry Source: {lead['inquiry_source']}\n"
f"Notes: {lead.get('notes', 'None')}\n\n"
f"Use your tools to score this lead and give me a qualification summary."
)
messages = [{"role": "user", "content": user_message}]
max_iterations = 10 # Safety cap to prevent infinite loops
iteration = 0
final_score_data = {}
while iteration < max_iterations:
iteration += 1
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=system_prompt,
tools=self.tools,
messages=messages
)
# Add Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
# If Claude is done, extract the final text and exit the loop
if response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text = block.text
break
return {
"lead_name": lead["name"],
"lead_email": lead["email"],
"score": final_score_data.get("score", 0),
"tier": final_score_data.get("tier", "unknown"),
"routing_action": final_score_data.get("routing_action", "manual_review"),
"budget_ratio": final_score_data.get("budget_ratio", 0),
"claude_summary": final_text,
"iterations": iteration
}
# If Claude wants to use a tool, execute it and feed results back
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_result_str = self.execute_tool(block.name, block.input)
# Capture score data so we can include it in the return dict
if block.name == "calculate_lead_score":
final_score_data = json.loads(tool_result_str)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result_str
})
# Return the tool results to Claude so it can continue reasoning
messages.append({"role": "user", "content": tool_results})
# If we somehow exhaust iterations, return what we have
return {
"lead_name": lead["name"],
"lead_email": lead["email"],
"score": final_score_data.get("score", 0),
"tier": final_score_data.get("tier", "unknown"),
"routing_action": final_score_data.get("routing_action", "manual_review"),
"claude_summary": "Max iterations reached. Manual review recommended.",
"iterations": iteration
}Step 5: Add Lead Routing Based on Qualification Scores
The last piece is the routing layer — a function that takes qualified leads and actually does something with each tier. For now we're printing the routing actions, but this is where you'd plug in your CRM webhook, your email platform, or your internal Slack alert.
We also add the main() block with example leads so you can run the whole thing end to end right now.
import os
import json
import anthropic
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
MODEL = "claude-sonnet-4-5"
TOOLS = [
{
"name": "get_property_market_data",
"description": (
"Retrieves current market data for a given city or zip code, "
"including median home price, average days on market, and inventory level."
),
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"},
"property_type": {
"type": "string",
"enum": ["single_family", "condo", "townhouse", "land", "multi_family"]
}
},
"required": ["location", "property_type"]
}
},
{
"name": "calculate_lead_score",
"description": "Scores a real estate lead from 0 to 100 and returns tier + routing action.",
"input_schema": {
"type": "object",
"properties": {
"budget": {"type": "number"},
"median_market_price": {"type": "number"},
"timeline_months": {"type": "integer"},
"pre_approved": {"type": "boolean"},
"inquiry_source": {
"type": "string",
"enum": ["website_form", "zillow", "referral", "cold_outreach", "open_house"]
}
},
"required": ["budget", "median_market_price", "timeline_months", "pre_approved", "inquiry_source"]
}
}
]
class RealEstateLeadQualifier:
def __init__(self):
self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
self.model = MODEL
self.tools = TOOLS
self.market_data = {
("Naples, FL", "single_family"): {"median_price": 850000, "days_on_market": 45, "inventory": "low"},
("Naples, FL", "condo"): {"median_price": 420000, "days_on_market": 60, "inventory": "moderate"},
("Bonita Springs, FL", "single_family"): {"median_price": 620000, "days_on_market": 38, "inventory": "low"},
("Marco Island, FL", "single_family"): {"median_price": 1200000, "days_on_market": 72, "inventory": "low"},
("Fort Myers, FL", "townhouse"): {"median_price": 310000, "days_on_market": 30, "inventory": "moderate"},
}
def get_property_market_data(self, location: str, property_type: str) -> dict:
key = (location, property_type)
data = self.market_data.get(key, {"median_price": 500000, "days_on_market": 50, "inventory": "unknown"})
return {
"location": location,
"property_type": property_type,
"median_price": data["median_price"],
"days_on_market": data["days_on_market"],
"inventory_level": data["inventory"]
}