← Back to Blog

What You'll Build

By the end of this tutorial, you'll have a fully working AI lead qualifier agent written in Python that uses the Claude API to evaluate inbound leads, score them on a 1–10 scale, and return a structured recommendation — whether to call now, nurture, or disqualify.

The agent uses Claude's tool use feature to run a multi-step qualification loop, meaning it doesn't just answer once — it reasons, calls tools, and acts like a real sales rep reviewing a lead file.

This is the exact pattern we use at Naples AI when we build custom lead automation systems for real estate teams, car dealerships, and service businesses in Southwest Florida.

📦 Full Source Code
All the code in this tutorial is complete and production-ready. Every snippet below builds on the last one. By Step 4, you'll have a running agent. Copy each block in order, or scroll to the end of each step to grab the full file. No pseudocode — everything here runs.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic Python knowledge — you don't need to know anything about AI
  • anthropic Python SDK installed (pip install anthropic)
  • A terminal and a code editor (VS Code works great)

Step 1: Set Up Your Claude API Environment

First, install the Anthropic SDK and set your API key as an environment variable. Never hardcode your API key in source files — treat it like a password.

Run this in your terminal:

terminal
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-your-key-here"

Now create a new file called lead_qualifier.py and add this at the top. This confirms your environment is wired up correctly before you write a single line of agent logic.

lead_qualifier.py
import os
import json
import anthropic

# Verify the key is loaded before doing anything else
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
    raise EnvironmentError("ANTHROPIC_API_KEY not set. Run: export ANTHROPIC_API_KEY='your-key'")

client = anthropic.Anthropic(api_key=api_key)

print("✅ Anthropic client initialized successfully.")

Run python lead_qualifier.py and you should see the success message. If you get an error here, check that your export command ran in the same terminal session.

💡 Tip: If you're on Windows, set the environment variable with set ANTHROPIC_API_KEY=your-key in Command Prompt, or use a .env file with the python-dotenv package.

Step 2: Define Lead Qualification Tools

This is where Claude gets its superpowers. Tools in the Claude API let the model call structured functions instead of just generating text — so instead of guessing at a score, it fills out a real data structure.

We're defining three tools: one to score the lead, one to look up industry benchmarks, and one to generate a follow-up recommendation. Together they give the agent everything it needs to reason through a lead end to end.

lead_qualifier.py
import os
import json
import anthropic

api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
    raise EnvironmentError("ANTHROPIC_API_KEY not set.")

client = anthropic.Anthropic(api_key=api_key)

# Tool definitions — these tell Claude what functions it can call and what
# parameters each one expects. Claude will decide when to call them.
TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Evaluates a sales lead and assigns a numeric quality score. "
            "Use this to assess how likely a lead is to convert based on "
            "budget, timeline, authority, and need (BANT framework)."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_name": {
                    "type": "string",
                    "description": "Full name of the lead"
                },
                "score": {
                    "type": "integer",
                    "description": "Lead quality score from 1 (worst) to 10 (best)",
                    "minimum": 1,
                    "maximum": 10
                },
                "bant_breakdown": {
                    "type": "object",
                    "description": "Scores for each BANT dimension (1-10 each)",
                    "properties": {
                        "budget":    {"type": "integer", "minimum": 1, "maximum": 10},
                        "authority": {"type": "integer", "minimum": 1, "maximum": 10},
                        "need":      {"type": "integer", "minimum": 1, "maximum": 10},
                        "timeline":  {"type": "integer", "minimum": 1, "maximum": 10}
                    },
                    "required": ["budget", "authority", "need", "timeline"]
                },
                "disqualifiers": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of any red flags or disqualifying factors found"
                }
            },
            "required": ["lead_name", "score", "bant_breakdown", "disqualifiers"]
        }
    },
    {
        "name": "get_industry_benchmark",
        "description": (
            "Returns average deal size and typical sales cycle length for a given industry. "
            "Use this to contextualize whether a lead's budget and timeline are realistic."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "industry": {
                    "type": "string",
                    "description": "Industry name, e.g. 'real estate', 'healthcare', 'restaurant'"
                }
            },
            "required": ["industry"]
        }
    },
    {
        "name": "generate_recommendation",
        "description": (
            "Produces a final sales action recommendation based on lead score and context. "
            "Call this last, after scoring and benchmarking are complete."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["call_now", "nurture", "disqualify"],
                    "description": "The recommended next action for this lead"
                },
                "priority": {
                    "type": "string",
                    "enum": ["high", "medium", "low"],
                    "description": "Priority level for the sales team"
                },
                "talking_points": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "3 specific talking points the sales rep should use on the call"
                },
                "follow_up_timing": {
                    "type": "string",
                    "description": "When to follow up, e.g. 'within 24 hours', 'next quarter'"
                }
            },
            "required": ["action", "priority", "talking_points", "follow_up_timing"]
        }
    }
]

