← Back to Blog

What You'll Build

If you're tired of your sales team manually sorting through leads and guessing who to call first, this tutorial is exactly what you've been looking for. You'll build a fully working AI lead qualifier agent in Python that uses Claude's tool use feature to score leads on quality, fit, and urgency — then output a prioritized recommendation with reasoning. By the end, you'll have a production-ready agent you can plug into any CRM or lead intake form.

📦 Full Source Code
The complete, working code for this lead qualifier agent is broken into steps below. Each step builds on the last, so I'd recommend reading through once before copy-pasting. The final agent handles multi-turn conversation, tool execution, and structured lead scoring output — all in under 200 lines of Python.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • anthropic Python SDK installed (pip install anthropic)
  • Basic familiarity with Python classes and functions
  • Optional: a .env file with ANTHROPIC_API_KEY=your_key_here

Step 1: Initialize the Claude Client and Define Lead Scoring Tools

Claude's tool use feature lets the model call predefined functions during a conversation. We're going to define three scoring tools: one for lead quality, one for product fit, and one for urgency. Claude will decide when to call each one based on the lead information you pass in.

Here's the foundation of the agent — the client setup and tool definitions.

lead_qualifier.py
import os
import json
import anthropic
from typing import Any

# Initialize the Anthropic client — pulls key from environment variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

# Tool definitions tell Claude what functions it can call and what inputs they expect
LEAD_SCORING_TOOLS = [
    {
        "name": "score_lead_quality",
        "description": (
            "Scores the overall quality of a lead based on provided information "
            "such as budget, company size, decision-making authority, and data completeness. "
            "Returns a score from 0-100."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "budget_confirmed": {
                    "type": "boolean",
                    "description": "Whether the lead has confirmed a budget"
                },
                "decision_maker": {
                    "type": "boolean",
                    "description": "Whether the contact is a decision maker or influencer"
                },
                "company_size": {
                    "type": "string",
                    "enum": ["solo", "small", "mid-market", "enterprise"],
                    "description": "Size category of the lead's company"
                },
                "contact_info_complete": {
                    "type": "boolean",
                    "description": "Whether name, email, and phone are all provided"
                }
            },
            "required": ["budget_confirmed", "decision_maker", "company_size", "contact_info_complete"]
        }
    },
    {
        "name": "score_product_fit",
        "description": (
            "Evaluates how well the lead's needs align with our product or service offering. "
            "Returns a fit score from 0-100 and a fit category."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "industry": {
                    "type": "string",
                    "description": "The lead's industry or business type"
                },
                "pain_points_match": {
                    "type": "boolean",
                    "description": "Whether the lead's pain points match what we solve"
                },
                "use_case_clear": {
                    "type": "boolean",
                    "description": "Whether there is a clear use case for our solution"
                },
                "competitor_mentioned": {
                    "type": "boolean",
                    "description": "Whether the lead is currently using a competitor's product"
                }
            },
            "required": ["industry", "pain_points_match", "use_case_clear", "competitor_mentioned"]
        }
    },
    {
        "name": "score_urgency",
        "description": (
            "Measures how urgently the lead needs a solution. "
            "Factors in timeline, stated urgency, and trigger events. "
            "Returns an urgency score from 0-100."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "timeline_days": {
                    "type": "integer",
                    "description": "How many days until the lead needs a solution (use 999 if unknown)"
                },
                "stated_urgency": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "critical"],
                    "description": "The urgency level the lead expressed directly"
                },
                "trigger_event": {
                    "type": "boolean",
                    "description": "Whether there is a triggering event driving the need (e.g. new hire, product launch)"
                }
            },
            "required": ["timeline_days", "stated_urgency", "trigger_event"]
        }
    }
]

Notice I'm not doing anything fancy here — just a clean list of tool definitions with strict schemas. The input_schema for each tool tells Claude exactly what fields it needs to populate before calling the function. This is what keeps the model grounded instead of hallucinating values.

Step 2: Create the Lead Qualifier Agent with Tool Use

Now we write the actual scoring functions that execute when Claude calls a tool. These are plain Python functions — no magic. Claude picks which one to call; your code runs it and returns the result.

