← Back to Blog

What You'll Build

You're going to build a production-ready AI lead qualifier that reads raw buyer inquiry text, extracts contact information, scores the lead on a 1–10 scale, and explains its reasoning — all in under 100 lines of Python. This agent uses Claude's tool use feature, so it doesn't just chat; it calls structured functions and returns clean, machine-readable data your CRM can actually use. By the end, you'll have something you can drop into a real estate workflow today.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and dictionaries
  • anthropic SDK installed: pip install anthropic
  • A .env file or environment variable set for ANTHROPIC_API_KEY
📦 Full Source Code
The complete, working code for this agent is built up step by step in the sections below. Each snippet is a real piece of the final file — by Step 4, you'll have everything you need copy-paste ready in one place. No placeholder functions, no pseudocode.

Step 1: Set Up Your Claude API Client and Authentication

First, let's get the client wired up. I always keep the API key out of source code entirely — pull it from an environment variable so you never accidentally commit it to GitHub.

Install the SDK if you haven't already, then create the entry point for your project:

lead_qualifier.py — imports and client setup
import os
import json
from anthropic import Anthropic

# Pull the key from your environment — never hardcode this
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"  # Pinning the model keeps behavior consistent

That's all the setup you need. The Anthropic() constructor will also auto-detect ANTHROPIC_API_KEY from your environment if you don't pass it explicitly — but I prefer being explicit so the code is obvious to the next person reading it.

💡 Tip: Use python-dotenv and a .env file locally. Run pip install python-dotenv, then add from dotenv import load_dotenv; load_dotenv() before the client initialization. Your key stays out of version control and you don't have to set it manually every terminal session.

Step 2: Define Lead Qualification Tools

This is where AI agents get interesting. Instead of just generating text, Claude can call tools you define — think of them as functions Claude is allowed to invoke when it needs to do something structured. We're defining two tools: one to extract contact info from messy inquiry text, and one to score the lead.

Tool definitions are just Python dictionaries that follow Anthropic's schema. Claude reads them, decides when to use them, and returns structured JSON you can parse.

lead_qualifier.py — tool definitions
TOOLS = [
    {
        "name": "extract_contact_info",
        "description": (
            "Extract structured contact information from a raw real estate inquiry. "
            "Use this tool first before scoring to capture all available buyer details."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Full name of the buyer, or 'Unknown' if not found"
                },
                "email": {
                    "type": "string",
                    "description": "Email address, or 'Not provided' if missing"
                },
                "phone": {
                    "type": "string",
                    "description": "Phone number, or 'Not provided' if missing"
                },
                "budget": {
                    "type": "string",
                    "description": "Stated budget or price range, e.g. '$500k-$700k'"
                },
                "timeline": {
                    "type": "string",
                    "description": "How soon the buyer wants to purchase, e.g. '30 days', '6 months'"
                },
                "property_type": {
                    "type": "string",
                    "description": "Type of property they want, e.g. 'single-family', 'condo', 'waterfront'"
                },
                "location_preference": {
                    "type": "string",
                    "description": "Desired neighborhood or area, e.g. 'Naples, FL', 'Port Royal'"
                }
            },
            "required": ["name", "email", "phone", "budget", "timeline", "property_type", "location_preference"]
        }
    },
    {
        "name": "score_lead",
        "description": (
            "Score the real estate lead on a scale of 1-10 based on extracted information. "
            "Call this tool after extract_contact_info to produce a final qualification score."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "score": {
                    "type": "integer",
                    "description": "Lead score from 1 (cold/unqualified) to 10 (hot/ready to buy)"
                },
                "tier": {
                    "type": "string",
                    "enum": ["Hot", "Warm", "Cold"],
                    "description": "Qualification tier based on score: Hot (8-10), Warm (5-7), Cold (1-4)"
                },
                "reasoning": {
                    "type": "string",
                    "description": "2-3 sentence explanation of why this score was assigned"
                },
                "recommended_action": {
                    "type": "string",
                    "description": "What the agent should do next, e.g. 'Call within 24 hours', 'Add to drip campaign'"
                },
                "flags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of notable signals, e.g. ['pre-approved', 'specific timeline', 'repeat inquiry']"
                }
            },
            "required": ["score", "tier", "reasoning", "recommended_action", "flags"]
        }
    }
]

Notice that each tool has a clear description telling Claude when to use it, and an input_schema that enforces the exact shape of the output. The required array means Claude can't skip fields — you always get a complete object back.

Step 3: Create the Lead Qualifier Agent Class

Now we wrap everything into a class. The class holds state between tool calls, handles the conversation history, and gives us a clean interface to call from anywhere in our app. This is the pattern I use for every production agent — keep the run loop separate from the business logic.

