← Back to Blog

What You'll Build

If you've ever watched a real estate team burn hours manually sorting through leads that go nowhere, this tutorial is for you. You're going to build a fully working AI agent in Python that automatically qualifies incoming real estate leads — scoring them by budget, property match, and buyer readiness — using the Claude API with Anthropic's SDK.

By the end, you'll have an agent that can process 100+ leads daily, output a priority score for each one, and explain its reasoning in plain English. No manual sorting, no spreadsheets, no guesswork.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python functions and dictionaries
  • The anthropic Python SDK installed (pip install anthropic)
  • Optional: a JSON file of lead data to test against
📦 Full Source Code
The complete working code for this agent is built up step by step in the sections below. Each snippet is self-contained and builds on the last. By Step 4, you'll have a fully runnable file. If you want to jump straight to the finished product, scroll to the complete lead_qualifier_agent.py in Step 4.

Step 1: Set Up Claude API and Python Environment

First, install the Anthropic SDK and set your API key as an environment variable. You never want to hardcode API keys in source files — set it in your shell or a .env file instead.

terminal
pip install anthropic python-dotenv

Create a .env file in your project root with your key:

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Now initialize the Anthropic client and confirm it works with a quick test call. This is the foundation everything else builds on.

agent_init.py
import os
import anthropic
from dotenv import load_dotenv

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

# Quick sanity check — confirm the client is live
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=64,
    messages=[{"role": "user", "content": "Say 'Agent online.' and nothing else."}]
)

print(response.content[0].text)

Run it. You should see exactly: Agent online. If you get an auth error, double-check your API key in the .env file.

Step 2: Define Lead Qualification Tools

This is where the real architecture starts. Claude's tool use feature lets you define functions the model can call when it needs structured data. We're defining three tools: a property matcher, a buyer profile scorer, and a priority ranker.

Each tool is defined as a JSON schema that tells Claude what inputs it accepts and what it returns. Claude decides when to call each tool based on context — you don't hardcode the logic flow.

tools.py
import anthropic

# Tool definitions passed to the Claude API
# Claude reads these schemas and decides when to call each tool

LEAD_QUALIFICATION_TOOLS = [
    {
        "name": "check_property_match",
        "description": (
            "Checks whether available property listings match the lead's stated "
            "preferences including location, property type, and price range. "
            "Returns a match score from 0 to 100 and a list of matching property IDs."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead"
                },
                "desired_location": {
                    "type": "string",
                    "description": "City, neighborhood, or zip code the buyer wants"
                },
                "property_type": {
                    "type": "string",
                    "description": "Type of property: single_family, condo, townhouse, land"
                },
                "min_price": {
                    "type": "integer",
                    "description": "Minimum budget in USD"
                },
                "max_price": {
                    "type": "integer",
                    "description": "Maximum budget in USD"
                },
                "min_bedrooms": {
                    "type": "integer",
                    "description": "Minimum number of bedrooms required"
                }
            },
            "required": ["lead_id", "desired_location", "property_type", "max_price"]
        }
    },
    {
        "name": "score_buyer_profile",
        "description": (
            "Evaluates the lead's buyer profile for qualification signals: "
            "pre-approval status, timeline, agent relationship, and motivation level. "
            "Returns a readiness score from 0 to 100."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead"
                },
                "has_preapproval": {
                    "type": "boolean",
                    "description": "Whether the buyer has mortgage pre-approval"
                },
                "buying_timeline_months": {
                    "type": "integer",
                    "description": "How many months until they intend to buy"
                },
                "has_existing_agent": {
                    "type": "boolean",
                    "description": "Whether the buyer is already working with another agent"
                },
                "motivation": {
                    "type": "string",
                    "description": "Buyer motivation: relocation, investment, upsizing, downsizing, first_purchase"
                }
            },
            "required": ["lead_id", "has_preapproval", "buying_timeline_months"]
        }
    },
    {
        "name": "calculate_priority_score",
        "description": (
            "Combines property match score and buyer readiness score into a final "
            "lead priority score from 0 to 100. Also returns a recommended action "
            "for the agent: call_now, nurture, or disqualify."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead"
                },
                "property_match_score": {
                    "type": "integer",
                    "description": "Score from check_property_match tool (0-100)"
                },
                "buyer_readiness_score": {
                    "type": "integer",
                    "description": "Score from score_buyer_profile tool (0-100)"
                }
            },
            "required": ["lead_id", "property_match_score", "buyer_readiness_score"]
        }
    }
]
💡 Why three separate tools?
Breaking the logic into three tools lets Claude reason step by step — it can check properties first, then evaluate the buyer, then combine. This produces much more reliable and explainable scores than asking the model to do everything in one shot.

Step 3: Create the Agent Loop with Tool Use

The agent loop is the core of how this works. Claude sends back a tool_use response when it wants to call one of your tools. You handle that call, return the result, and send the conversation back to Claude. This loop continues until Claude sends a final text response.

This pattern — sometimes called an "agentic loop" — is how you build AI agents that can take real actions, not just generate text.

