← Back to Blog

What You'll Build

If you've ever watched a sales team manually sort through dozens of leads trying to figure out who to call first, you already know the problem. By the end of this tutorial, you'll have a fully working AI lead qualifier agent in Python that uses Claude's tool-use API to automatically score, rank, and prioritize inbound leads — no human sorting required.

The agent pulls lead data, runs it through a scoring function, looks up CRM history, and returns a ranked list with qualification notes. You'll go from zero to a production-ready agent loop in about 30 minutes.

📦 Full Source Code
The complete, working code for this project is broken into numbered steps below. Each step builds on the last, so by Step 6 you'll have the entire agent assembled. Copy each block in order and you'll have a running lead qualifier by the end.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and functions
  • anthropic Python SDK installed (pip install anthropic)
  • A .env file or environment variable set for ANTHROPIC_API_KEY

Step 1: Set Up Claude API and Anthropic SDK

First, install the SDK and make sure your API key is available. I keep mine in a .env file so I'm not hardcoding secrets anywhere near version control.

terminal
pip install anthropic python-dotenv

Now create your project directory and a .env file:

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Then verify the SDK connects before you write a single line of agent code. This quick smoke test saves you debugging time later.

test_connection.py
import os
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=64,
    messages=[{"role": "user", "content": "Say: API connection successful."}]
)

print(response.content[0].text)

If you see API connection successful. in your terminal, you're good to move on. If you get an auth error, double-check that your .env file is in the same directory you're running the script from.

Step 2: Define the Qualification Tools

Claude's tool-use feature lets you give the model a set of functions it can call during reasoning. You define the tools in JSON schema format, and Claude decides when and how to call them. Think of it like giving the model a toolbox — it picks the right tool based on what it needs to do next.

For our lead qualifier, we need three tools: one to score a lead, one to look up CRM history, and one to rank a batch of leads. Here's the full tool definition block.

tools.py
import os
import json
import random
from dotenv import load_dotenv
import anthropic

load_dotenv()

# Tool schemas that Claude will use to understand what functions are available
LEAD_TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Scores a single lead based on company size, budget, urgency, and "
            "fit with our AI services. Returns a numeric score from 0 to 100 "
            "and a qualification tier: Hot, Warm, or Cold."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead."
                },
                "company_name": {
                    "type": "string",
                    "description": "Name of the company or individual."
                },
                "industry": {
                    "type": "string",
                    "description": "Industry vertical, e.g. real estate, healthcare, restaurant."
                },
                "employee_count": {
                    "type": "integer",
                    "description": "Approximate number of employees."
                },
                "annual_revenue_usd": {
                    "type": "integer",
                    "description": "Estimated annual revenue in USD."
                },
                "budget_range": {
                    "type": "string",
                    "description": "Stated budget range, e.g. under_5k, 5k_to_20k, over_20k."
                },
                "urgency": {
                    "type": "string",
                    "description": "How urgently they need a solution: low, medium, high."
                },
                "interested_services": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of services the lead is interested in."
                }
            },
            "required": [
                "lead_id", "company_name", "industry",
                "employee_count", "annual_revenue_usd",
                "budget_range", "urgency", "interested_services"
            ]
        }
    },
    {
        "name": "lookup_crm_data",
        "description": (
            "Retrieves historical CRM data for a lead, including past interactions, "
            "open deals, and previous purchase history. Returns a summary dict."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead to look up."
                }
            },
            "required": ["lead_id"]
        }
    },
    {
        "name": "rank_leads",
        "description": (
            "Takes a list of scored leads and returns them sorted from highest "
            "to lowest score, with a recommended outreach priority for each."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "scored_leads": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "lead_id": {"type": "string"},
                            "score": {"type": "number"},
                            "tier": {"type": "string"}
                        },
                        "required": ["lead_id", "score", "tier"]
                    },
                    "description": "Array of leads that have already been scored."
                }
            },
            "required": ["scored_leads"]
        }
    }
]
💡 Tip: Tool descriptions matter more than you think
Claude reads the description field to decide when to use a tool. Write them like you're explaining the function to a junior developer — be specific about what it returns and when to use it.

Step 3: Implement the Tool Handler Functions

Now we write the actual Python functions that get called when Claude triggers a tool. These are your real business logic — in production, lookup_crm_data would hit your actual CRM API. For this tutorial, I'm using realistic mock data so the whole thing runs without external dependencies.

tool_handlers.py
import json

# Simulated CRM database — replace with real API calls in production
MOCK_CRM_DB = {
    "lead_001": {
        "past_interactions": 3,
        "last_contact_days_ago": 14,
        "previous_purchases": [],
        "open_deals": 1,
        "notes": "Attended AI webinar. Very interested in chatbot automation."
    },
    "lead_002": {
        "past_interactions": 0,
        "last_contact_days_ago": None,
        "previous_purchases": ["SEO Content Package"],
        "open_deals": 0,
        "notes": "Existing customer. Exploring AI chatbot upgrade."
    },
    "lead_003": {
        "past_interactions": 1,
        "last_contact_days_ago": 60,
        "previous_purchases": [],
        "open_deals": 0,
        "notes": "Cold inquiry. No follow-up yet."
    }
}