Notice that each tool has a description field — this is what Claude actually reads to decide when to call the tool. Write those descriptions like you'd explain the function to a smart intern. The more specific you are, the better the model's decisions.

Step 3: Build the Agent Class with Tool Use

Now we wrap everything in a clean class. The LeadQualifierAgent holds the client, tools, and a simulated knowledge base that the get_industry_benchmark tool draws from.

The execute_tool method is the bridge between Claude's decisions and your actual Python logic. When Claude says "call this tool with these inputs," this method runs it and hands the result back.

lead_qualifier.py
import os
import json
import anthropic

api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
    raise EnvironmentError("ANTHROPIC_API_KEY not set.")

client = anthropic.Anthropic(api_key=api_key)

TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Evaluates a sales lead and assigns a numeric quality score. "
            "Use this to assess how likely a lead is to convert based on "
            "budget, timeline, authority, and need (BANT framework)."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_name": {"type": "string"},
                "score": {"type": "integer", "minimum": 1, "maximum": 10},
                "bant_breakdown": {
                    "type": "object",
                    "properties": {
                        "budget":    {"type": "integer", "minimum": 1, "maximum": 10},
                        "authority": {"type": "integer", "minimum": 1, "maximum": 10},
                        "need":      {"type": "integer", "minimum": 1, "maximum": 10},
                        "timeline":  {"type": "integer", "minimum": 1, "maximum": 10}
                    },
                    "required": ["budget", "authority", "need", "timeline"]
                },
                "disqualifiers": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["lead_name", "score", "bant_breakdown", "disqualifiers"]
        }
    },
    {
        "name": "get_industry_benchmark",
        "description": (
            "Returns average deal size and typical sales cycle length for a given industry."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "industry": {"type": "string"}
            },
            "required": ["industry"]
        }
    },
    {
        "name": "generate_recommendation",
        "description": (
            "Produces a final sales action recommendation based on lead score and context. "
            "Call this last, after scoring and benchmarking are complete."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["call_now", "nurture", "disqualify"]
                },
                "priority": {
                    "type": "string",
                    "enum": ["high", "medium", "low"]
                },
                "talking_points": {"type": "array", "items": {"type": "string"}},
                "follow_up_timing": {"type": "string"}
            },
            "required": ["action", "priority", "talking_points", "follow_up_timing"]
        }
    }
]