lead_qualifier.py — LeadQualifierAgent class
class LeadQualifierAgent:
    """Qualifies real estate leads using Claude's tool use feature."""

    def __init__(self):
        self.client = client
        self.model = MODEL
        self.tools = TOOLS
        # Stores extracted contact data after the first tool call
        self.contact_info = {}
        # Stores the final lead score after the second tool call
        self.lead_score = {}

    def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        """
        Execute the tool Claude requested and return the result as a string.
        In production you'd persist this to a database here.
        """
        if tool_name == "extract_contact_info":
            self.contact_info = tool_input
            return json.dumps({
                "status": "success",
                "message": "Contact information extracted successfully",
                "data": tool_input
            })

        elif tool_name == "score_lead":
            self.lead_score = tool_input
            return json.dumps({
                "status": "success",
                "message": "Lead scored successfully",
                "data": tool_input
            })

        # Catch-all for unknown tools — prevents silent failures
        return json.dumps({"status": "error", "message": f"Unknown tool: {tool_name}"})

The process_tool_call method is where you'd plug in your real business logic. Right now it stores the data in memory — but in production, this is where you'd write to your CRM, fire a webhook to Follow Up Boss, or insert a row into your Supabase database.

Step 4: Implement the Agent Run Loop with Tool Use

This is the core of the agent — the agentic loop that keeps running until Claude says it's done. Claude might call one tool, two tools, or decide it has enough context without calling any. The loop handles all of that.

lead_qualifier.py — agent run loop and qualify method
    def qualify_lead(self, inquiry_text: str) -> dict:
        """
        Main entry point. Pass in raw inquiry text, get back a full qualification report.
        """
        print(f"\n{'='*60}")
        print("LEAD QUALIFIER AGENT STARTING")
        print(f"{'='*60}")
        print(f"Analyzing inquiry:\n{inquiry_text}\n")

        # Seed the conversation with the lead inquiry
        messages = [
            {
                "role": "user",
                "content": (
                    f"Please qualify this real estate lead inquiry. "
                    f"First extract the contact information, then score the lead.\n\n"
                    f"Inquiry:\n{inquiry_text}"
                )
            }
        ]

        system_prompt = (
            "You are a real estate lead qualification specialist. "
            "Your job is to analyze buyer inquiries and determine how likely they are to purchase. "
            "Always call extract_contact_info first, then call score_lead. "
            "Be direct and data-driven in your scoring."
        )

        # Agentic loop — runs until Claude returns stop_reason='end_turn'
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=system_prompt,
                tools=self.tools,
                messages=messages
            )

            print(f"Stop reason: {response.stop_reason}")

            # Claude is done — no more tool calls
            if response.stop_reason == "end_turn":
                # Pull the final text response if Claude added a summary
                final_text = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        final_text = block.text
                        break
                print(f"\nAgent complete. Final message: {final_text}")
                break

            # Claude wants to call one or more tools
            if response.stop_reason == "tool_use":
                # Append Claude's response (including tool use blocks) to history
                messages.append({"role": "assistant", "content": response.content})

                # Collect all tool results before sending back
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        print(f"\n→ Tool called: {block.name}")
                        print(f"  Input: {json.dumps(block.input, indent=2)}")

                        result = self.process_tool_call(block.name, block.input)
                        print(f"  Result: {result}")

                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,  # Must match the tool_use block id
                            "content": result
                        })

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

        # Build and return the final qualification report
        return {
            "contact_info": self.contact_info,
            "lead_score": self.lead_score,
            "inquiry_text": inquiry_text
        }

The key thing to understand here is the message history. Every time Claude calls a tool, you append its response to messages, then append your tool results, and send the whole conversation back. Claude reads the full history each time — that's how it knows what it already did.

⚠️ Common Mistake: A lot of tutorials send tool results back one at a time inside the loop. That's wrong — if Claude calls two tools in one turn, you need to collect all the results and send them back together in a single user message. The code above does this correctly with the tool_results list.

Full Source Code — Complete Working File

Here's the entire file in one place. This is exactly what you'd save as lead_qualifier.py and run from your terminal.

lead_qualifier.py — complete file
import os
import json
from anthropic import Anthropic

client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

TOOLS = [
    {
        "name": "extract_contact_info",
        "description": (
            "Extract structured contact information from a raw real estate inquiry. "
            "Use this tool first before scoring to capture all available buyer details."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Full name of the buyer, or 'Unknown' if not found"},
                "email": {"type": "string", "description": "Email address, or 'Not provided' if missing"},
                "phone": {"type": "string", "description": "Phone number, or 'Not provided' if missing"},
                "budget": {"type": "string", "description": "Stated budget or price range"},
                "timeline": {"type": "string", "description": "How soon the buyer wants to purchase"},
                "property_type": {"type": "string", "description": "Type of property they want"},
                "location_preference": {"type": "string", "description": "Desired neighborhood or area"}
            },
            "required": ["name", "email", "phone", "budget", "timeline", "property_type", "location_preference"]
        }
    },
    {
        "name": "score_lead",
        "description": (
            "Score the real estate lead on a scale of 1-10 based on extracted information. "
            "Call this tool after extract_contact_info to produce a final qualification score."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "score": {"type": "integer", "description": "Lead score from 1 to 10"},
                "tier": {"type": "string", "enum": ["Hot", "Warm", "Cold"], "description": "Qualification tier"},
                "reasoning": {"type": "string", "description": "2-3 sentence explanation of the score"},
                "recommended_action": {"type": "string", "description": "What to do next with this lead"},
                "flags": {"type": "array", "items": {"type": "string"}, "description": "Notable buyer signals"}
            },
            "required": ["score", "tier", "reasoning", "recommended_action", "flags"]
        }
    }
]


