What You'll Build
You're going to build a fully functional AI lead qualifier agent in Python that uses Claude's tool-calling API to score and evaluate inbound leads automatically. The agent reads raw lead data, calls enrichment tools to validate and score each field, then returns a structured qualification decision with a confidence score and next-step recommendation. By the end of this tutorial, you'll have code you can run against 100+ leads in a single script without touching a single record manually.
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
anthropicPython SDK installed (pip install anthropic) - Optional but helpful: some familiarity with JSON and REST APIs
Step 1: Set Up Claude API and Python Environment
Start by creating a fresh project folder and installing the Anthropic SDK. I always use a virtual environment to keep dependencies clean, especially when a project like this might eventually get more tools added to it.
terminal
# Create and activate a virtual environment
python -m venv lead-qualifier-env
source lead-qualifier-env/bin/activate # On Windows: lead-qualifier-env\Scripts\activate
# Install the Anthropic SDK
pip install anthropic
# Verify the install
python -c "import anthropic; print(anthropic.__version__)"
Next, store your API key as an environment variable. Never hardcode it in your script. This habit will save you from accidentally pushing credentials to GitHub.
terminal
# Add this to your shell profile (~/.zshrc, ~/.bashrc, or ~/.bash_profile)
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
# Reload your shell or run:
source ~/.zshrc
Now create your project file. We'll keep everything in one script for this tutorial, then talk about splitting it up at the end.
lead_qualifier.py
import anthropic
import json
import os
from typing import Any
# Initialize the Anthropic client using the environment variable automatically
client = anthropic.Anthropic()
print("Anthropic client initialized successfully.")
ANTHROPIC_API_KEY environment variable. You don't need to pass it explicitly when you call anthropic.Anthropic(). This keeps your code clean and portable.
Step 2: Define the Lead Qualifier Agent with Tool Use
This is the core of the whole system. We're building a Python class that wraps the Claude API call and holds the agent's system prompt, model config, and conversation loop. The system prompt is where you define the agent's personality, decision criteria, and output format.
I spent a lot of time tuning the system prompt here. The key insight is that you want Claude to reason through qualification criteria in a specific order, not just pattern-match on keywords. That's what makes this feel like a real SDR, not a simple scoring script.
lead_qualifier.py
import anthropic
import json
import os
from typing import Any
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are an expert B2B lead qualification agent for a software agency.
Your job is to evaluate inbound leads and determine whether they are a good fit for outreach.
You have access to tools that let you validate email addresses, estimate company size from
domain data, and score individual lead fields. Use these tools before making your final
qualification decision.
Always follow this reasoning order:
1. Validate the email domain using the validate_email_domain tool.
2. Look up company firmographic data using the get_company_data tool.
3. Score the lead's budget and timeline signals using the score_lead_signals tool.
4. Return a structured JSON qualification report.
Your final output must be valid JSON in this exact format:
{
"qualified": true or false,
"score": a number from 0 to 100,
"tier": "hot", "warm", or "cold",
"reason": "one sentence explaining the decision",
"recommended_action": "Call within 24 hours" or "Add to nurture sequence" or "Disqualify"
}
Do not include any text outside of this JSON in your final response."""
class LeadQualifierAgent:
def __init__(self):
self.client = anthropic.Anthropic()
self.model = "claude-sonnet-4-6"
self.system_prompt = SYSTEM_PROMPT
self.tools = self._define_tools()
def _define_tools(self) -> list[dict]:
"""Return the list of tool definitions the agent can call."""
return [] # We'll fill this in Step 3
def qualify_lead(self, lead: dict) -> dict:
"""Run the full qualification loop for a single lead."""
pass # We'll implement this in Step 4
Notice that we're using claude-sonnet-4-6 as the model. This is the right balance of speed and reasoning quality for a task like lead scoring where you need structured output but also want to process a high volume of records affordably.
Step 3: Create Tool Definitions for Data Enrichment
Tool definitions tell Claude what functions it can call, what parameters each function needs, and what the function does. Claude decides on its own when to call a tool and what arguments to pass. Your Python code then executes the actual function and feeds the result back into the conversation.
We're defining three tools: email domain validation, company data lookup, and lead signal scoring. In a real production system, these would call external APIs like Clearbit, Hunter.io, or your own CRM. For this tutorial, I'm using realistic mock logic so you can see the full flow without needing API keys for a dozen services.
lead_qualifier.py
import anthropic
import json
import os
import re
from typing import Any
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are an expert B2B lead qualification agent for a software agency.
Your job is to evaluate inbound leads and determine whether they are a good fit for outreach.
You have access to tools that let you validate email addresses, estimate company size from
domain data, and score individual lead fields. Use these tools before making your final
qualification decision.
Always follow this reasoning order:
1. Validate the email domain using the validate_email_domain tool.
2. Look up company firmographic data using the get_company_data tool.
3. Score the lead's budget and timeline signals using the score_lead_signals tool.
4. Return a structured JSON qualification report.
Your final output must be valid JSON in this exact format:
{
"qualified": true or false,
"score": a number from 0 to 100,
"tier": "hot", "warm", or "cold",
"reason": "one sentence explaining the decision",
"recommended_action": "Call within 24 hours" or "Add to nurture sequence" or "Disqualify"
}
Do not include any text outside of this JSON in your final response."""
# --- Tool Execution Functions ---
def validate_email_domain(email: str) -> dict:
"""
Validates an email address and checks whether the domain is a business domain.
Returns validation status and domain type.
"""
# Basic format check using regex
pattern = r"^[\w\.\+\-]+@([\w\-]+\.)+[\w\-]{2,}$"
is_valid_format = bool(re.match(pattern, email))
# Flag free/personal email providers as lower quality leads
free_providers = {
"gmail.com", "yahoo.com", "hotmail.com",
"outlook.com", "aol.com", "icloud.com"
}
domain = email.split("@")[-1].lower() if "@" in email else ""
is_business_email = domain not in free_providers and domain != ""
return {
"email": email,
"is_valid_format": is_valid_format,
"domain": domain,
"is_business_email": is_business_email,
"domain_quality": "business" if is_business_email else "personal"
}
def get_company_data(domain: str) -> dict:
"""
Simulates a firmographic data lookup for a company domain.
In production, replace this with a real API call to Clearbit, Apollo, or similar.
"""
# Simulated data based on domain patterns for demo purposes
mock_data = {
"naplesgrouphomes.com": {
"company_name": "Naples Group Homes",
"industry": "Real Estate",
"estimated_employees": 45,
"estimated_annual_revenue": "$5M - $10M",
"location": "Naples, FL",
"technologies": ["WordPress", "HubSpot"]
},
"swflmanufacturing.com": {
"company_name": "SWFL Manufacturing Co.",
"industry": "Manufacturing",
"estimated_employees": 220,
"estimated_annual_revenue": "$25M - $50M",
"location": "Fort Myers, FL",
"technologies": ["SAP", "Salesforce"]
},
"default": {
"company_name": "Unknown Company",
"industry": "Unknown",
"estimated_employees": 10,
"estimated_annual_revenue": "Under $1M",
"location": "Unknown",
"technologies": []
}
}
return mock_data.get(domain, mock_data["default"])
def score_lead_signals(budget: str, timeline: str, pain_point: str) -> dict:
"""
Scores the qualitative signals in a lead form submission.
Returns a numeric score and reasoning for each signal.
"""
score = 0
breakdown = {}
# Score budget signal
budget_lower = budget.lower()
if any(term in budget_lower for term in ["$10k", "$20k", "$50k", "10,000", "20,000", "50,000"]):
score += 40
breakdown["budget"] = {"value": budget, "points": 40, "note": "High budget signal"}
elif any(term in budget_lower for term in ["$5k", "5,000", "few thousand"]):
score += 20
breakdown["budget"] = {"value": budget, "points": 20, "note": "Medium budget signal"}
else:
score += 5
breakdown["budget"] = {"value": budget, "points": 5, "note": "Low or unclear budget signal"}
# Score timeline signal
timeline_lower = timeline.lower()
if any(term in timeline_lower for term in ["asap", "immediately", "this month", "this quarter"]):
score += 35
breakdown["timeline"] = {"value": timeline, "points": 35, "note": "Urgent timeline"}
elif any(term in timeline_lower for term in ["next quarter", "3 months", "6 months"]):
score += 20
breakdown["timeline"] = {"value": timeline, "points": 20, "note": "Near-term timeline"}
else:
score += 5
breakdown["timeline"] = {"value": timeline, "points": 5, "note": "Vague or long-term timeline"}
# Score pain point specificity
if len(pain_point) > 100:
score += 25
breakdown["pain_point"] = {"points": 25, "note": "Detailed pain point description"}
elif len(pain_point) > 40:
score += 15
breakdown["pain_point"] = {"points": 15, "note": "Moderate detail in pain point"}
else:
score += 5
breakdown["pain_point"] = {"points": 5, "note": "Vague or short pain point description"}
return {"total_signal_score": score, "breakdown": breakdown}
# --- Tool Dispatcher ---
def execute_tool(tool_name: str, tool_input: dict) -> Any:
"""Routes a tool call from Claude to the correct Python function."""
if tool_name == "validate_email_domain":
return validate_email_domain(tool_input["email"])
elif tool_name == "get_company_data":
return get_company_data(tool_input["domain"])
elif tool_name == "score_lead_signals":
return score_lead_signals(
tool_input["budget"],
tool_input["timeline"],
tool_input["pain_point"]
)
else:
return {"error": f"Unknown tool: {tool_name}"}
class LeadQualifierAgent:
def __init__(self):
self.client = anthropic.Anthropic()
self.model = "claude-sonnet-4-6"
self.system_prompt = SYSTEM_PROMPT
self.tools = self._define_tools()
def _define_tools(self) -> list[dict]:
"""Return the tool definitions Claude uses to understand what it can call."""
return [
{
"name": "validate_email_domain",
"description": (
"Validates an email address format and checks if it is a business "
"email or a free personal email provider. Returns domain quality signal."
),
"input_schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "The full email address to validate."
}
},
"required": ["email"]
}
},
{
"name": "get_company_data",
"description": (
"Looks up firmographic data for a company based on its domain. "
"Returns industry, estimated employee count, revenue range, and tech stack."
),
"input_schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The company's website domain, e.g. 'acme.com'."
}
},
"required": ["domain"]
}
},
{
"name": "score_lead_signals",
"description": (
"Scores the qualitative signals in a lead's form submission including "
"budget, timeline urgency, and specificity of their pain point."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {
"type": "string",
"description": "The lead's stated budget or budget range."
},
"timeline": {
"type": "string",
"description": "The lead's stated timeline for starting the project."
},
"pain_point": {
"type": "string",
"description": "The lead's description of the problem they want to solve."
}
},
"required": ["budget", "timeline", "pain_point"]
}
}
]
def qualify_lead(self, lead: dict) -> dict:
"""Run the full qualification loop for a single lead."""
pass # Implemented in Step 4
Step 4: Build the Evaluation Loop
The evaluation loop is the heart of any tool-calling agent. Claude returns a response, you check if it wants to call a tool, you execute that tool, then you feed the result back and ask Claude to continue. You repeat this until Claude stops requesting tools and gives you a final answer.
This loop pattern is the same regardless of what the agent is doing. Once you understand it here, you can apply it to any agentic task you build with the Anthropic SDK.
lead_qualifier.py
import anthropic
import json
import os
import re
from typing import Any
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are an expert B2B lead qualification agent for a software agency.
Your job is to evaluate inbound leads and determine whether they are a good fit for outreach.
You have access to tools that let you validate email addresses, estimate company size from
domain data, and score individual lead fields. Use these tools before making your final
qualification decision.
Always follow this reasoning order:
1. Validate the email domain using the validate_email_domain tool.
2. Look up company firmographic data using the get_company_data tool.
3. Score the lead's budget and timeline signals using the score_lead_signals tool.
4. Return a structured JSON qualification report.
Your final output must be valid JSON in this exact format:
{
"qualified": true or false,
"score": a number from 0 to 100,
"tier": "hot", "warm", or "cold",
"reason": "one sentence explaining the decision",
"recommended_action": "Call within 24 hours" or "Add to nurture sequence" or "Disqualify"
}
Do not include any text outside of this JSON in your final response."""
def validate_email_domain(email: str) -> dict:
pattern = r"^[\w\.\+\-]+@([\w\-]+\.)+[\w\-]{2,}$"
is_valid_format = bool(re.match(pattern, email))
free_providers = {
"gmail.com", "yahoo.com", "hotmail.com",
"outlook.com", "aol.com", "icloud.com"
}
domain = email.split("@")[-1].lower() if "@" in email else ""
is_business_email = domain not in free_providers and domain != ""
return {
"email": email,
"is_valid_format": is_valid_format,
"domain": domain,
"is_business_email": is_business_email,
"domain_quality": "business" if is_business_email else "personal"
}
def get_company_data(domain: str) -> dict:
mock_data = {
"naplesgrouphomes.com": {
"company_name": "Naples Group Homes",
"industry": "Real Estate",
"estimated_employees": 45,
"estimated_annual_revenue": "$5M - $10M",
"location": "Naples, FL",
"technologies": ["WordPress", "HubSpot"]
},
"swflmanufacturing.com": {
"company_name": "SWFL Manufacturing Co.",
"industry": "Manufacturing",
"estimated_employees": 220,
"estimated_annual_revenue": "$25M - $50M",
"location": "Fort Myers, FL",
"technologies": ["SAP", "Salesforce"]
},
"default": {
"company_name": "Unknown Company",
"industry": "Unknown",
"estimated_employees": 10,
"estimated_annual_revenue": "Under $1M",
"location": "Unknown",
"technologies": []
}
}
return mock_data.get(domain, mock_data["default"])
def score_lead_signals(budget: str, timeline: str, pain_point: str) -> dict:
score = 0
breakdown = {}
budget_lower = budget.lower()
if any(term in budget_lower for term in ["$10k", "$20k", "$50k", "10,000", "20,000", "50,000"]):
score += 40
breakdown["budget"] = {"value": budget, "points": 40, "note": "High budget signal"}
elif any(term in budget_lower for term in ["$5k", "5,000", "few thousand"]):
score += 20
breakdown["budget"] = {"value": budget, "points": 20, "note": "Medium budget signal"}
else:
score += 5
breakdown["budget"] = {"value": budget, "points": 5, "note": "Low or unclear budget signal"}
timeline_lower = timeline.lower()
if any(term in timeline_lower for term in ["asap", "immediately", "this month", "this quarter"]):
score += 35
breakdown["timeline"] = {"value": timeline, "points": 35, "note": "Urgent timeline"}
elif any(term in timeline_lower for term in ["next quarter", "3 months", "6 months"]):
score += 20
breakdown["timeline"] = {"value": timeline, "points": 20, "note": "Near-term timeline"}
else:
score += 5
breakdown["timeline"] = {"value": timeline, "points": 5, "note": "Vague or long-term timeline"}
if len(pain_point) > 100:
score += 25
breakdown["pain_point"] = {"points": 25, "note": "Detailed pain point description"}
elif len(pain_point) > 40:
score += 15
breakdown["pain_point"] = {"points": 15, "note": "Moderate detail in pain point"}
else:
score += 5
breakdown["pain_point"] = {"points": 5, "note": "Vague or short pain point description"}
return {"total_signal_score": score, "breakdown": breakdown}
def execute_tool(tool_name: str, tool_input: dict) -> Any:
if tool_name == "validate_email_domain":
return validate_email_domain(tool_input["email
Naples AI