class LeadQualifierAgent:
    """
    An AI agent that qualifies sales leads using Claude's tool use feature.
    It scores leads, benchmarks against industry data, and recommends next actions.
    """

    def __init__(self):
        self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"
        self.tools = TOOLS
        self.max_iterations = 10  # Safety cap on the agentic loop

        # Simulated industry benchmark database
        # In production, replace this with a real CRM or database query
        self.industry_benchmarks = {
            "real estate": {
                "avg_deal_size": "$12,000",
                "sales_cycle": "30-90 days",
                "notes": "Urgency driven by market conditions. Decision maker is usually the broker or team lead."
            },
            "healthcare": {
                "avg_deal_size": "$25,000",
                "sales_cycle": "60-180 days",
                "notes": "Long compliance review cycles. Identify the practice manager early."
            },
            "restaurant": {
                "avg_deal_size": "$3,500",
                "sales_cycle": "7-21 days",
                "notes": "Fast decisions, budget-sensitive. Owner is usually the decision maker."
            },
            "car dealership": {
                "avg_deal_size": "$18,000",
                "sales_cycle": "14-45 days",
                "notes": "ROI-driven buyers. Focus on lead volume and floor traffic metrics."
            },
            "manufacturing": {
                "avg_deal_size": "$40,000",
                "sales_cycle": "90-270 days",
                "notes": "Committee decisions. Expect multiple stakeholders and an RFP process."
            }
        }

    def execute_tool(self, tool_name: str, tool_input: dict) -> str:
        """
        Executes the tool Claude requested and returns the result as a string.
        Claude sees this return value and uses it to continue reasoning.
        """
        if tool_name == "score_lead":
            # Claude has already done the scoring — we just acknowledge and log it
            result = {
                "status": "scored",
                "lead_name": tool_input["lead_name"],
                "overall_score": tool_input["score"],
                "bant": tool_input["bant_breakdown"],
                "disqualifiers": tool_input["disqualifiers"]
            }
            return json.dumps(result)

        elif tool_name == "get_industry_benchmark":
            industry = tool_input["industry"].lower().strip()
            # Try exact match first, then partial match
            benchmark = self.industry_benchmarks.get(industry)
            if not benchmark:
                for key in self.industry_benchmarks:
                    if key in industry or industry in key:
                        benchmark = self.industry_benchmarks[key]
                        break
            if not benchmark:
                benchmark = {
                    "avg_deal_size": "Unknown",
                    "sales_cycle": "Unknown",
                    "notes": "No benchmark data available for this industry."
                }
            return json.dumps(benchmark)

        elif tool_name == "generate_recommendation":
            # Store the final recommendation so we can return it after the loop
            self._final_recommendation = tool_input
            result = {
                "status": "recommendation_generated",
                "action": tool_input["action"],
                "priority": tool_input["priority"],
                "follow_up_timing": tool_input["follow_up_timing"],
                "talking_points": tool_input["talking_points"]
            }
            return json.dumps(result)

        else:
            return json.dumps({"error": f"Unknown tool: {tool_name}"})

Step 4: Implement the Agentic Loop

This is the core of the tutorial. The agentic loop is what makes this an agent instead of just a single API call. Claude responds, we check if it wants to use a tool, run the tool, feed the result back, and repeat — until Claude says it's done.

Most beginners write a single client.messages.create() call and wonder why the AI doesn't actually do anything complex. The loop is the missing piece. It's what turns Claude from a chatbot into a reasoning system that takes actions.

lead_qualifier.py
    def qualify_lead(self, lead_data: dict) -> dict:
        """
        Runs the full agentic qualification loop for a single lead.
        Returns the final structured recommendation dict.
        """
        self._final_recommendation = None

        # Build the initial prompt with all lead info formatted clearly
        lead_summary = "\n".join([f"- {k}: {v}" for k, v in lead_data.items()])
        system_prompt = (
            "You are an expert B2B sales qualifier. Your job is to evaluate inbound leads "
            "using the BANT framework (Budget, Authority, Need, Timeline). "
            "Always use your tools in this order: first score_lead, then get_industry_benchmark, "
            "then generate_recommendation. Be precise and data-driven. "
            "Do not skip steps or generate a recommendation without scoring first."
        )

        messages = [
            {
                "role": "user",
                "content": f"Please qualify this lead and provide a full recommendation:\n\n{lead_summary}"
            }
        ]

        print(f"\n🔍 Starting qualification for: {lead_data.get('name', 'Unknown')}")

        # The agentic loop — keeps running until Claude stops requesting tools
        for iteration in range(self.max_iterations):
            print(f"   → Iteration {iteration + 1}...")

            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=system_prompt,
                tools=self.tools,
                messages=messages
            )

            # Add Claude's full response to the message history
            messages.append({"role": "assistant", "content": response.content})

            # Check why Claude stopped
            if response.stop_reason == "end_turn":
                # Claude is done — no more tool calls
                print("   ✅ Agent completed reasoning.")
                break

            if response.stop_reason == "tool_use":
                # Claude wants to call one or more tools
                tool_results = []

                for block in response.content:
                    if block.type == "tool_use":
                        print(f"   🔧 Calling tool: {block.name}")
                        result = self.execute_tool(block.name, block.input)

                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result
                        })

                # Feed all tool results back to Claude as a user message
                messages.append({
                    "role": "user",
                    "content": tool_results
                })

            else:
                # Unexpected stop reason — log and exit the loop safely
                print(f"   ⚠️  Unexpected stop reason: {response.stop_reason}")
                break

        if not self._final_recommendation:
            return {"error": "Agent did not produce a final recommendation."}

        return self._final_recommendation
⚠️ Watch the loop count: The max_iterations guard is there to prevent runaway API calls if something goes wrong. In production, also add cost tracking so you know exactly how many tokens each lead qualification uses.

Step 5: Parse and Score Leads

Now let's wire everything together with a sample lead, run the agent, and print a clean output. This is the part you'd eventually connect to a CRM webhook, a form submission handler, or a nightly batch job.

I've included two test leads — one strong and one weak — so you can see how the agent handles both ends of the spectrum.

lead_qualifier.py
def format_output(lead_name: str, result: dict) -> None:
    """Pretty-prints the qualification result to the terminal."""
    print("\n" + "=" * 55)
    print(f"  LEAD QUALIFICATION REPORT: {lead_name}")
    print("=" * 55)

    if "error" in result:
        print(f"  ❌ Error: {result['error']}")
        return

    action_emoji = {
        "call_now":   "📞",
        "nurture":    "🌱",
        "disqualify": "🚫"
    }.get(result.get("action"), "❓")

    priority_emoji = {
        "high":   "🔴",
        "medium": "🟡",
        "low":    "🟢"
    }.get(result.get("priority"), "⚪")

    print(f"  Action:   {action_emoji}  {result.get('action', 'N/A').upper()}")
    print(f"  Priority: {priority_emoji}  {result.get('priority', 'N/A').upper()}")
    print(f"  Follow Up: {result.get('follow_up_timing', 'N/A')}")
    print("\n  Talking Points:")
    for i, point in enumerate(result.get("talking_points", []), 1):
        print(f"    {i}. {point}")
    print("=" * 55 + "\n")


if __name__ == "__main__":
    agent = LeadQualifierAgent()

    # --- Lead 1: Strong lead ---
    strong_lead = {
        "name": "Maria Gonzalez",
        "company": "Coastal Realty Group",
        "industry": "Real Estate",
        "title": "Broker / Owner",
        "budget": "$15,000 - $20,000 available for technology this quarter",
        "need": "Wants to automate listing descriptions and lead follow-up emails",
        "timeline": "Ready to start within 30 days",
        "source": "Referral from existing client",
        "notes": "Has 22 agents, currently spending 8 hours/week manually writing listings"
    }

    result_1 = agent.qualify_lead(strong_lead)
    format_output(strong_lead["name"], result_1)

    # --- Lead 2: Weak lead ---
    weak_lead = {
        "name": "Tom Briggs",
        "company": "Tom's Diner",
        "industry": "Restaurant",
        "title": "Part-time manager",
        "budget": "No formal budget, owner would need to approve anything over $200",
        "need": "Vagu