agent_loop.py
import os
import json
import anthropic
from dotenv import load_dotenv
from tools import LEAD_QUALIFICATION_TOOLS

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))


def handle_tool_call(tool_name: str, tool_input: dict) -> str:
    """
    Simulated tool execution layer.
    In production, these functions would query your CRM and MLS database.
    """
    if tool_name == "check_property_match":
        return simulate_property_match(tool_input)
    elif tool_name == "score_buyer_profile":
        return simulate_buyer_profile_score(tool_input)
    elif tool_name == "calculate_priority_score":
        return simulate_priority_score(tool_input)
    else:
        return json.dumps({"error": f"Unknown tool: {tool_name}"})


def simulate_property_match(inputs: dict) -> str:
    """Returns a mock property match — replace with real MLS API call."""
    max_price = inputs.get("max_price", 0)
    location = inputs.get("desired_location", "").lower()

    # Simple scoring logic for demo purposes
    score = 75 if max_price > 400000 else 45
    if "naples" in location or "bonita" in location or "marco" in location:
        score = min(score + 15, 100)

    matching_ids = ["LST-1042", "LST-1087"] if score > 60 else ["LST-2201"]

    return json.dumps({
        "lead_id": inputs["lead_id"],
        "match_score": score,
        "matching_property_ids": matching_ids,
        "total_matches": len(matching_ids)
    })


def simulate_buyer_profile_score(inputs: dict) -> str:
    """Returns a mock buyer readiness score — replace with CRM lookup."""
    score = 50
    if inputs.get("has_preapproval"):
        score += 30
    if inputs.get("buying_timeline_months", 99) <= 3:
        score += 20
    elif inputs.get("buying_timeline_months", 99) <= 6:
        score += 10
    if inputs.get("has_existing_agent"):
        score -= 25  # Already working with someone else

    score = max(0, min(score, 100))

    return json.dumps({
        "lead_id": inputs["lead_id"],
        "readiness_score": score,
        "flags": {
            "preapproved": inputs.get("has_preapproval", False),
            "timeline_months": inputs.get("buying_timeline_months"),
            "has_agent": inputs.get("has_existing_agent", False)
        }
    })


def simulate_priority_score(inputs: dict) -> str:
    """Calculates final priority and recommended action."""
    pm = inputs.get("property_match_score", 0)
    br = inputs.get("buyer_readiness_score", 0)

    # Weighted average: buyer readiness matters slightly more
    final_score = int((pm * 0.45) + (br * 0.55))

    if final_score >= 70:
        action = "call_now"
    elif final_score >= 40:
        action = "nurture"
    else:
        action = "disqualify"

    return json.dumps({
        "lead_id": inputs["lead_id"],
        "priority_score": final_score,
        "recommended_action": action
    })


def run_lead_qualifier_agent(lead_data: dict) -> str:
    """
    Main agent loop. Sends lead data to Claude, handles tool calls,
    and returns the final qualification summary.
    """
    system_prompt = """You are a real estate lead qualification agent for a Southwest Florida brokerage.
Your job is to evaluate incoming leads and determine how much priority they deserve.

For each lead, you MUST:
1. Call check_property_match to see if we have suitable listings
2. Call score_buyer_profile to assess buyer readiness
3. Call calculate_priority_score with both scores to get the final verdict
4. Provide a clear, concise summary with the priority score, recommended action, and your reasoning

Be direct. Agents need to know immediately whether to pick up the phone or move on."""

    user_message = f"""Please qualify this lead:

Lead ID: {lead_data['id']}
Name: {lead_data['name']}
Desired Location: {lead_data['desired_location']}
Property Type: {lead_data['property_type']}
Budget: ${lead_data['min_price']:,} - ${lead_data['max_price']:,}
Bedrooms Needed: {lead_data.get('min_bedrooms', 2)}
Pre-approved: {lead_data['has_preapproval']}
Buying Timeline: {lead_data['buying_timeline_months']} months
Currently Working with Agent: {lead_data['has_existing_agent']}
Motivation: {lead_data['motivation']}
Notes: {lead_data.get('notes', 'None')}"""

    messages = [{"role": "user", "content": user_message}]

    # Agentic loop — keeps going until Claude stops calling tools
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=system_prompt,
            tools=LEAD_QUALIFICATION_TOOLS,
            messages=messages
        )

        # Claude is done — return the final text response
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text

        # Claude wants to use a tool
        if response.stop_reason == "tool_use":
            # Append Claude's response (with tool_use blocks) to message history
            messages.append({"role": "assistant", "content": response.content})

            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    tool_result = handle_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": tool_result
                    })

            # Send tool results back to Claude
            messages.append({"role": "user", "content": tool_results})

        else:
            # Unexpected stop reason — exit loop safely
            break

    return "Agent loop ended without a final response."

Step 4: Integrate Property and Lead Data Sources

Now we wire everything together. In production you'd pull leads from a CRM like Follow Up Boss or a webhook from your website. For this tutorial, we're using a realistic JSON dataset so you can run it locally right now.