def score_lead(
    lead_id: str,
    company_name: str,
    industry: str,
    employee_count: int,
    annual_revenue_usd: int,
    budget_range: str,
    urgency: str,
    interested_services: list
) -> dict:
    """Score a lead from 0-100 based on fit and intent signals."""

    score = 0

    # Budget scoring — higher budget = higher score
    budget_scores = {"under_5k": 10, "5k_to_20k": 25, "over_20k": 40}
    score += budget_scores.get(budget_range, 0)

    # Urgency scoring
    urgency_scores = {"low": 5, "medium": 15, "high": 25}
    score += urgency_scores.get(urgency, 0)

    # Industry fit — these are the verticals Naples AI specializes in
    high_fit_industries = {"real estate", "healthcare", "restaurant", "car dealership", "manufacturing"}
    if industry.lower() in high_fit_industries:
        score += 20
    else:
        score += 8

    # Company size bonus — bigger teams have more automation ROI
    if employee_count >= 50:
        score += 10
    elif employee_count >= 10:
        score += 5

    # Revenue bonus
    if annual_revenue_usd >= 1_000_000:
        score += 5

    # Cap score at 100
    score = min(score, 100)

    # Assign qualification tier
    if score >= 70:
        tier = "Hot"
    elif score >= 40:
        tier = "Warm"
    else:
        tier = "Cold"

    return {
        "lead_id": lead_id,
        "company_name": company_name,
        "score": score,
        "tier": tier,
        "industry": industry,
        "budget_range": budget_range,
        "urgency": urgency,
        "interested_services": interested_services
    }


def lookup_crm_data(lead_id: str) -> dict:
    """Fetch CRM history for a lead. Returns empty record if not found."""

    record = MOCK_CRM_DB.get(lead_id, {
        "past_interactions": 0,
        "last_contact_days_ago": None,
        "previous_purchases": [],
        "open_deals": 0,
        "notes": "No prior history found."
    })

    return {"lead_id": lead_id, "crm_data": record}


def rank_leads(scored_leads: list) -> dict:
    """Sort scored leads by score descending and assign outreach priority."""

    sorted_leads = sorted(scored_leads, key=lambda x: x["score"], reverse=True)

    priority_labels = {0: "Priority 1", 1: "Priority 2", 2: "Priority 3"}

    ranked = []
    for i, lead in enumerate(sorted_leads):
        ranked.append({
            "rank": i + 1,
            "lead_id": lead["lead_id"],
            "score": lead["score"],
            "tier": lead["tier"],
            "outreach_priority": priority_labels.get(i, f"Priority {i + 1}")
        })

    return {"ranked_leads": ranked}


def execute_tool(tool_name: str, tool_input: dict) -> str:
    """Route Claude's tool call to the correct Python function."""

    if tool_name == "score_lead":
        result = score_lead(**tool_input)
    elif tool_name == "lookup_crm_data":
        result = lookup_crm_data(**tool_input)
    elif tool_name == "rank_leads":
        result = rank_leads(**tool_input)
    else:
        result = {"error": f"Unknown tool: {tool_name}"}

    # Claude expects tool results as a JSON string
    return json.dumps(result)

Step 4: Build the Agent Loop with Tool Use

This is where it all comes together. The agent loop is the core pattern for any Claude tool-use agent — you send a message, check if Claude wants to use a tool, execute the tool, send the result back, and repeat until Claude gives you a final text response.

I've wrapped this in a class so it's easy to reuse across different lead batches. The loop handles multi-step reasoning, meaning Claude can call multiple tools in sequence before delivering its final assessment.

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

# Import our tools and handlers
from tools import LEAD_TOOLS
from tool_handlers import execute_tool

load_dotenv()


class LeadQualifierAgent:
    """
    An agentic lead qualifier that uses Claude's tool-use API to score,
    enrich, and rank inbound leads automatically.
    """

    def __init__(self):
        self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"
        self.max_iterations = 10  # Safety limit to prevent infinite loops

    def qualify_leads(self, leads: list) -> str:
        """
        Main entry point. Pass a list of lead dicts and get back
        a prioritized assessment from Claude.
        """

        # Build the initial prompt with all leads as structured context
        leads_json = json.dumps(leads, indent=2)

        system_prompt = (
            "You are an expert AI sales qualifier for a Southwest Florida AI agency "
            "called Naples AI. Your job is to evaluate inbound leads and determine "
            "which ones are the best fit for custom AI solutions. "
            "Use the available tools to score each lead, look up their CRM history, "
            "and then rank all leads from highest to lowest priority. "
            "After ranking, provide a concise written summary of your top recommendations "
            "and why each lead is or isn't a strong fit."
        )

        user_message = (
            f"Please qualify and rank the following inbound leads. "
            f"Score each one using score_lead, look up their CRM data using lookup_crm_data, "
            f"then rank them all using rank_leads. Finally, give me your written assessment.\n\n"
            f"Leads:\n{leads_json}"
        )

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

        # Agentic loop — Claude may call multiple tools before finishing
        for iteration in range(self.max_iterations):
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=system_prompt,
                tools=LEAD_TOOLS,
                messages=messages
            )

            # If Claude is done reasoning and has a final answer, return it
            if response.stop_reason == "end_turn":
                final_text = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        final_text += block.text
                return final_text

            # If Claude wants to use tools, execute them and send results back
            if response.stop_reason == "tool_use":
                # Add Claude's response (which includes tool_use blocks) to history
                messages.append({"role": "assistant", "content": response.content})

                # Build tool results to send back in the next turn
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        print(f"  → Claude calling tool: {block.name}")
                        print(f"    Input: {json.dumps(block.input, indent=4)}")

                        # Execute the actual tool function
                        result = execute_tool(block.name, block.input)

                        print(f"    Result: {result}\n")

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

                # Send tool results back to Claude as the next user message
                messages.append({"role": "user", "content": tool_results})

            else:
                # Unexpected stop reason — break to avoid hanging
                print(f"Unexpected stop_reason: {response.stop_reason}")
                break

        return "Agent loop completed without a final response. Try increasing max_iterations."
