← Back to Blog

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.

📦 Full Source Code Note: The complete, working code is broken into steps below so you understand what each piece does. Every snippet is production-ready — copy them in order and you'll have a running agent. No pseudocode, no placeholder functions.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • anthropic SDK installed: pip install anthropic
  • python-dotenv for 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.

.env
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.

lead_qualifier.py
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"}
⚠️ Important: The agent loop only terminates when 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:

Sample Output
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.