lead_qualifier_agent.py
import os
import json
import anthropic
from dotenv import load_dotenv
from tools import LEAD_QUALIFICATION_TOOLS

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

# ── Simulated tool execution ─────────────────────────────────────────────────

def handle_tool_call(tool_name: str, tool_input: dict) -> str:
    if tool_name == "check_property_match":
        return simulate_property_match(tool_input)
    elif tool_name == "score_buyer_profile":
        return simulate_buyer_profile_score(tool_input)
    elif tool_name == "calculate_priority_score":
        return simulate_priority_score(tool_input)
    return json.dumps({"error": f"Unknown tool: {tool_name}"})


def simulate_property_match(inputs: dict) -> str:
    max_price = inputs.get("max_price", 0)
    location = inputs.get("desired_location", "").lower()
    score = 75 if max_price > 400000 else 45
    if any(city in location for city in ["naples", "bonita", "marco", "estero"]):
        score = min(score + 15, 100)
    matching_ids = ["LST-1042", "LST-1087"] if score > 60 else ["LST-2201"]
    return json.dumps({
        "lead_id": inputs["lead_id"],
        "match_score": score,
        "matching_property_ids": matching_ids,
        "total_matches": len(matching_ids)
    })


def simulate_buyer_profile_score(inputs: dict) -> str:
    score = 50
    if inputs.get("has_preapproval"):
        score += 30
    timeline = inputs.get("buying_timeline_months", 99)
    if timeline <= 3:
        score += 20
    elif timeline <= 6:
        score += 10
    if inputs.get("has_existing_agent"):
        score -= 25
    score = max(0, min(score, 100))
    return json.dumps({
        "lead_id": inputs["lead_id"],
        "readiness_score": score,
        "flags": {
            "preapproved": inputs.get("has_preapproval", False),
            "timeline_months": timeline,
            "has_agent": inputs.get("has_existing_agent", False)
        }
    })


def simulate_priority_score(inputs: dict) -> str:
    pm = inputs.get("property_match_score", 0)
    br = inputs.get("buyer_readiness_score", 0)
    final_score = int((pm * 0.45) + (br * 0.55))
    if final_score >= 70:
        action = "call_now"
    elif final_score >= 40:
        action = "nurture"
    else:
        action = "disqualify"
    return json.dumps({
        "lead_id": inputs["lead_id"],
        "priority_score": final_score,
        "recommended_action": action
    })


# ── Agent loop ───────────────────────────────────────────────────────────────

def run_lead_qualifier_agent(lead_data: dict) -> str:
    system_prompt = """You are a real estate lead qualification agent for a Southwest Florida brokerage.
Your job is to evaluate incoming leads and determine how much priority they deserve.

For each lead, you MUST:
1. Call check_property_match to see if we have suitable listings
2. Call score_buyer_profile to assess buyer readiness
3. Call calculate_priority_score with both scores to get the final verdict
4. Provide a clear, concise summary with the priority score, recommended action, and your reasoning

Be direct. Agents need to know immediately whether to pick up the phone or move on."""

    user_message = f"""Please qualify this lead:

Lead ID: {lead_data['id']}
Name: {lead_data['name']}
Desired Location: {lead_data['desired_location']}
Property Type: {lead_data['property_type']}
Budget: ${lead_data['min_price']:,} - ${lead_data['max_price']:,}
Bedrooms Needed: {lead_data.get('min_bedrooms', 2)}
Pre-approved: {lead_data['has_preapproval']}
Buying Timeline: {lead_data['buying_timeline_months']} months
Currently Working with Agent: {lead_data['has_existing_agent']}
Motivation: {lead_data['motivation']}
Notes: {lead_data.get('notes', 'None')}"""

    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=system_prompt,
            tools=LEAD_QUALIFICATION_TOOLS,
            messages=messages
        )

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text

        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = handle_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            messages.append({"role": "user", "content": tool_results})
        else:
            break

    return "Agent loop ended without a final response."


# ── Sample lead data ─────────────────────────────────────────────────────────

SAMPLE_LEADS = [
    {
        "id": "LEAD-001",
        "name": "Sandra and Tom Kowalski",
        "desired_location": "Naples, FL",
        "property_type": "single_family",
        "min_price": 650000,
        "max_price": 850000,
        "min_bedrooms": 3,
        "has_preapproval": True,
        "buying_timeline_months": 2,
        "has_existing_agent": False,
        "motivation": "relocation",
        "notes": "Relocating from Chicago. Tom starts new job in Naples in April."
    },
    {
        "id": "LEAD-002",
        "name": "Derek Fontaine",
        "desired_location": "Bonita Springs, FL",
        "property_type": "condo",
        "min_price": 200000,
        "max_price": 320000,
        "min_bedrooms": 2,
        "has_preapproval": False,
        "buying_timeline_months": 18,
        "has_existing_agent": True,
        "motivation": "investment",
        "notes": "Browsing casually. Just started looking."
    },
    {
        "id": "LEAD-003",
        "name": "Maria Delgado",