class LeadQualifierAgent:
    """Qualifies real estate leads using Claude's tool use feature."""

    def __init__(self):
        self.client = client
        self.model = MODEL
        self.tools = TOOLS
        self.contact_info = {}
        self.lead_score = {}

    def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        if tool_name == "extract_contact_info":
            self.contact_info = tool_input
            return json.dumps({"status": "success", "message": "Contact information extracted", "data": tool_input})
        elif tool_name == "score_lead":
            self.lead_score = tool_input
            return json.dumps({"status": "success", "message": "Lead scored", "data": tool_input})
        return json.dumps({"status": "error", "message": f"Unknown tool: {tool_name}"})

    def qualify_lead(self, inquiry_text: str) -> dict:
        print(f"\n{'='*60}")
        print("LEAD QUALIFIER AGENT STARTING")
        print(f"{'='*60}")
        print(f"Analyzing inquiry:\n{inquiry_text}\n")

        messages = [
            {
                "role": "user",
                "content": (
                    f"Please qualify this real estate lead inquiry. "
                    f"First extract the contact information, then score the lead.\n\n"
                    f"Inquiry:\n{inquiry_text}"
                )
            }
        ]

        system_prompt = (
            "You are a real estate lead qualification specialist. "
            "Analyze buyer inquiries and determine purchase likelihood. "
            "Always call extract_contact_info first, then call score_lead. "
            "Be direct and data-driven in your scoring."
        )

        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=system_prompt,
                tools=self.tools,
                messages=messages
            )

            print(f"Stop reason: {response.stop_reason}")

            if response.stop_reason == "end_turn":
                final_text = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        final_text = block.text
                        break
                print(f"\nAgent complete. Final message: {final_text}")
                break

            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":
                        print(f"\n→ Tool called: {block.name}")
                        print(f"  Input: {json.dumps(block.input, indent=2)}")
                        result = self.process_tool_call(block.name, block.input)
                        print(f"  Result: {result}")
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result
                        })
                messages.append({"role": "user", "content": tool_results})

        return {
            "contact_info": self.contact_info,
            "lead_score": self.lead_score,
            "inquiry_text": inquiry_text
        }


def print_report(result: dict) -> None:
    """Print a formatted qualification report to the console."""
    contact = result["contact_info"]
    score = result["lead_score"]

    print(f"\n{'='*60}")
    print("LEAD QUALIFICATION REPORT")
    print(f"{'='*60}")
    print(f"Name:              {contact.get('name', 'N/A')}")
    print(f"Email:             {contact.get('email', 'N/A')}")
    print(f"Phone:             {contact.get('phone', 'N/A')}")
    print(f"Budget:            {contact.get('budget', 'N/A')}")
    print(f"Timeline:          {contact.get('timeline', 'N/A')}")
    print(f"Property Type:     {contact.get('property_type', 'N/A')}")
    print(f"Location:          {contact.get('location_preference', 'N/A')}")
    print(f"\n--- SCORE ---")
    print(f"Score:             {score.get('score', 'N/A')}/10")
    print(f"Tier:              {score.get('tier', 'N/A')}")
    print(f"Reasoning:         {score.get('reasoning', 'N/A')}")
    print(f"Recommended Action:{score.get('recommended_action', 'N/A')}")
    print(f"Flags:             {', '.join(score.get('flags', []))}")
    print(f"{'='*60}\n")


if __name__ == "__main__":
    # Sample inquiry — the kind that comes in through a Zillow or website form
    sample_inquiry = """
    Hi, my name is Maria Gonzalez. I found your listing on Zillow and I'm very interested
    in waterfront properties in Naples, FL. My budget is around $1.2M to $1.5M and I'm
    looking to close within the next 60 days — we're relocating from Chicago for work.
    I'm pre-approved through Chase. My email is [email protected] and you can
    reach me at (312) 555-0192. We'd prefer a single-family home with at least 3 beds.
    """

    agent = LeadQualifierAgent()
    result = agent.qualify_lead(sample_inquiry)
    print_report(result)

Example Conversation and Lead Score Output

Here's what you actually see in your terminal when you run this against the sample inquiry above. The agent calls both tools in sequence and produces a clean report at the end.

terminal output
============================================================
LEAD QUALIFIER AGENT STARTING
============================================================
Analyzing inquiry:
    Hi, my name is Maria Gonzalez. I found your listing on Zillow...

Stop reason: tool_use

→ Tool called: extract_contact_info
  Input: {
    "name": "Maria Gonzalez",