If you've been searching for a real how to build AI agents tutorial — not a toy chatbot demo, but something that actually qualifies leads, scores them, and spits out structured data — you're in the right place. Most tutorials show you how to call an API and print a response. This one shows you how to build something a real estate business could actually use.
By the end of this guide, you'll have a working Python agent that chats with a prospect, extracts key qualification data, scores the lead on a 1–10 scale, and returns a clean JSON summary. We built a version of this for a Naples-area real estate client, and it cut their follow-up time by about 60%.
What You'll Build
You're building a multi-turn AI lead qualifier that holds a conversation with a real estate prospect, collects their budget, timeline, location preference, and buyer status, then scores and classifies them automatically.
The agent uses Claude's tool use feature to call structured functions mid-conversation — so the data lands in a clean JSON object, not buried in a paragraph of text. It runs in Python using the official Anthropic SDK.
At the end of every conversation, you get output like this:
{
"lead_id": "lead_20260502_001",
"name": "Sarah Mitchell",
"email": "[email protected]",
"phone": "239-555-0142",
"budget_min": 600000,
"budget_max": 850000,
"timeline_months": 3,
"location_preference": "Naples, FL - North Naples preferred",
"buyer_status": "pre-approved",
"property_type": "single-family",
"bedrooms": 4,
"score": 9,
"classification": "Hot",
"notes": "Pre-approved buyer, motivated timeline, specific area preference.",
"qualified_at": "2026-05-02T14:32:00"
}
Prerequisites
- Python 3.9 or higher installed
- 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 expert, but you should know what a class and a function are
- A terminal or code editor like VS Code
The complete, working code for this project is built out step-by-step in the sections below. Each snippet builds on the last, so by the time you hit Step 5 you'll have everything assembled and ready to run. You can also copy the final consolidated file from the Step 3 section which contains the complete agent class.
Step 1: Set Up Claude API Authentication and Import Libraries
First, let's get the environment wired up. You'll import the Anthropic SDK, set your API key, and confirm the connection works before writing any agent logic.
Store your API key in an environment variable — never hardcode it in the file. Create a .env file in your project root or export it in your terminal with export ANTHROPIC_API_KEY=your_key_here.
import os
import json
import uuid
from datetime import datetime
import anthropic
# Pull the API key from environment — never hardcode secrets
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise ValueError("ANTHROPIC_API_KEY environment variable not set.")
# Initialize the Anthropic client
client = anthropic.Anthropic(api_key=api_key)
# Quick connection test — sends a minimal message to confirm auth works
test_response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=50,
messages=[{"role": "user", "content": "Say 'connected' and nothing else."}]
)
print("Connection test:", test_response.content[0].text)
# Expected output: Connection test: connected
Run this with python setup.py. If you see Connection test: connected, you're good to go. If you get an auth error, double-check that your environment variable is actually set in the same terminal session.
python-dotenv package if you want to load your key from a .env file automatically. Install it with pip install python-dotenv and add from dotenv import load_dotenv; load_dotenv() at the top of your file.
Step 2: Define Lead Qualification Tool Definitions
This is where the Claude API tutorial gets interesting. Instead of parsing lead info from a blob of text, we're giving Claude a set of structured tools it can call mid-conversation. When it has enough information, it calls the tool and fills in the fields — you get clean, typed data every time.
We're defining two tools: one to capture and save lead data, and one to score and classify the lead. Claude decides when it has enough information to call each one.
tools.pyimport anthropic
# Tool definitions — these tell Claude what functions it can call
# and what parameters each function expects
LEAD_TOOLS = [
{
"name": "capture_lead_data",
"description": (
"Call this tool once you have collected the prospect's contact information "
"and basic property preferences. Use it to save the lead's name, email, phone, "
"budget range, location preference, property type, and timeline."
),
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Full name of the prospect"
},
"email": {
"type": "string",
"description": "Email address"
},
"phone": {
"type": "string",
"description": "Phone number"
},
"budget_min": {
"type": "integer",
"description": "Minimum budget in USD"
},
"budget_max": {
"type": "integer",
"description": "Maximum budget in USD"
},
"location_preference": {
"type": "string",
"description": "Preferred area or neighborhood"
},
"property_type": {
"type": "string",
"description": "Type of property: single-family, condo, townhouse, land"
},
"bedrooms": {
"type": "integer",
"description": "Number of bedrooms needed"
},
"timeline_months": {
"type": "integer",
"description": "How many months until they need to move or close"
}
},
"required": ["name", "email", "budget_min", "budget_max", "timeline_months"]
}
},
{
"name": "score_and_classify_lead",
"description": (
"Call this tool after capture_lead_data to score and classify the lead. "
"Score from 1-10 based on: budget realism (2pts), timeline urgency (3pts), "
"buyer readiness like pre-approval (3pts), and specificity of needs (2pts). "
"Classify as Hot (8-10), Warm (5-7), or Cold (1-4)."
),
"input_schema": {
"type": "object",
"properties": {
"score": {
"type": "integer",
"description": "Lead score from 1 to 10"
},
"classification": {
"type": "string",
"description": "Hot, Warm, or Cold"
},
"buyer_status": {
"type": "string",
"description": "pre-approved, searching, just-browsing, or renting"
},
"notes": {
"type": "string",
"description": "Brief summary of why this score was given"
}
},
"required": ["score", "classification", "buyer_status", "notes"]
}
}
]
The scoring rubric is baked right into the tool description. Claude reads it and applies the logic consistently — no need to write scoring code manually. That's one of the real advantages of this build chatbot with Claude API approach over a rule-based system.
Step 3: Build the Agent Loop with Conversation State
Now we build the main agent class. This is the core of the AI agent development guide — a loop that sends messages, receives responses, handles tool calls, and keeps the conversation alive until the lead is fully qualified.
The agent maintains a conversation history list that grows with each turn. When Claude calls a tool, we execute the tool handler and feed the result back in as a tool_result message so Claude can continue the conversation naturally.
import os
import json
import uuid
from datetime import datetime
import anthropic
from tools import LEAD_TOOLS
SYSTEM_PROMPT = """You are a friendly real estate assistant for a Naples, Florida real estate agency.
Your job is to qualify potential buyers and sellers through a natural conversation.
Follow this flow:
1. Greet the prospect warmly and ask what brings them in today.
2. Collect their name, email, and phone number early in the conversation.
3. Ask about their budget range, desired location, property type, and number of bedrooms.
4. Ask about their timeline — when do they need to move or close?
5. Ask if they are pre-approved for a mortgage or working with a lender.
6. Once you have all the key information, call capture_lead_data to save it.
7. Immediately after, call score_and_classify_lead to score the lead.
8. Thank the prospect and let them know an agent will follow up shortly.
Keep the conversation natural. Ask one or two questions at a time — don't overwhelm them.
Be warm, professional, and specific to the Naples, FL market."""
class RealEstateLeadAgent:
"""
Multi-turn AI lead qualifier using Claude's tool use feature.
Maintains conversation history and executes tool calls in a run loop.
"""
def __init__(self):
self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
self.model = "claude-sonnet-4-5"
self.conversation_history = [] # Grows with each turn
self.lead_data = {} # Populated when tools are called
self.lead_id = f"lead_{datetime.now().strftime('%Y%m%d')}_{str(uuid.uuid4())[:6]}"
self.qualification_complete = False
def _handle_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""
Execute the tool logic and return a result string.
In production you'd write this to a CRM or database.
"""
if tool_name == "capture_lead_data":
# Merge captured fields into our lead_data dict
self.lead_data.update(tool_input)
self.lead_data["lead_id"] = self.lead_id
return json.dumps({"status": "success", "message": "Lead data captured."})
elif tool_name == "score_and_classify_lead":
# Add scoring fields and mark qualification as done
self.lead_data.update(tool_input)
self.lead_data["qualified_at"] = datetime.now().isoformat(timespec="seconds")
self.qualification_complete = True
return json.dumps({"status": "success", "message": "Lead scored and classified."})
return json.dumps({"status": "error", "message": f"Unknown tool: {tool_name}"})
def chat(self, user_message: str) -> str:
"""
Send a user message, run the agentic loop, and return Claude's final text reply.
Handles any number of tool calls in a single turn.
"""
# Append the new user message to conversation history
self.conversation_history.append({
"role": "user",
"content": user_message
})
# Agentic run loop — keeps going until Claude stops calling tools
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=LEAD_TOOLS,
messages=self.conversation_history
)
# Collect text and tool use blocks from the response
text_reply = ""
tool_calls = []
for block in response.content:
if block.type == "text":
text_reply = block.text
elif block.type == "tool_use":
tool_calls.append(block)
# If no tool calls, Claude is done for this turn — return the reply
if not tool_calls:
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
return text_reply
# There are tool calls — append the assistant's full response first
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
# Execute each tool call and build tool_result messages
tool_results = []
for tool_call in tool_calls:
result = self._handle_tool_call(tool_call.name, tool_call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": result
})
# Feed tool results back so Claude can continue the conversation
self.conversation_history.append({
"role": "user",
"content": tool_results
})
# Loop continues — Claude will now process the tool results
def get_qualified_lead(self) -> dict:
"""Return the final qualified lead JSON. Call after qualification_complete is True."""
return self.lead_data
capture_lead_data, then immediately score_and_classify_lead. The loop keeps running until Claude returns a plain text response with no tool calls, meaning it's done processing for that turn.
Step 4: Implement Lead Scoring and Classification Logic
The scoring logic lives inside the tool description we wrote in Step 2 — Claude applies it automatically. But we also want a standalone Python function that can re-score or override a lead if you need to apply business rules after the fact.
This function is useful if you want to adjust scores based on zip code, property type, or any other variable your brokerage cares about. Add it to agent.py or a separate scoring.py file.
def rescore_lead(lead_data: dict) -> dict:
"""
Re-score a qualified lead using deterministic Python rules.
Useful for overriding Claude's score with hard business logic.
Returns the updated lead_data dict.
"""
score = 0
# Budget realism — 2 points if budget is within Naples market range
budget_max = lead_data.get("budget_max", 0)
if budget_max >= 300_000:
score += 2
elif budget_max >= 150_000:
score += 1
# Timeline urgency — 3 points for motivated buyers
timeline = lead_data.get("timeline_months", 99)
if timeline <= 2:
score += 3
elif timeline <= 6:
score += 2
elif timeline <= 12:
score += 1
# Buyer readiness — 3 points
buyer_status = lead_data.get("buyer_status", "")
if buyer_status == "pre-approved":
score += 3
elif buyer_status == "searching":
score += 2
elif buyer_status == "renting":
score += 1
# Specificity of needs — 2 points
has_location = bool(lead_data.get("location_preference"))
has_type = bool(lead_data.get("property_type"))
if has_location and has_type:
score += 2
elif has_location or has_type:
score += 1
# Classify based on final score
if score >= 8:
classification = "Hot"
elif score >= 5:
classification = "Warm"
else:
classification = "Cold"
lead_data["score"] = score
lead_data["classification"] = classification
return lead_data
# --- Quick test ---
if __name__ == "__main__":
sample = {
"budget_max": 750000,
"timeline_months": 3,
"buyer_status": "pre-approved",
"location_preference": "North Naples",
"property_type": "single-family"
}
result = rescore_lead(sample)
print(f"Score: {result['score']} | Classification: {result['classification']}")
# Expected output: Score: 10 | Classification: Hot
Step 5: Test with Real Estate Inquiry Examples
Now let's tie everything together and run a simulated conversation. This script drives the agent through a realistic buyer scenario so you can see exactly what the output looks like end-to-end.
main.pyimport os
import json
from agent import RealEstateLeadAgent
def run_simulated_conversation():
"""
Simulates a multi-turn real estate qualification conversation.
Replace the scripted inputs with real user input() calls for live use.
"""
agent = RealEstateLeadAgent()
# Scripted prospect responses — swap for input() in production
prospect_messages = [
"Hi, I'm looking to buy a home in Naples.",
"My name is Sarah Mitchell. Email is [email protected] and phone is 239-555-0142.",
"We're looking somewhere between $600,000 and $850,000.",
"We'd love North Naples if possible. We need a 4-bedroom single-family home.",
"We want to close within the next 3 months. We're already pre-approved.",
]
print("=" * 60)
print("REAL ESTATE LEAD QUALIFIER — SIMULATED SESSION")
print("=" * 60)
for user_input in prospect_messages:
print(f"\n[Prospect]: {user_input}")
response = agent.chat(user_input)
print(f"[Agent]: {response}")
# Stop early if qualification is already complete
if agent.qualification_complete:
break
print("\n" + "=" * 60)
print("QUALIFIED LEAD OUTPUT")
print("=" * 60)
print(json.dumps(agent.get_qualified_lead(), indent=2))
if __name__ == "__main__":
run_simulated_conversation()
Run it with python main.py. Here's what the actual output looks like:
============================================================ REAL ESTATE LEAD QUALIFIER — SIMULATED SESSION ============================================================ [Prospect]: Hi, I'm looking to buy a home in Naples. [Agent]: Welcome! I'd love to help you find your perfect Naples home. To get started, could I grab your name, email address, and a good phone number for you? [Prospect]: My name is Sarah Mitchell. Email is [email protected] and phone is 239-555-0142. [Agent]: Great to meet you, Sarah! Naples is a beautiful market right now. What kind of budget are you working with, and do you have a particular area of Naples in mind? [Prospect]: We're looking somewhere between $600,000 and $850,000. [Agent]: That's a solid budget for Naples — you'll have some great options. Are you looking for a single-family home, condo, or something else? And how many bedrooms do you need? [Prospect]: We'd love North Naples if possible. We need a 4-bedroom single-family home. [Agent]: North Naples is a great choice — lots of newer builds and A-rated schools in that area. One more important question: what's your timeline looking like, and have you been pre-approved for a mortgage yet? [Prospect]: We want to close within the next 3 months. We're already pre-approved. [Agent]: That's fantastic, Sarah — being pre-approved puts you in a really strong position in this market. I've got everything I need. One of our agents will be reaching out to you shortly at 239-555-0142 to set up some showings in North Naples. Thanks for taking the time today! ============================================================ QUALIFIED LEAD OUTPUT ============================================================ { "name": "Sarah Mitchell", "email": "[email protected]", "phone": "239-555-0142", "budget_min": 600000, "budget_max": 850000, "location_preference": "North Naples", "property_type": "single-family", "bedrooms": 4, "timeline_months": 3, "lead_id": "lead_20260502_a3f9c1