← Back to Blog

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.

📦 Full Source Code Notice: All code snippets shown below are complete and working. Each step builds on the last, and by Step 5 you'll have the entire agent assembled. Copy them in order and you're ready to run.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (console.anthropic.com)
  • anthropic Python 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.

lead_qualifier.py — Part 1: Client Setup
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 Definitions
import 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 Class
import 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.

lead_qualifier.py — Part 5: Routing + Full Runner
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"]
        }