What You'll Build
If you've ever wondered how to build AI agents that actually do something useful, this is the tutorial for you. By the end of this guide, you'll have a fully working Python agent that takes raw real estate lead data, scores each prospect across three criteria — property value, purchase timeline, and motivation level — and returns a structured JSON output you can pipe directly into your CRM or dashboard.
This isn't a toy demo. It's the same pattern we use at Naples AI when building Claude API real estate automation for local brokerages. You'll use Anthropic's tool use feature to give Claude a set of qualification functions, then wire up an agent loop that calls those tools automatically.
The finished agent processes a lead in under three seconds and outputs a score from 0–100 with a human-readable summary. Let's build it.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
anthropicSDK installed:pip install anthropicpython-dotenvfor environment variables:pip install python-dotenv- Basic familiarity with Python classes and dictionaries
- About 20 minutes of focused time
Step 1: Set Up Your Claude API Client and Environment
First, create a .env file in your project root. This keeps your API key out of your source code — which matters when you push to GitHub or hand this off to a client.
ANTHROPIC_API_KEY=your_api_key_here
Now create the main agent file. The setup here is straightforward — load the key, initialize the client, and define the model you're targeting. We're using claude-sonnet-4-6 because it handles structured tool calls reliably and is fast enough for real-time lead qualification.
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
# Initialize the Anthropic client with your API key
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
# Quick sanity check — prints the model name on startup
if __name__ == "__main__":
print(f"Client initialized. Using model: {MODEL}")
Run this file with python lead_qualifier.py and you should see Client initialized. Using model: claude-sonnet-4-6 printed to your terminal. If you see an authentication error instead, double-check your .env file is in the same directory.
Step 2: Define Lead Qualification Tools
This is where the real work happens. Claude's tool use feature lets you define Python functions and tell the model they exist. When Claude decides it needs data from one of those functions, it returns a structured tool call that your code intercepts and executes.
We're defining three qualification tools: one for property value, one for purchase timeline, and one for buyer motivation. Each tool takes a raw lead input and returns a score from 0–10 with a reason. That keeps the scoring modular — you can add or remove criteria without rewriting the agent logic.
lead_qualifier.py (continued)import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
# Tool schemas tell Claude what functions are available and what arguments they expect
TOOLS = [
{
"name": "score_property_value",
"description": (
"Scores a lead based on their target property value range. "
"Higher-value properties represent larger commission opportunities. "
"Returns a score from 0 to 10 and a brief reason."
),
"input_schema": {
"type": "object",
"properties": {
"min_budget": {
"type": "number",
"description": "Minimum property budget in USD"
},
"max_budget": {
"type": "number",
"description": "Maximum property budget in USD"
}
},
"required": ["min_budget", "max_budget"]
}
},
{
"name": "score_purchase_timeline",
"description": (
"Scores a lead based on how soon they intend to purchase. "
"Shorter timelines indicate higher intent and urgency. "
"Returns a score from 0 to 10 and a brief reason."
),
"input_schema": {
"type": "object",
"properties": {
"timeline_months": {
"type": "integer",
"description": "How many months until the prospect plans to purchase"
}
},
"required": ["timeline_months"]
}
},
{
"name": "score_buyer_motivation",
"description": (
"Scores a lead based on their stated motivation for buying. "
"Evaluates factors like life events, financial readiness, and urgency signals. "
"Returns a score from 0 to 10 and a brief reason."
),
"input_schema": {
"type": "object",
"properties": {
"motivation_text": {
"type": "string",
"description": "The prospect's stated reason for wanting to buy"
},
"is_pre_approved": {
"type": "boolean",
"description": "Whether the prospect has mortgage pre-approval"
}
},
"required": ["motivation_text", "is_pre_approved"]
}
}
]
Notice that each tool has a description field. Claude reads these descriptions to decide which tool to call and when. Write them like you're explaining the function to a smart colleague — be specific about what the function measures and what it returns.
Step 3: Build the Agent Loop with Tool Use
Now we write the actual qualification functions that run when Claude calls a tool, and then we wire up the agent loop. The loop sends the lead data to Claude, waits for tool calls, executes them, sends the results back, and repeats until Claude has enough information to write a final summary.
This pattern — send, receive, execute, repeat — is the core of how to build AI agents with any API that supports tool use. Once you understand this loop, you can adapt it to almost any workflow.
lead_qualifier.py (continued)def score_property_value(min_budget: float, max_budget: float) -> dict:
"""Score a lead based on their property budget range."""
avg_budget = (min_budget + max_budget) / 2
if avg_budget >= 1_000_000:
score, reason = 10, "Luxury buyer — top priority for high-commission listings"
elif avg_budget >= 500_000:
score, reason = 8, "Strong mid-to-upper market buyer"
elif avg_budget >= 300_000:
score, reason = 6, "Solid mid-market — good volume potential"
elif avg_budget >= 150_000:
score, reason = 4, "Entry-level buyer — lower commission, still worth nurturing"
else:
score, reason = 2, "Below typical market threshold — low priority"
return {"score": score, "reason": reason, "avg_budget": avg_budget}
def score_purchase_timeline(timeline_months: int) -> dict:
"""Score a lead based on how soon they plan to buy."""
if timeline_months <= 1:
score, reason = 10, "Ready to buy now — immediate follow-up required"
elif timeline_months <= 3:
score, reason = 8, "Buying within 90 days — high intent"
elif timeline_months <= 6:
score, reason = 6, "6-month window — active nurture candidate"
elif timeline_months <= 12:
score, reason = 4, "12-month horizon — add to drip campaign"
else:
score, reason = 2, "Long timeline — low urgency, monitor only"
return {"score": score, "reason": reason, "timeline_months": timeline_months}
def score_buyer_motivation(motivation_text: str, is_pre_approved: bool) -> dict:
"""Score a lead based on their motivation and financial readiness."""
motivation_lower = motivation_text.lower()
score = 5 # start at neutral
# Strong positive signals
high_intent_keywords = ["relocating", "job transfer", "growing family", "retiring", "downsizing", "must move"]
for keyword in high_intent_keywords:
if keyword in motivation_lower:
score += 2
break
# Financial readiness is a major qualifier
if is_pre_approved:
score += 3
pre_approval_note = "Pre-approved — financing is confirmed"
else:
pre_approval_note = "Not pre-approved — financial risk factor"
score = min(score, 10) # cap at 10
reason = f"Motivation signals detected. {pre_approval_note}."
return {"score": score, "reason": reason, "is_pre_approved": is_pre_approved}
# Maps tool names to their actual Python functions
TOOL_FUNCTIONS = {
"score_property_value": score_property_value,
"score_purchase_timeline": score_purchase_timeline,
"score_buyer_motivation": score_buyer_motivation
}
def run_tool(tool_name: str, tool_input: dict) -> str:
"""Execute a tool by name and return its result as a JSON string."""
func = TOOL_FUNCTIONS.get(tool_name)
if not func:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
result = func(**tool_input)
return json.dumps(result)
class LeadQualifierAgent:
"""Agent that qualifies real estate leads using Claude and tool use."""
def __init__(self, client: anthropic.Anthropic, model: str, tools: list):
self.client = client
self.model = model
self.tools = tools
def qualify_lead(self, lead: dict) -> dict:
"""
Run the full qualification loop for a single lead.
Returns a dict with total_score, grade, summary, and per-criterion breakdown.
"""
system_prompt = (
"You are a real estate lead qualification specialist. "
"When given a prospect's details, use the available tools to score them "
"across three criteria: property value, purchase timeline, and buyer motivation. "
"After calling all three tools, respond with a JSON object containing: "
"total_score (0-100, weighted average), grade (A/B/C/D/F), "
"summary (one sentence), and scores (object with each criterion's score and reason). "
"Return ONLY the JSON object — no markdown, no extra text."
)
# Format the lead data into a clear prompt
user_message = (
f"Please qualify this real estate lead:\n\n"
f"Name: {lead.get('name', 'Unknown')}\n"
f"Budget: ${lead.get('min_budget', 0):,} - ${lead.get('max_budget', 0):,}\n"
f"Timeline: {lead.get('timeline_months', 12)} months\n"
f"Motivation: {lead.get('motivation', 'Not provided')}\n"
f"Pre-approved: {lead.get('is_pre_approved', False)}\n"
)
messages = [{"role": "user", "content": user_message}]
# Agent loop — keeps running until Claude stops calling tools
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=system_prompt,
tools=self.tools,
messages=messages
)
# If Claude is done and has no more tool calls, extract the final answer
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
try:
return json.loads(block.text)
except json.JSONDecodeError:
# Return raw text if JSON parsing fails
return {"summary": block.text, "total_score": 0, "grade": "F"}
break
# If Claude wants to call tools, process each one
if response.stop_reason == "tool_use":
# Add Claude's response (with tool calls) to the conversation history
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_output = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_output
})
# Send all tool results back to Claude in one message
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason — exit the loop to avoid infinite spinning
break
return {"error": "Agent loop ended without a final response", "total_score": 0, "grade": "F"}
stop_reason is "end_turn". Always include a fallback break for unexpected stop reasons — otherwise a bug in your tool definitions could cause an infinite loop that burns through API credits.
Step 4: Parse Prospect Data and Generate Scores
With the agent class built, we need a thin layer that accepts raw lead dictionaries, runs them through the agent, and formats the output. In a real deployment, this function would pull from your CRM API or a database query. For now, it takes a plain Python dict.
I also added a grade mapping here so the output is immediately actionable — an "A" lead goes to your top agent today, a "D" goes into a long-term drip, and so on.
lead_qualifier.py (continued)def calculate_grade(total_score: int) -> str:
"""Convert a numeric score to a letter grade for easy prioritization."""
if total_score >= 80:
return "A"
elif total_score >= 65:
return "B"
elif total_score >= 50:
return "C"
elif total_score >= 35:
return "D"
else:
return "F"
def format_lead_report(lead_name: str, result: dict) -> str:
"""Format the agent output into a readable console report."""
grade = result.get("grade", calculate_grade(result.get("total_score", 0)))
score = result.get("total_score", 0)
summary = result.get("summary", "No summary available.")
scores = result.get("scores", {})
lines = [
f"\n{'='*50}",
f" LEAD REPORT: {lead_name}",
f"{'='*50}",
f" Total Score : {score}/100",
f" Grade : {grade}",
f" Summary : {summary}",
f"\n --- CRITERION BREAKDOWN ---"
]
for criterion, data in scores.items():
if isinstance(data, dict):
lines.append(f" {criterion}: {data.get('score', 'N/A')}/10 — {data.get('reason', '')}")
lines.append(f"{'='*50}\n")
return "\n".join(lines)
def qualify_leads(leads: list[dict]) -> list[dict]:
"""Process a batch of leads and return their qualification results."""
agent = LeadQualifierAgent(client=client, model=MODEL, tools=TOOLS)
results = []
for lead in leads:
print(f"Qualifying: {lead.get('name', 'Unknown lead')}...")
result = agent.qualify_lead(lead)
# Attach lead metadata to the result for downstream use
result["lead_name"] = lead.get("name")
result["lead_email"] = lead.get("email", "")
# Print a formatted report to the console
print(format_lead_report(lead.get("name", "Unknown"), result))
results.append(result)
return results
Step 5: Test with Sample Real Estate Leads
Here's where everything comes together. I've included three sample leads that represent common scenarios you'd see in a Southwest Florida real estate market — a luxury buyer relocating from out of state, a first-time buyer with no pre-approval, and a retiree with a long timeline. These give you a good spread to verify your scoring logic is working correctly.
lead_qualifier.py (main block)if __name__ == "__main__":
# Sample leads representing common Southwest Florida buyer profiles
sample_leads = [
{
"name": "Margaret Thornton",
"email": "[email protected]",
"min_budget": 950_000,
"max_budget": 1_400_000,
"timeline_months": 2,
"motivation": "Relocating from Chicago due to job transfer to Naples area. Already sold our home.",
"is_pre_approved": True
},
{
"name": "Derek Santos",
"email": "[email protected]",
"min_budget": 220_000,
"max_budget": 280_000,
"timeline_months": 9,
"motivation": "First time buyer, just starting to look around and see what's out there.",
"is_pre_approved": False
},
{
"name": "Carol and Jim Pruitt",
"email": "[email protected]",
"min_budget": 600_000,
"max_budget": 800_000,
"timeline_months": 18,
"motivation": "Retiring in a year and a half and want to move to Florida eventually.",
"is_pre_approved": False
}
]
print("Starting Lead Qualification Agent...")
print(f"Processing {len(sample_leads)} leads with model: {MODEL}\n")
all_results = qualify_leads(sample_leads)
# Output raw JSON for CRM integration or downstream processing
print("\n--- RAW JSON OUTPUT (for CRM integration) ---")
print(json.dumps(all_results, indent=2))
Run the full script with python lead_qualifier.py. Here's an example of what the console output looks like for the first lead:
Starting Lead Qualification Agent... Processing 3 leads with model: claude-sonnet-4-6 Qualifying: Margaret Thornton... ================================================== LEAD REPORT: Margaret Thornton ================================================== Total Score : 92/100 Grade : A Summary : Pre-approved luxury relocator with immediate urgency — top priority lead. --- CRITERION BREAKDOWN --- property_value: 10/10 — Luxury buyer — top priority for high-commission listings purchase_timeline: 8/10 — Buying within 90 days — high intent buyer_motivation: 10/10 — Relocation signals detected. Pre-approved — financing is confirmed. ================================================== Qualifying: Derek Santos... ================================================== LEAD REPORT: Derek Santos ================================================== Total Score : 43/100 Grade : D Summary : Entry-level first-time buyer with no pre-approval and a 9-month window — add to nurture campaign. --- CRITERION BREAKDOWN --- property_value: 4/10 — Entry-level buyer — lower commission, still worth nurturing purchase_timeline: 4/10 — 12-month horizon — add to drip campaign buyer_motivation: 5/10 — No strong urgency signals. Not pre-approved — financial risk factor. ================================================== Qualifying: Carol and Jim Pruitt... ================================================== LEAD REPORT: Carol and Jim Pruitt ================================================== Total Score : 58/100 Grade : C Summary : Mid-market retirees with a long timeline but solid budget — good long-term nurture candidate. --- CRITERION BREAKDOWN --- property_value: 8/10 — Strong mid-to-upper market buyer purchase_timeline: 2/10 — Long timeline — low urgency, monitor only buyer_motivation: 7/10 — Retirement signals detected. Not pre-approved — financial risk factor. ==================================================
How It Works
Here's the plain-English version of what just happened. When you call qualify_lead(), it sends the lead data to Claude along with your three tool definitions. Claude reads the lead details and decides it needs to call all three scoring tools to do its job.
Claude doesn't execute those tools itself — it just tells your code which tool to call and with what arguments. Your agent loop intercepts that, runs the actual Python function, and sends the result back to Claude. Claude then has all three scores and writes a final JSON summary.