If you're running a real estate business in Southwest Florida, you already know the problem: your team spends half the day chasing leads that were never serious buyers to begin with. Manually qualifying every inbound inquiry — sorting the curious browsers from the motivated buyers with real budgets — is slow, inconsistent, and expensive. This tutorial shows you exactly how to fix that with a Claude API lead qualifier agent built in Python.
By the end of this walkthrough, you'll have a working AI agent that automatically extracts contact details, property preferences, intent signals, and budget information from raw lead conversations — then scores each lead and returns a structured qualification report. No fluff, no theory. Just code that runs.
lead_qualifier.py. The full runnable version will be complete by the end of Step 5.
1. What You'll Build
You're building a Python-based AI agent that takes a raw lead description — the kind of messy notes your sales team types into a CRM — and runs it through Claude's tool use system to extract structured data across four dimensions: contact info, property preferences, purchase intent, and budget.
The agent scores each lead from 0–100 and returns a clean JSON report your team can act on immediately. It uses the Anthropic SDK with claude-sonnet-4-6, streaming responses, and a proper agentic loop that handles multi-turn tool calls automatically.
2. Prerequisites
- Python 3.10 or higher
- An Anthropic API key (get one at console.anthropic.com)
anthropicPython SDK installed (pip install anthropic)- Basic Python knowledge — you don't need to be an AI expert
- A terminal and a text editor (VS Code works great)
3. Full Source Code
lead_qualifier.py, in the order they appear, and you'll have a fully working agent. Every snippet is syntactically correct and tested.
4. Step 1: Set Up Your Claude API Client and Environment
First, let's install the SDK and set up your API key. Never hardcode your key in source files — use an environment variable instead.
Run this in your terminal to install the dependency and export your key:
terminalpip install anthropic # Mac/Linux export ANTHROPIC_API_KEY="sk-ant-your-key-here" # Windows PowerShell $env:ANTHROPIC_API_KEY="sk-ant-your-key-here"
Now create your file and set up the client. This is the foundation everything else plugs into:
lead_qualifier.pyimport os
import json
import anthropic
# Initialize the Anthropic client using the ANTHROPIC_API_KEY env variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
# This will hold our conversation turns for the agentic loop
conversation_history = []
That's all you need to start. The client automatically reads your API key from the environment, which is the right way to handle secrets in any production codebase.
5. Step 2: Define Lead Qualification Tools and Schemas
This is where the magic happens. Claude's tool use feature lets you define structured functions that the model can call when it identifies relevant information. Think of it like handing Claude a set of forms to fill out as it reads the lead.
We're defining four tools: one for each qualification dimension. Each tool has a strict JSON schema so the output is always machine-readable.
lead_qualifier.py (continued)# Tool definitions tell Claude what structured data to extract
QUALIFICATION_TOOLS = [
{
"name": "capture_contact_info",
"description": (
"Extract contact information for the lead including name, "
"email, phone number, and preferred contact method."
),
"input_schema": {
"type": "object",
"properties": {
"full_name": {
"type": "string",
"description": "Full name of the lead"
},
"email": {
"type": "string",
"description": "Email address if provided"
},
"phone": {
"type": "string",
"description": "Phone number if provided"
},
"preferred_contact": {
"type": "string",
"enum": ["email", "phone", "text", "unknown"],
"description": "Preferred method of contact"
}
},
"required": ["full_name", "preferred_contact"]
}
},
{
"name": "capture_property_preferences",
"description": (
"Extract what type of property the lead is looking for, "
"including location, size, property type, and must-have features."
),
"input_schema": {
"type": "object",
"properties": {
"property_type": {
"type": "string",
"enum": ["single_family", "condo", "townhouse", "land", "commercial", "unknown"],
"description": "Type of property the lead wants"
},
"target_location": {
"type": "string",
"description": "Desired city, neighborhood, or zip code"
},
"bedrooms_min": {
"type": "integer",
"description": "Minimum number of bedrooms needed"
},
"bathrooms_min": {
"type": "number",
"description": "Minimum number of bathrooms needed"
},
"must_have_features": {
"type": "array",
"items": {"type": "string"},
"description": "Non-negotiable features like pool, garage, waterfront"
},
"square_footage_min": {
"type": "integer",
"description": "Minimum square footage if mentioned"
}
},
"required": ["property_type", "target_location"]
}
},
{
"name": "capture_purchase_intent",
"description": (
"Assess how serious and motivated the lead is about purchasing, "
"including their timeline, whether they are pre-approved, "
"and whether they are currently working with another agent."
),
"input_schema": {
"type": "object",
"properties": {
"timeline": {
"type": "string",
"enum": ["immediate", "1_to_3_months", "3_to_6_months", "6_plus_months", "unknown"],
"description": "How soon the lead wants to purchase"
},
"is_pre_approved": {
"type": "boolean",
"description": "Whether the lead has mortgage pre-approval"
},
"has_existing_agent": {
"type": "boolean",
"description": "Whether the lead is already working with another agent"
},
"motivation_notes": {
"type": "string",
"description": "Any context about why they are buying — relocation, investment, lifestyle"
},
"is_also_selling": {
"type": "boolean",
"description": "Whether the lead also needs to sell an existing property"
}
},
"required": ["timeline", "is_pre_approved", "has_existing_agent"]
}
},
{
"name": "capture_budget_info",
"description": (
"Extract the lead's budget range, financing method, "
"and any flexibility signals around price."
),
"input_schema": {
"type": "object",
"properties": {
"budget_min": {
"type": "integer",
"description": "Minimum budget in USD"
},
"budget_max": {
"type": "integer",
"description": "Maximum budget in USD"
},
"financing_type": {
"type": "string",
"enum": ["cash", "conventional", "fha", "va", "unknown"],
"description": "How the lead plans to finance the purchase"
},
"is_budget_flexible": {
"type": "boolean",
"description": "Whether the lead indicated flexibility above their stated max"
},
"down_payment_ready": {
"type": "boolean",
"description": "Whether the lead has confirmed they have a down payment ready"
}
},
"required": ["budget_max", "financing_type"]
}
}
]
500000. The schema forces structured, typed output your code can actually use without fragile string parsing.
6. Step 3: Build the Agent Loop with tool_use
This is the core of the agent. The loop sends the lead description to Claude, receives tool call requests, executes the tools, feeds the results back, and keeps going until Claude signals it's done.
Claude will often call multiple tools in a single pass — that's expected behavior. The loop handles all of it.
lead_qualifier.py (continued)def run_qualification_agent(lead_description: str) -> dict:
"""
Run the full agentic loop to qualify a lead.
Returns a dict of all extracted tool results.
"""
# Collected tool outputs stored by tool name
tool_results_store = {}
system_prompt = """You are an expert real estate lead qualification assistant.
Your job is to analyze lead descriptions and extract structured information
using the tools available to you. Be thorough — call all relevant tools
even if some information is incomplete or inferred. If a detail is not
mentioned, use your best judgment based on context clues or mark it unknown.
Always call all four tools for every lead."""
messages = [
{
"role": "user",
"content": f"Please qualify this real estate lead:\n\n{lead_description}"
}
]
# Agentic loop — runs until Claude stops calling tools
while True:
response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=system_prompt,
tools=QUALIFICATION_TOOLS,
messages=messages
)
# Append Claude's response to the conversation
messages.append({"role": "assistant", "content": response.content})
# Check if we are done (no more tool calls)
if response.stop_reason == "end_turn":
break
# Process any tool use blocks in the response
tool_use_blocks = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_use_blocks:
break
# Build the tool results to send back to Claude
tool_results = []
for tool_use in tool_use_blocks:
tool_name = tool_use.name
tool_input = tool_use.input
# Store the extracted data — this is our qualification output
tool_results_store[tool_name] = tool_input
# Send a success acknowledgment back to Claude for each tool call
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps({"status": "captured", "data": tool_input})
})
# Add tool results back into the conversation so Claude can continue
messages.append({
"role": "user",
"content": tool_results
})
return tool_results_store
The while True loop is intentional. Claude might call one tool, then decide it needs more information before calling another. We keep the loop running until stop_reason is "end_turn", which is Claude's signal that it's finished.
7. Step 4: Implement Lead Scoring Logic
Raw extracted data is useful, but your sales team needs to know who to call first. This scoring function turns the structured tool outputs into a numeric score from 0–100.
The scoring weights are based on what actually drives close rates in real estate: pre-approval status, timeline, and budget clarity matter most.
lead_qualifier.py (continued)def score_lead(tool_results: dict) -> dict:
"""
Score a lead from 0-100 based on qualification signals.
Returns a score dict with breakdown and tier label.
"""
score = 0
breakdown = {}
# --- Contact completeness (max 15 points) ---
contact = tool_results.get("capture_contact_info", {})
contact_score = 0
if contact.get("email"):
contact_score += 7
if contact.get("phone"):
contact_score += 8
score += contact_score
breakdown["contact_completeness"] = contact_score
# --- Purchase intent signals (max 40 points) ---
intent = tool_results.get("capture_purchase_intent", {})
intent_score = 0
timeline = intent.get("timeline", "unknown")
timeline_points = {
"immediate": 20,
"1_to_3_months": 15,
"3_to_6_months": 8,
"6_plus_months": 3,
"unknown": 0
}
intent_score += timeline_points.get(timeline, 0)
# Pre-approval is the strongest buying signal
if intent.get("is_pre_approved"):
intent_score += 15
# Already has an agent is a negative signal
if intent.get("has_existing_agent"):
intent_score -= 10
intent_score = max(0, intent_score)
score += intent_score
breakdown["purchase_intent"] = intent_score
# --- Budget clarity (max 30 points) ---
budget = tool_results.get("capture_budget_info", {})
budget_score = 0
if budget.get("budget_max"):
budget_score += 15
if budget.get("budget_min"):
budget_score += 5
financing = budget.get("financing_type", "unknown")
if financing == "cash":
budget_score += 10
elif financing in ("conventional", "va", "fha"):
budget_score += 6
if budget.get("down_payment_ready"):
budget_score += 5
budget_score = min(30, budget_score)
score += budget_score
breakdown["budget_clarity"] = budget_score
# --- Property specificity (max 15 points) ---
prefs = tool_results.get("capture_property_preferences", {})
property_score = 0
if prefs.get("target_location") and prefs["target_location"].lower() != "unknown":
property_score += 7
if prefs.get("bedrooms_min"):
property_score += 4
if prefs.get("must_have_features"):
property_score += 4
score += property_score
breakdown["property_specificity"] = property_score
# Clamp the final score to 0-100
final_score = max(0, min(100, score))
# Assign a human-readable tier
if final_score >= 75:
tier = "HOT"
elif final_score >= 50:
tier = "WARM"
elif final_score >= 25:
tier = "COLD"
else:
tier = "UNQUALIFIED"
return {
"score": final_score,
"tier": tier,
"breakdown": breakdown
}
timeline_points and bonus values based on your own close-rate data.
8. Step 5: Parse and Format Qualified Lead Output
The last piece ties everything together: a function that calls the agent, scores the result, and formats a clean report you can print, log, or push into your CRM via API.
This is also where we add a realistic sample lead and run the whole thing end to end.
lead_qualifier.py (continued)def format_qualification_report(
lead_description: str,
tool_results: dict,
score_data: dict
) -> str:
"""
Format a human-readable qualification report from extracted data.
"""
contact = tool_results.get("capture_contact_info", {})
prefs = tool_results.get("capture_property_preferences", {})
intent = tool_results.get("capture_purchase_intent", {})
budget = tool_results.get("capture_budget_info", {})
lines = [
"=" * 55,
f" LEAD QUALIFICATION REPORT",
f" Score: {score_data['score']}/100 | Tier: {score_data['tier']}",
"=" * 55,
"",
"CONTACT",
f" Name: {contact.get('full_name', 'Unknown')}",
f" Email: {contact.get('email', 'Not provided')}",
f" Phone: {contact.get('phone', 'Not provided')}",
f" Prefers: {contact.get('preferred_contact', 'unknown')}",
"",
"PROPERTY PREFERENCES",
f" Type: {prefs.get('property_type', 'unknown')}",
f" Location: {prefs.get('target_location', 'unknown')}",
f" Beds: {prefs.get('bedrooms_min', 'any')}+",
f" Baths: {prefs.get('bathrooms_min', 'any')}+",
f" Must-haves: {', '.join(prefs.get('must_have_features', [])) or 'None stated'}",
"",
"INTENT",
f" Timeline: {intent.get('timeline', 'unknown')}",
f" Pre-approved: {'Yes' if intent.get('is_pre_approved') else 'No'}",
f" Has agent: {'Yes' if intent.get('has_existing_agent') else 'No'}",
f" Notes: {intent.get('motivation_notes', 'None')}",
"",
"BUDGET",
f" Range: ${budget.get('budget_min', 0):,} – ${budget.get('budget_max', 0):,}",
f" Financing: {budget.get('financing_type', 'unknown')}",
f" Down pmt: {'Ready' if budget.get('down_payment_ready') else 'Not confirmed'}",
f" Flexible: {'Yes' if budget.get('is_budget_flexible') else 'No'}",
"",
"SCORE BREAKDOWN",
]
for category, points in score_data["breakdown"].items():
lines.append(f" {category.replace('_', ' ').title()}: {points} pts")
lines.append("=" * 55)
return "\n".join(lines)
def qualify_lead(lead_description: str) -> None:
"""
Main entry point — qualifies a lead and prints the full report.
"""
print(f"\nQualifying lead... (this may take a few seconds)\n")
# Run the agentic loop to extract structured data
tool_results = run_qualification_agent(lead_description)
# Score the extracted data
score_data = score_lead(tool_results)
# Format and print the report
report = format_qualification_report(lead_description, tool_results, score_data)
print(report)
# Also dump the raw JSON for CRM integration
print("\nRAW JSON OUTPUT (for CRM/webhook):")
output = {
"score": score_data["score"],
"tier": score_data["tier"],
"breakdown": score_data["breakdown"],
"extracted_data": tool_results
}
print(json.dumps(output, indent=2))
# --- Run it with a sample lead ---
if __name__ == "__main__":
sample_lead = """
Hey, my name is Maria Gonzalez. I found your listing on Zillow for that
waterfront place in Naples and I'm really interested. My husband and I
are looking to relocate from Chicago — he just got a remote job so we
want to make the move within the next 6-8 weeks. We've been pre-approved
for a conventional loan up to $1.2 million but honestly if the right place
came up we could probably go a bit higher. We're looking for at least
3 bedrooms, 2 bathrooms, and a pool is non-negotiable. We're not working
with an agent yet. You can reach me by email at [email protected] or