lead_qualifier.py (continued)
def score_lead_quality(
    budget_confirmed: bool,
    decision_maker: bool,
    company_size: str,
    contact_info_complete: bool
) -> dict:
    """Calculate a lead quality score from 0-100."""
    score = 0

    # Budget confirmation is the strongest quality signal
    if budget_confirmed:
        score += 35
    if decision_maker:
        score += 30

    size_scores = {"solo": 5, "small": 15, "mid-market": 25, "enterprise": 20}
    score += size_scores.get(company_size, 0)

    if contact_info_complete:
        score += 10

    grade = "A" if score >= 80 else "B" if score >= 60 else "C" if score >= 40 else "D"
    return {"quality_score": score, "grade": grade}


def score_product_fit(
    industry: str,
    pain_points_match: bool,
    use_case_clear: bool,
    competitor_mentioned: bool
) -> dict:
    """Evaluate product-market fit for this specific lead."""
    score = 0

    # Strong pain point alignment is the core of fit scoring
    if pain_points_match:
        score += 45
    if use_case_clear:
        score += 30

    # Competitor usage shows they're already buying in the category
    if competitor_mentioned:
        score += 15

    fit_label = "Excellent" if score >= 80 else "Good" if score >= 55 else "Fair" if score >= 30 else "Poor"
    return {
        "fit_score": score,
        "fit_label": fit_label,
        "industry": industry
    }


def score_urgency(
    timeline_days: int,
    stated_urgency: str,
    trigger_event: bool
) -> dict:
    """Determine how urgent this lead's need is."""
    score = 0

    urgency_scores = {"low": 10, "medium": 30, "high": 55, "critical": 70}
    score += urgency_scores.get(stated_urgency, 10)

    if trigger_event:
        score += 20

    # Shorter timelines push urgency up
    if timeline_days <= 7:
        score += 10
    elif timeline_days <= 30:
        score += 5

    score = min(score, 100)  # Cap at 100
    priority = "Immediate" if score >= 75 else "This Week" if score >= 50 else "This Month" if score >= 25 else "Nurture"
    return {"urgency_score": score, "follow_up_priority": priority}


# Maps tool names to their actual Python functions
TOOL_FUNCTIONS = {
    "score_lead_quality": score_lead_quality,
    "score_product_fit": score_product_fit,
    "score_urgency": score_urgency
}


def execute_tool(tool_name: str, tool_input: dict) -> Any:
    """Look up and run the correct scoring function."""
    if tool_name not in TOOL_FUNCTIONS:
        return {"error": f"Unknown tool: {tool_name}"}
    return TOOL_FUNCTIONS[tool_name](**tool_input)


class LeadQualifierAgent:
    """An AI agent that qualifies sales leads using Claude and structured scoring tools."""

    def __init__(self):
        self.client = client
        self.model = MODEL
        self.tools = LEAD_SCORING_TOOLS

    def qualify_lead(self, lead_data: str) -> dict:
        """
        Run the full qualification process for a single lead.
        Returns scores, reasoning, and a recommended next action.
        """
        system_prompt = (
            "You are an expert sales qualification agent. "
            "When given lead information, you MUST call all three scoring tools — "
            "score_lead_quality, score_product_fit, and score_urgency — before forming your final recommendation. "
            "After running all three tools, provide a structured summary with: "
            "overall score (average of the three), key strengths, key risks, and a recommended next action. "
            "Be direct and specific."
        )

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

        # Kick off the agentic loop — this runs until Claude stops calling tools
        return self._run_agent_loop(system_prompt, messages)

The LeadQualifierAgent class keeps everything tidy. The qualify_lead method is the only public interface you need — pass in a string describing the lead, and it handles everything else internally.

Step 3: Build the Agent Loop with Multi-Turn Conversation

This is the part most tutorials skip over. Claude doesn't just call one tool and stop — it can call multiple tools across multiple turns before it's confident enough to give you a final answer. The agent loop handles exactly that.

The loop keeps running until Claude returns a stop_reason of "end_turn", which means it's done using tools and is ready to give us the final response.