⚠️ Always set a max_iterations limit
Without an iteration cap, a buggy tool or unexpected Claude behavior can cause your agent to loop indefinitely and rack up API costs. Ten iterations is generous for most lead qualification tasks — I've never seen it need more than four in testing.

Step 5: Implement Lead Assessment Logic and Ranking

Now let's wire it all up with a realistic lead dataset and run the agent. This is what you'd actually hand to the agent in a real sales workflow — leads pulled from your web form, CRM intake, or ad platform.

run_qualifier.py
from lead_qualifier_agent import LeadQualifierAgent

# Sample inbound leads — in production these come from your CRM or form submissions
sample_leads = [
    {
        "lead_id": "lead_001",
        "company_name": "Gulf Coast Realty Group",
        "industry": "real estate",
        "employee_count": 32,
        "annual_revenue_usd": 4_200_000,
        "budget_range": "5k_to_20k",
        "urgency": "high",
        "interested_services": ["AI chatbot", "real estate listing automation"]
    },
    {
        "lead_id": "lead_002",
        "company_name": "Bella Napoli Restaurant",
        "industry": "restaurant",
        "employee_count": 18,
        "annual_revenue_usd": 950_000,
        "budget_range": "under_5k",
        "urgency": "medium",
        "interested_services": ["AI chatbot", "intelligent process automation"]
    },
    {
        "lead_id": "lead_003",
        "company_name": "Suncoast Auto Dealers",
        "industry": "car dealership",
        "employee_count": 75,
        "annual_revenue_usd": 12_000_000,
        "budget_range": "over_20k",
        "urgency": "high",
        "interested_services": [
            "predictive analytics",
            "AI chatbot",
            "custom AI development"
        ]
    }
]

if __name__ == "__main__":
    print("=" * 60)
    print("Naples AI — Lead Qualifier Agent")
    print("=" * 60)
    print()

    agent = LeadQualifierAgent()

    print("Running qualification agent...\n")
    result = agent.qualify_leads(sample_leads)

    print("\n" + "=" * 60)
    print("AGENT FINAL ASSESSMENT")
    print("=" * 60)
    print(result)

Here's what the output actually looks like when you run this. The tool call logs print as they happen so you can see exactly what Claude is doing at each step.

sample output
============================================================
Naples AI — Lead Qualifier Agent
============================================================

Running qualification agent...

  → Claude calling tool: score_lead
    Input: {
        "lead_id": "lead_001",
        "company_name": "Gulf Coast Realty Group",
        "industry": "real estate",
        "employee_count": 32,
        "annual_revenue_usd": 4200000,
        "budget_range": "5k_to_20k",
        "urgency": "high",
        "interested_services": ["AI chatbot", "real estate listing automation"]
    }
    Result: {"lead_id": "lead_001", "company_name": "Gulf Coast Realty Group",
             "score": 85, "tier": "Hot", "industry": "real estate",
             "budget_range": "5k_to_20k", "urgency": "high",
             "interested_services": ["AI chatbot", "real estate listing automation"]}

  → Claude calling tool: lookup_crm_data
    Input: {"lead_id": "lead_001"}
    Result: {"lead_id": "lead_001", "crm_data": {"past_interactions": 3,
             "last_contact_days_ago": 14, "previous_purchases": [],
             "open_deals": 1, "notes": "Attended AI webinar. Very interested in chatbot automation."}}

  → Claude calling tool: score_lead
    Input: {
        "lead_id": "lead_002",
        "company_name": "Bella Napoli Restaurant",
        "industry": "restaurant",
        "employee_count": 18,
        "annual_revenue_usd": 950000,
        "budget_range": "under_5k",
        "urgency": "medium",
        "interested_services": ["AI chatbot", "intelligent process automation"]
    }
    Result: {"lead_id": "lead_002", "score": 50, "tier": "Warm", ...}

  → Claude calling tool: