All articles

What You'll Build

If you've ever watched a sales team manually sort through dozens of inbound leads, copying data from forms into spreadsheets and guessing who's worth a call, you already know the problem this tutorial solves. By the end of this guide, you'll have a fully working AI lead qualifier agent in Python that reads raw lead data, extracts key information using Claude's tool use API, and outputs a numeric score with a classification (Hot, Warm, or Cold).

The whole thing runs in about 150 lines of code using the Anthropic SDK and the model claude-sonnet-4-6. No external databases, no complex infrastructure, just a clean Python script you can drop into any existing workflow.

Prerequisites

  • Python 3.10 or higher installed on your machine
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and functions
  • The anthropic Python package (we'll install it in Step 1)
  • A terminal or code editor like VS Code
Full Source Code Note: The complete, working script is built piece by piece in the steps below. Every snippet connects to the next one, and by the end of Step 6 you'll have the entire agent ready to run. You can also copy each block in order into a single file called lead_qualifier.py.

Step 1: Set Up Your Environment and Install Dependencies

Start by creating a fresh project folder and installing the Anthropic SDK. I always use a virtual environment here so nothing bleeds into other projects on my machine.

terminal
mkdir lead-qualifier-agent
cd lead-qualifier-agent
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install anthropic python-dotenv

Next, create a .env file in the project root to store your API key. Never hardcode secrets in your source files.

.env
ANTHROPIC_API_KEY=your_api_key_here

Now create the main script file. The first block handles imports and loads the environment variable.

lead_qualifier.py
import os
import json
import anthropic
from dotenv import load_dotenv

load_dotenv()

ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
MODEL = "claude-sonnet-4-6"

Step 2: Initialize the Claude Client and Define Your Tools

Claude's tool use feature lets the model call structured functions during a conversation instead of just returning free-text. We're going to define two tools: one to extract structured fields from raw lead text, and one to compute a qualification score.

Think of tool definitions as a contract. You tell Claude what the function is called, what it does, and exactly what parameters it expects, and Claude decides when and how to call it.

lead_qualifier.py (continued)
TOOLS = [
    {
        "name": "extract_lead_data",
        "description": (
            "Extracts structured information from raw lead text. "
            "Call this first to parse fields like company size, budget, "
            "timeline, and pain points from unstructured input."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "company_name": {
                    "type": "string",
                    "description": "Name of the prospect's company"
                },
                "industry": {
                    "type": "string",
                    "description": "Industry or business sector"
                },
                "company_size": {
                    "type": "string",
                    "enum": ["1-10", "11-50", "51-200", "201-500", "500+"],
                    "description": "Approximate number of employees"
                },
                "budget_range": {
                    "type": "string",
                    "description": "Stated or implied budget for the project"
                },
                "timeline": {
                    "type": "string",
                    "description": "How soon they want to move forward"
                },
                "pain_points": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of specific problems they mentioned"
                },
                "decision_maker": {
                    "type": "boolean",
                    "description": "True if the contact appears to be a decision maker"
                }
            },
            "required": [
                "company_name", "industry", "company_size",
                "pain_points", "decision_maker"
            ]
        }
    },
    {
        "name": "score_lead",
        "description": (
            "Calculates a qualification score from 0 to 100 based on "
            "the extracted lead data. Call this after extract_lead_data."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "budget_score": {
                    "type": "integer",
                    "description": "Score 0-30 based on budget fit"
                },
                "timeline_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on urgency of timeline"
                },
                "fit_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on industry and company size fit"
                },
                "authority_score": {
                    "type": "integer",
                    "description": "Score 0-20 based on whether contact is a decision maker"
                },
                "reasoning": {
                    "type": "string",
                    "description": "One or two sentences explaining the score"
                }
            },
            "required": [
                "budget_score", "timeline_score",
                "fit_score", "authority_score", "reasoning"
            ]
        }
    }
]

Step 3: Create Tool Functions for Lead Data Extraction

When Claude calls a tool, your Python code needs to actually do something with that call and return a result. These functions are the backend that Claude is orchestrating. The first function stores the extracted data, and the second calculates the final score.

lead_qualifier.py (continued)
def handle_extract_lead_data(tool_input: dict) -> dict:
    """Store and return the structured lead data Claude extracted."""
    # Pass the data through so it's available in the next tool call
    return {
        "status": "success",
        "extracted_data": tool_input
    }


def handle_score_lead(tool_input: dict) -> dict:
    """Sum the sub-scores and classify the lead."""
    budget_score = tool_input.get("budget_score", 0)
    timeline_score = tool_input.get("timeline_score", 0)
    fit_score = tool_input.get("fit_score", 0)
    authority_score = tool_input.get("authority_score", 0)

    total_score = budget_score + timeline_score + fit_score + authority_score

    # Classify based on total
    if total_score >= 75:
        classification = "Hot"
    elif total_score >= 45:
        classification = "Warm"
    else:
        classification = "Cold"

    return {
        "total_score": total_score,
        "classification": classification,
        "score_breakdown": {
            "budget": budget_score,
            "timeline": timeline_score,
            "fit": fit_score,
            "authority": authority_score
        },
        "reasoning": tool_input.get("reasoning", "")
    }


def process_tool_call(tool_name: str, tool_input: dict) -> dict:
    """Route a tool call to the correct handler function."""
    if tool_name == "extract_lead_data":
        return handle_extract_lead_data(tool_input)
    elif tool_name == "score_lead":
        return handle_score_lead(tool_input)
    else:
        return {"error": f"Unknown tool: {tool_name}"}

Step 4: Build the Main Agent Loop with Tool Use

This is where everything comes together. The agent loop sends messages to Claude, checks whether Claude wants to use a tool, runs the tool, and feeds the result back until Claude is done. This is the standard agentic pattern with the Anthropic SDK.

I built this as a class so it's easy to instantiate and call from other scripts or a web framework later.

lead_qualifier.py (continued)
class LeadQualifierAgent:
    def __init__(self):
        self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
        self.extracted_data = {}
        self.score_result = {}

    def qualify_lead(self, lead_text: str) -> dict:
        """
        Main entry point. Takes raw lead text and returns
        a fully qualified lead report.
        """
        messages = [
            {
                "role": "user",
                "content": (
                    "Please qualify this lead by first extracting the key data "
                    "using the extract_lead_data tool, then scoring it using the "
                    "score_lead tool. Here is the lead information:\n\n"
                    + lead_text
                )
            }
        ]

        system_prompt = (
            "You are an expert sales qualification agent. Your job is to analyze "
            "inbound lead information and determine how qualified the prospect is. "
            "Always call extract_lead_data first, then call score_lead. "
            "Be realistic and critical in your scoring."
        )

        # Agent loop: keep going until Claude stops calling tools
        while True:
            response = self.client.messages.create(
                model=MODEL,
                max_tokens=1024,
                system=system_prompt,
                tools=TOOLS,
                messages=messages
            )

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

            # If Claude is done (no more tool calls), break the loop
            if response.stop_reason == "end_turn":
                break

            # Check if Claude wants to use tools
            tool_use_blocks = [
                block for block in response.content
                if block.type == "tool_use"
            ]

            if not tool_use_blocks:
                break

            # Process each tool_use_block Claude returned
            tool_results = []
            for tool_use_block in tool_use_blocks:
                tool_name = tool_use_block.name
                tool_input = tool_use_block.input

                print(f"  [Agent] Calling tool: {tool_name}")

                result = process_tool_call(tool_name, tool_input)

                # Store results locally for the final report
                if tool_name == "extract_lead_data":
                    self.extracted_data = result.get("extracted_data", {})
                elif tool_name == "score_lead":
                    self.score_result = result

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

            # Feed tool results back to Claude so it can continue
            messages.append({
                "role": "user",
                "content": tool_results
            })

        return self._build_report()

    def _build_report(self) -> dict:
        """Assemble the final lead qualification report."""
        return {
            "lead_data": self.extracted_data,
            "qualification": self.score_result
        }
Why the while loop? Claude might need multiple rounds of tool use before it's finished. The loop keeps the conversation alive until Claude returns a stop reason of end_turn, which means it has nothing more to do. This is the correct pattern for any agentic workflow with the Anthropic SDK.

Step 5: Implement Scoring Logic and Lead Classification

The scoring logic lives inside handle_score_lead, but you need a way to print the report in a readable format. Add this helper function and the main execution block at the bottom of your file.

lead_qualifier.py (continued)
def print_report(report: dict) -> None:
    """Print a formatted lead qualification report to the console."""
    lead = report.get("lead_data", {})
    qual = report.get("qualification", {})
    breakdown = qual.get("score_breakdown", {})

    print("\n" + "=" * 55)
    print("         LEAD QUALIFICATION REPORT")
    print("=" * 55)
    print(f"Company:        {lead.get('company_name', 'N/A')}")
    print(f"Industry:       {lead.get('industry', 'N/A')}")
    print(f"Company Size:   {lead.get('company_size', 'N/A')} employees")
    print(f"Budget Range:   {lead.get('budget_range', 'Not stated')}")
    print(f"Timeline:       {lead.get('timeline', 'Not stated')}")
    print(f"Decision Maker: {'Yes' if lead.get('decision_maker') else 'No'}")
    print(f"Pain Points:")
    for point in lead.get("pain_points", []):
        print(f"  - {point}")
    print("-" * 55)
    print(f"Score Breakdown:")
    print(f"  Budget fit:     {breakdown.get('budget', 0)}/30")
    print(f"  Timeline:       {breakdown.get('timeline', 0)}/25")
    print(f"  Industry fit:   {breakdown.get('fit', 0)}/25")
    print(f"  Authority:      {breakdown.get('authority', 0)}/20")
    print(f"  TOTAL SCORE:    {qual.get('total_score', 0)}/100")
    print(f"  CLASSIFICATION: {qual.get('classification', 'Unknown')}")
    print("-" * 55)
    print(f"Reasoning: {qual.get('reasoning', '')}")
    print("=" * 55 + "\n")

Step 6: Test With Real Lead Data and Validate Output

Now let's wire everything together and run it with two realistic lead samples, one strong and one weak, so you can see the scoring difference. Add this block at the bottom of the file.

lead_qualifier.py (continued)
if __name__ == "__main__":
    agent = LeadQualifierAgent()

    # --- Lead 1: High-quality prospect ---
    hot_lead = """
    Contact: Maria Gonzalez, VP of Operations
    Company: Coastal Orthopedics Group
    Industry: Healthcare / Medical Practice
    Employees: Approximately 120 staff across 3 locations in Naples, FL

    Maria reached out after seeing our AI automation demo. They are currently
    losing 15-20 hours per week on manual patient intake forms and insurance
    pre-authorization. She has full budget authority and her CEO has already
    signed off on exploring AI solutions. Budget is pre-approved at $40,000-$60,000.
    They want to have something in place within 60 days before their busy season.
    """

    # --- Lead 2: Weak prospect ---
    cold_lead = """
    Contact: Jake (no last name given)
    Company: Jake's Lawn Care
    Industry: Landscaping
    Employees: Just Jake and one part-time helper

    Jake heard about AI on a podcast and is curious what it could do for his
    lawn care business. He doesn't have a specific problem in mind. No budget
    mentioned. He said he'd need to talk to his wife before committing to anything.
    No urgency, just exploring.
    """

    print("\nQualifying Lead 1: Coastal Orthopedics Group")
    print("[Agent] Starting qualification...")
    report1 = agent.qualify_lead(hot_lead)
    print_report(report1)

    # Reset stored data between leads
    agent.extracted_data = {}
    agent.score_result = {}

    print("\nQualifying Lead 2: Jake's Lawn Care")
    print("[Agent] Starting qualification...")
    report2 = agent.qualify_lead(cold_lead)
    print_report(report2)