lead_qualifier.py (continued)
    def _run_agent_loop(self, system_prompt: str, messages: list) -> dict:
        """
        Manages the multi-turn conversation between Claude and our scoring tools.
        Keeps looping until Claude finishes all tool calls and returns a final answer.
        """
        tool_results_log = []  # Track all tool calls and their results

        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 using tools, extract and return the final text
            if response.stop_reason == "end_turn":
                final_text = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        final_text = block.text
                        break
                return {
                    "recommendation": final_text,
                    "tool_results": tool_results_log,
                    "input_tokens": response.usage.input_tokens,
                    "output_tokens": response.usage.output_tokens
                }

            # Claude wants to call tools — process each tool use block
            if response.stop_reason == "tool_use":
                # Append Claude's response (including tool_use blocks) to conversation
                messages.append({"role": "assistant", "content": response.content})

                # Build a list of tool results to send back in one user turn
                tool_result_blocks = []

                for block in response.content:
                    if block.type == "tool_use":
                        tool_result = execute_tool(block.name, block.input)
                        tool_results_log.append({
                            "tool": block.name,
                            "input": block.input,
                            "result": tool_result
                        })

                        tool_result_blocks.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": json.dumps(tool_result)
                        })

                # Return all tool results in a single user message
                messages.append({
                    "role": "user",
                    "content": tool_result_blocks
                })

            else:
                # Unexpected stop reason — bail out gracefully
                break

        return {"recommendation": "Agent loop ended unexpectedly.", "tool_results": tool_results_log}
⚠️ Important: Notice how I append Claude's full response content (including tool_use blocks) back into the messages list before adding tool results. If you skip that step, Claude loses track of what it asked for and the API throws a validation error.

Step 4: Test with Sample Real Estate and Sales Leads

Let's run the agent against two realistic leads — one from real estate and one from a B2B software sale. This is what I used to validate the agent before we deployed a version of this for a Naples-area real estate team.

test_leads.py
import json
from lead_qualifier import LeadQualifierAgent

agent = LeadQualifierAgent()

# --- Lead 1: Real Estate Buyer ---
real_estate_lead = """
Lead Name: Maria Gonzalez
Industry: Real Estate (Home Buyer)
Contact Info: Email + Phone provided — complete
Budget: $650,000 confirmed, pre-approved mortgage in hand
Decision Maker: Yes — sole buyer, no co-signer involved
Company Size: N/A (individual buyer) — classify as 'solo'
Pain Points: Relocating from Miami to Naples within 45 days for a new job.
             Frustrated with previous agent who went silent. Wants proactive communication.
Use Case: Looking for a 3BR waterfront property in North Naples or Pelican Bay.
          Our team specializes in exactly this market.
Urgency: Must close within 45 days or loses job relocation package.
Competitor: Was working with a competing agency but dropped them last week.
Trigger Event: Yes — job start date is firm.
Stated Urgency: High
"""

print("=" * 60)
print("QUALIFYING LEAD: Maria Gonzalez (Real Estate)")
print("=" * 60)
result_1 = agent.qualify_lead(real_estate_lead)

print("\n--- Tool Calls Made ---")
for tr in result_1["tool_results"]:
    print(f"\nTool: {tr['tool']}")
    print(f"Input: {json.dumps(tr['input'], indent=2)}")
    print(f"Result: {json.dumps(tr['result'], indent=2)}")

print("\n--- Agent Recommendation ---")
print(result_1["recommendation"])
print(f"\nTokens used — Input: {result_1['input_tokens']} | Output: {result_1['output_tokens']}")

# --- Lead 2: B2B SaaS Sales Lead ---
saas_lead = """
Lead Name: Derek Hoffman
Title: Office Manager
Company: Gulf Coast Dental Group (12 locations)
Industry: Healthcare / Dental
Contact Info: Email only — no phone number
Budget: Unconfirmed. Said "we have some budget" but no number given.
Decision Maker: No — needs approval from the regional VP
Company Size: Mid-market
Pain Points: Spending too much time on manual patient intake forms and follow-up emails.
             Looking for automation but hasn't evaluated any tools yet.
Use Case: Potentially matches our AI process automation service.
Urgency: No hard deadline. "Would love to have something by Q4."
Competitor: Not currently using any automation tool — doing everything manually.
Trigger Event: No
Stated Urgency: Low
"""

print("\n" + "=" * 60)
print("QUALIFYING LEAD: Derek Hoffman (Healthcare B2B)")
print("=" * 60)
result_2 = agent.qualify_lead(saas_lead)

print("\n--- Tool Calls Made ---")
for tr in result_2["tool_results"]:
    print(f"\nTool: {tr['tool']}")
    print(f"Input: {json.dumps(tr['input'], indent=2)}")
    print(f"Result: {json.dumps(tr['result'], indent=2)}")

