What You'll Build
If you've ever watched a real estate team manually sort through dozens of inbound leads trying to figure out who's actually serious, you know how much time that wastes. In this tutorial, you'll build a Python-based AI lead qualifier that holds a multi-turn conversation with a prospect, extracts key qualification signals, and outputs a scored ranking — all using the Claude API.
By the end, you'll have a production-ready agent that asks about budget, timeline, and location preferences, then classifies leads as hot, warm, or cold with a numeric score. No machine learning background required — just Python and an Anthropic API key.
The complete, working code for this project is broken into steps below. Each snippet builds on the last, so by the time you reach Step 4, you'll have a fully functional lead qualifier. Copy the pieces together into one file as you go, or grab the full combined version from Step 4.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicPython SDK installed (pip install anthropic)- A
.envfile or environment variable set forANTHROPIC_API_KEY
Step 1: Set Up the Claude API Client and Authentication
First things first — let's get the Anthropic client initialized and make sure authentication is working before we write a single line of agent logic. This keeps the setup isolated and easy to debug.
Create a new file called lead_qualifier.py and start with this:
import os
import json
from anthropic import Anthropic
# Load API key from environment — never hardcode this
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
if not ANTHROPIC_API_KEY:
raise EnvironmentError(
"ANTHROPIC_API_KEY not found. Set it in your environment or .env file."
)
client = Anthropic(api_key=ANTHROPIC_API_KEY)
# Quick sanity check — confirms auth works before building anything on top of it
def test_connection():
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=64,
messages=[{"role": "user", "content": "Say 'API connected' and nothing else."}]
)
print(response.content[0].text)
if __name__ == "__main__":
test_connection()Run this with python lead_qualifier.py. You should see API connected printed to your terminal. If you get an authentication error, double-check that your environment variable is set correctly in the same shell session you're running Python from.
python-dotenv to load a .env file automatically. Just run pip install python-dotenv and add from dotenv import load_dotenv; load_dotenv() at the top of your file before reading the environment variable.
Step 2: Define Lead Qualification Criteria and Tool Schemas
This is where things get interesting. Instead of parsing free-form text with regex or if-statements, we're going to give Claude structured tools it can call when it collects a qualifying data point. Think of tools as forms that Claude fills out as the conversation progresses.
We need three core qualification signals for real estate: budget, timeline, and target location. Here's how we define them as tool schemas the Anthropic SDK understands:
lead_qualifier.py (add below the imports)import os
import json
from anthropic import Anthropic
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
if not ANTHROPIC_API_KEY:
raise EnvironmentError("ANTHROPIC_API_KEY not found.")
client = Anthropic(api_key=ANTHROPIC_API_KEY)
# Tool definitions — these are the structured signals our agent extracts
QUALIFICATION_TOOLS = [
{
"name": "record_budget",
"description": (
"Call this tool when the prospect has stated or clearly implied "
"their purchase budget for a property. Extract the numeric range."
),
"input_schema": {
"type": "object",
"properties": {
"min_budget": {
"type": "number",
"description": "Minimum budget in USD"
},
"max_budget": {
"type": "number",
"description": "Maximum budget in USD"
},
"budget_confidence": {
"type": "string",
"enum": ["firm", "flexible", "uncertain"],
"description": "How confident the prospect seems about their budget"
}
},
"required": ["min_budget", "max_budget", "budget_confidence"]
}
},
{
"name": "record_timeline",
"description": (
"Call this tool when the prospect has indicated when they want "
"to buy or move. Capture urgency so we can score lead priority."
),
"input_schema": {
"type": "object",
"properties": {
"timeline_months": {
"type": "number",
"description": "How many months until the prospect wants to complete a purchase"
},
"is_pre_approved": {
"type": "boolean",
"description": "Whether the prospect has mortgage pre-approval"
}
},
"required": ["timeline_months", "is_pre_approved"]
}
},
{
"name": "record_location",
"description": (
"Call this tool when the prospect mentions specific neighborhoods, "
"cities, or geographic preferences for their property search."
),
"input_schema": {
"type": "object",
"properties": {
"preferred_areas": {
"type": "array",
"items": {"type": "string"},
"description": "List of neighborhoods, cities, or zip codes the prospect mentioned"
},
"flexibility": {
"type": "string",
"enum": ["specific", "somewhat_flexible", "open"],
"description": "How flexible the prospect is on location"
}
},
"required": ["preferred_areas", "flexibility"]
}
},
{
"name": "finalize_qualification",
"description": (
"Call this tool when you have collected enough information to "
"make a qualification decision. Use this to end the conversation."
),
"input_schema": {
"type": "object",
"properties": {
"ready_to_qualify": {
"type": "boolean",
"description": "Set to true when qualification data is complete"
},
"summary": {
"type": "string",
"description": "Brief summary of what was learned about this lead"
}
},
"required": ["ready_to_qualify", "summary"]
}
}
]Each tool maps to a real qualification signal. Claude will call these automatically mid-conversation when the prospect says something that satisfies the trigger condition in the description field. You don't have to write any parsing logic — the model handles extraction for you.
Step 3: Build the Agent Loop with Multi-Turn Conversations
Now we'll build the actual agent class. The key design here is an agentic loop — a pattern where we keep sending messages back to Claude until it either runs out of tools to call or decides the conversation is done. This is what makes it an agent rather than just a single API call.
The loop also maintains conversation history so Claude remembers what was said earlier in the chat, which is critical for natural qualification conversations.
lead_qualifier.py (add the agent class)class RealEstateLeadQualifier:
"""
Conversational AI agent that qualifies real estate leads
by extracting budget, timeline, and location via tool use.
"""
SYSTEM_PROMPT = """You are a friendly real estate intake specialist for a Southwest Florida agency.
Your job is to have a natural conversation with prospective buyers and sellers to understand their needs.
Ask about:
1. Their budget range for a property purchase
2. Their desired timeline to buy or move
3. Which neighborhoods or areas in Southwest Florida interest them
Keep the conversation warm and conversational — not like an interrogation.
Ask one question at a time. When you've gathered budget, timeline, and location data,
call the finalize_qualification tool to wrap up the conversation.
Use the recording tools (record_budget, record_timeline, record_location) as soon as
the prospect provides that information — even mid-conversation."""
def __init__(self):
self.client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
self.conversation_history = []
self.collected_data = {
"budget": None,
"timeline": None,
"location": None,
"summary": None
}
self.qualification_complete = False
def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""Handle tool calls from Claude and store the extracted data."""
if tool_name == "record_budget":
self.collected_data["budget"] = tool_input
return f"Budget recorded: ${tool_input['min_budget']:,} - ${tool_input['max_budget']:,} ({tool_input['budget_confidence']})"
elif tool_name == "record_timeline":
self.collected_data["timeline"] = tool_input
pre_approved = "pre-approved" if tool_input["is_pre_approved"] else "not pre-approved"
return f"Timeline recorded: {tool_input['timeline_months']} months, {pre_approved}"
elif tool_name == "record_location":
self.collected_data["location"] = tool_input
areas = ", ".join(tool_input["preferred_areas"])
return f"Location recorded: {areas} (flexibility: {tool_input['flexibility']})"
elif tool_name == "finalize_qualification":
self.collected_data["summary"] = tool_input["summary"]
self.qualification_complete = tool_input["ready_to_qualify"]
return "Qualification data finalized."
return "Tool executed."
def chat(self, user_message: str) -> str:
"""
Send a message and run the agentic loop until Claude
either responds to the user or finishes qualifying.
"""
# Add user message to history
self.conversation_history.append({
"role": "user",
"content": user_message
})
# Agentic loop — keeps running while Claude wants to use tools
while True:
response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=self.SYSTEM_PROMPT,
tools=QUALIFICATION_TOOLS,
messages=self.conversation_history
)
# Check if Claude is done or wants to use a tool
if response.stop_reason == "end_turn":
# Claude sent a message to the user — extract text and return it
assistant_text = next(
(block.text for block in response.content if hasattr(block, "text")),
""
)
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
return assistant_text
elif response.stop_reason == "tool_use":
# Claude called one or more tools — process each one
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = self._process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Add Claude's tool call message and our results back to history
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
self.conversation_history.append({
"role": "user",
"content": tool_results
})
# If qualification is complete, we can break out of the loop
if self.qualification_complete:
# One final call to get Claude's closing message
final_response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
system=self.SYSTEM_PROMPT,
tools=QUALIFICATION_TOOLS,
messages=self.conversation_history
)
closing_text = next(
(block.text for block in final_response.content if hasattr(block, "text")),
"Thank you for your time! An agent will follow up shortly."
)
return closing_text
# Otherwise loop back and let Claude continue the conversation
continue
else:
# Unexpected stop reason — bail out gracefully
return "I'm sorry, something went wrong. Please try again."The loop is the heart of the whole thing. Every time Claude decides to call a tool, we process it, feed the result back, and let Claude keep going. This is exactly how production agentic systems work — you're not just calling an LLM once, you're orchestrating a back-and-forth that mimics how a real intake agent would think through a conversation.
Step 4: Implement Scoring Logic and Lead Ranking
Now we tie everything together. Once the conversation is complete and we have structured data from the tools, we run a scoring function that turns the collected signals into a numeric score and a classification.
Here's the complete file with scoring logic and a demo runner so you can see it all working end to end:
lead_qualifier.py (complete file)import os
import json
from anthropic import Anthropic
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
if not ANTHROPIC_API_KEY:
raise EnvironmentError("ANTHROPIC_API_KEY not found.")
QUALIFICATION_TOOLS = [
{
"name": "record_budget",
"description": (
"Call this tool when the prospect has stated or clearly implied "
"their purchase budget for a property. Extract the numeric range."
),
"input_schema": {
"type": "object",
"properties": {
"min_budget": {"type": "number", "description": "Minimum budget in USD"},
"max_budget": {"type": "number", "description": "Maximum budget in USD"},
"budget_confidence": {
"type": "string",
"enum": ["firm", "flexible", "uncertain"],
"description": "How confident the prospect seems about their budget"
}
},
"required": ["min_budget", "max_budget", "budget_confidence"]
}
},
{
"name": "record_timeline",
"description": (
"Call this tool when the prospect has indicated when they want "
"to buy or move. Capture urgency to score lead priority."
),
"input_schema": {
"type": "object",
"properties": {
"timeline_months": {
"type": "number",
"description": "How many months until the prospect wants to complete a purchase"
},
"is_pre_approved": {
"type": "boolean",
"description": "Whether the prospect has mortgage pre-approval"
}
},
"required": ["timeline_months", "is_pre_approved"]
}
},
{
"name": "record_location",
"description": (
"Call this tool when the prospect mentions specific neighborhoods, "
"cities, or geographic preferences for their property search."
),
"input_schema": {
"type": "object",
"properties": {
"preferred_areas": {
"type": "array",
"items": {"type": "string"},
"description": "List of neighborhoods, cities, or zip codes mentioned"
},
"flexibility": {
"type": "string",
"enum": ["specific", "somewhat_flexible", "open"],
"description": "How flexible the prospect is on location"
}
},
"required": ["preferred_areas", "flexibility"]
}
},
{
"name": "finalize_qualification",
"description": (
"Call this tool when you have collected enough information to "
"make a qualification decision. Use this to end the conversation."
),
"input_schema": {
"type": "object",
"properties": {
"ready_to_qualify": {
"type": "boolean",
"description": "Set to true when qualification data is complete"
},
"summary": {
"type": "string",
"description": "Brief summary of what was learned about this lead"
}
},
"required": ["ready_to_qualify", "summary"]
}
}
]
class RealEstateLeadQualifier:
SYSTEM_PROMPT = """You are a friendly real estate intake specialist for a Southwest Florida agency.
Your job is to have a natural conversation with prospective buyers and sellers to understand their needs.
Ask about:
1. Their budget range for a property purchase
2. Their desired timeline to buy or move
3. Which neighborhoods or areas in Southwest Florida interest them
Keep the conversation warm and conversational — not like an interrogation.
Ask one question at a time. When you've gathered budget, timeline, and location data,
call the finalize_qualification tool to wrap up the conversation.
Use the recording tools (record_budget, record_timeline, record_location) as soon as
the prospect provides that information — even mid-conversation."""
def __init__(self):
self.client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
self.conversation_history = []
self.collected_data = {
"budget": None,
"timeline": None,
"location": None,
"summary": None
}
self.qualification_complete = False
def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
if tool_name == "record_budget":
self.collected_data["budget"] = tool_input
return f"Budget recorded: ${tool_input['min_budget']:,} - ${tool_input['max_budget']:,}"
elif tool_name == "record_timeline":
self.collected_data["timeline"] = tool_input
return f"Timeline recorded: {tool_input['timeline_months']} months"
elif tool_name == "record_location":
self.collected_data["location"] = tool_input
return f"Location recorded: {', '.join(tool_input['preferred_areas'])}"
elif tool_name == "finalize_qualification":
self.collected_data["summary"] = tool_input["summary"]
self.qualification_complete = tool_input["ready_to_qualify"]
return "Qualification finalized."
return "Tool executed."
def chat(self, user_message: str) -> str:
self.conversation_history.append({"role": "user", "content": user_message})
while True:
response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=self.SYSTEM_PROMPT,
tools=QUALIFICATION_TOOLS,
messages=self.conversation_history
)
if response.stop_reason == "end_turn":
assistant_text = next(
(block.text for block in response.content if hasattr(block, "text")), ""
)
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
return assistant_text
elif response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = self._process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
self.conversation_history.append({
"role": "user",
"content": tool_results
})
if self.qualification_complete:
final_response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
system=self.SYSTEM_PROMPT,
tools=QUALIFICATION_TOOLS,
messages=self.conversation_history
)
return next(
(block.text for block in final_response.content if hasattr(block, "text")),
"Thank you! An agent will follow up shortly."
)
continue
else:
return "Something went wrong. Please try again."
def score_lead(self) -> dict:
"""
Score the lead on a 0-100 scale based on collected qualification data.
Returns a dict with score, classification, and reasoning.
"""
score = 0
reasons = []
# Budget scoring (max 40 points)
budget = self.collected_data.get("budget")
if budget:
max_b = budget.get("max_budget", 0)
confidence = budget.get("budget_confidence", "uncertain")
if max_b >= 500000:
score += 30
reasons.append("High budget ($500K+)")
elif max_b >= 300000:
score += 20
reasons.append("Mid-