Run the script with python lead_qualifier.py and you should see output like this:

sample output
Qualifying Lead 1: Coastal Orthopedics Group
[Agent] Starting qualification...
  [Agent] Calling tool: extract_lead_data
  [Agent] Calling tool: score_lead

=======================================================
         LEAD QUALIFICATION REPORT
=======================================================
Company:        Coastal Orthopedics Group
Industry:       Healthcare / Medical Practice
Company Size:   51-200 employees
Budget Range:   $40,000-$60,000
Timeline:       60 days
Decision Maker: Yes
Pain Points:
  - Manual patient intake forms consuming 15-20 hours per week
  - Insurance pre-authorization handled manually
-------------------------------------------------------
Score Breakdown:
  Budget fit:     28/30
  Timeline:       23/25
  Industry fit:   22/25
  Authority:      20/20
  TOTAL SCORE:    93/100
  CLASSIFICATION: Hot
-------------------------------------------------------
Reasoning: Strong budget, clear urgency, and full decision-making
authority make this an excellent prospect. Healthcare automation
is a proven fit for our AI solutions.
=======================================================

Qualifying Lead 2: Jake's Lawn Care
[Agent] Starting qualification...
  [Agent] Calling tool: extract_lead_data
  [Agent] Calling tool: score_lead

=======================================================
         LEAD QUALIFICATION REPORT
=======================================================
Company:        Jake's Lawn Care
Industry:       Landscaping
Company Size:   1-10 employees
Budget Range:   Not stated
Timeline:       Not stated
Decision Maker: No
Pain Points:
  - General curiosity about AI, no specific problem identified
-------------------------------------------------------
Score Breakdown:
  Budget fit:     3/30
  Timeline:       2/25
  Industry fit:   8/25
  Authority:      5/20
  TOTAL SCORE:    18/100
  CLASSIFICATION: Cold
-------------------------------------------------------
Reasoning: No defined budget, no urgency, and no clear pain point.
Contact is not the sole decision maker. Follow up in 6 months.
=======================================================

How It Works

Claude receives the raw lead text and a system prompt telling it to act as a sales qualification expert. It decides on its own to call extract_lead_data first, filling in the structured schema from whatever information is available in the text.

Your Python code catches that tool call, runs the handler function, and sends the result back to Claude as a tool result message. Claude then calls score_lead with numeric sub-scores based on everything it just learned.

The agent loop keeps this conversation going until Claude has nothing left to do, at which point you assemble the final report from the stored results. The model handles all the ambiguity in the natural language input, and your Python functions handle the deterministic math.

Common Errors and Fixes

Error 1: AuthenticationError on startup

anthropic.AuthenticationError: Invalid API key

This means your API key isn't loading. Make sure your .env file is in the same directory as your script and that you called load_dotenv() before accessing os.getenv(). Double-check the key has no trailing spaces in the file.

Error 2: AttributeError on tool_use_block

AttributeError: 'TextBlock' object has no attribute 'name'

This happens when you iterate over response.content without checking the block type first. The response can contain both TextBlock and ToolUseBlock objects. Always filter with block.type == "tool_use" before accessing block.name, exactly as shown in Step 4.

Error 3: Infinite loop in the agent

# The script runs forever and never prints the report

This usually means stop_reason never reaches "end_turn" because tool results aren't being sent back correctly. Make sure your tool result message uses "type": "tool_result" and includes the correct tool_use_id from the original tool_use_block.id. A mismatched ID causes Claude to wait indefinitely.

Next Steps

  • Connect to a CRM: After scoring, use the HubSpot or Salesforce API to automatically create or update a contact record with the qualification data.
  • Add a web interface: Wrap the agent in a FastAPI endpoint so your sales team can paste lead notes into a form and get instant scores.
  • Add memory across leads: Store historical scores in a SQLite database to track lead quality trends over time and adjust scoring weights.
  • Extend the tools: Add a third tool that generates a personalized follow-up email draft based on the extracted pain points and classification.