print("\n--- Agent Recommendation ---")
print(result_2["recommendation"])

Run it with python test_leads.py and here's what you'll see:

Sample Output
============================================================
QUALIFYING LEAD: Maria Gonzalez (Real Estate)
============================================================

--- Tool Calls Made ---

Tool: score_lead_quality
Input: {
  "budget_confirmed": true,
  "decision_maker": true,
  "company_size": "solo",
  "contact_info_complete": true
}
Result: {
  "quality_score": 80,
  "grade": "A"
}

Tool: score_product_fit
Input: {
  "industry": "Real Estate (Home Buyer)",
  "pain_points_match": true,
  "use_case_clear": true,
  "competitor_mentioned": true
}
Result: {
  "fit_score": 90,
  "fit_label": "Excellent",
  "industry": "Real Estate (Home Buyer)"
}

Tool: score_urgency
Input: {
  "timeline_days": 45,
  "stated_urgency": "high",
  "trigger_event": true
}
Result: {
  "urgency_score": 80,
  "follow_up_priority": "Immediate"
}

--- Agent Recommendation ---
## Lead Qualification Summary: Maria Gonzalez

**Overall Score: 83/100 — Grade A**

| Dimension     | Score | Rating    |
|---------------|-------|-----------|
| Lead Quality  | 80    | A         |
| Product Fit   | 90    | Excellent |
| Urgency       | 80    | Immediate |

**Key Strengths:**
- Pre-approved budget of $650K is confirmed — no financial uncertainty
- Clear 45-day hard deadline driven by a job relocation package creates genuine urgency
- Already dropped a competitor, meaning she's actively choosing a new agent right now
- Use case (North Naples waterfront) aligns precisely with your market specialty

**Key Risks:**
- Solo buyer means one person drives the entire deal — if priorities shift, there's no backup contact
- Relocation stress may slow decision-making despite the urgency

**Recommended Next Action:**
Call within the next 2 hours. Lead this with two things: your specific North Naples waterfront
listings in the $600K–$700K range, and a clear communication SLA so she knows you won't
go silent like the last agent. This lead should close if you move today.

Tokens used — Input: 1847 | Output: 412

============================================================
QUALIFYING LEAD: Derek Hoffman (Healthcare B2B)
============================================================

--- Tool Calls Made ---

Tool: score_lead_quality
Input: {
  "budget_confirmed": false,
  "decision_maker": false,
  "company_size": "mid-market",
  "contact_info_complete": false
}
Result: {
  "quality_score": 25,
  "grade": "D"
}

Tool: score_product_fit
Input: {
  "industry": "Healthcare / Dental",
  "pain_points_match": true,
  "use_case_clear": true,
  "competitor_mentioned": false
}
Result: {
  "fit_score": 75,
  "fit_label": "Good",
  "industry": "Healthcare / Dental"
}

Tool: score_urgency
Input: {
  "timeline_days": 999,
  "stated_urgency": "low",
  "trigger_event": false
}
Result: {
  "urgency_score": 10,
  "follow_up_priority": "Nurture"
}

--- Agent Recommendation ---
## Lead Qualification Summary: Derek Hoffman

**Overall Score: 37/100 — Grade C**

| Dimension     | Score | Rating    |
|---------------|-------|-----------|
| Lead Quality  | 25    | D         |
| Product Fit   | 75    | Good      |
| Urgency       | 10    | Nurture   |

**Key Strengths:**
- Dental group with 12 locations is a meaningful account size if it converts
- Pain points (manual intake, follow-up emails) map directly to what you automate
- No competitor entrenched yet — you'd be the first solution they adopt

**Key Risks:**
- Derek is not the decision maker — the regional VP needs to be involved before any real progress
- Budget is completely unconfirmed — "some budget" is not a buying signal
- No urgency driver whatsoever; this lead could stall for quarters

**Recommended Next Action:**
Add to a 30-day email nurture sequence. Your priority is to get Derek to schedule an
intro call with the regional VP present. Send one piece of content this week about
AI automation ROI in multi-location dental practices — something concrete enough
to give him ammunition to bring to his VP. Do not prioritize this over active deals.

How It Works

Here's the plain-English version of what's happening under