What You'll Build
If you've ever watched your sales team spend hours chasing leads that never convert, this tutorial is for you. You're going to build a fully working AI lead qualifier agent in Python that uses Claude's API to score, enrich, and triage inbound leads automatically.
By the end, you'll have a production-ready agent class with tool-calling, an agentic run loop, and a scoring system that spits out qualified vs. unqualified decisions with reasoning. This is exactly the kind of automation we build for clients at Naples AI — and now you can build it yourself.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and dictionaries
pipavailable in your terminal- A text editor or IDE (VS Code works great)
All the code you need is included step-by-step below. Each snippet builds on the last, and the final section shows you the complete assembled agent. Copy each block in order and you'll have a working lead qualifier by the end of this tutorial.
Step 1: Set Up Your Python Environment and Anthropic SDK
First, let's get your environment clean and your dependencies installed. I always recommend a virtual environment so nothing bleeds into your global Python install.
Open your terminal and run these commands:
terminal
# Create and activate a virtual environment
python -m venv lead-agent-env
source lead-agent-env/bin/activate # On Windows: lead-agent-env\Scripts\activate
# Install the Anthropic SDK
pip install anthropic python-dotenv
Now create a .env file in your project root and add your API key:
ANTHROPIC_API_KEY=your_api_key_here
Never hardcode your API key directly in Python files. The python-dotenv package loads it from .env automatically, keeping your credentials out of version control.
Step 2: Define Lead Qualification Tools and Scoring Criteria
Claude's tool-use feature lets the model call functions you define, then act on the results. This is what makes it an "agent" rather than just a chatbot. We're going to define three tools: one to score a lead, one to enrich it with context, and one to make a final qualification decision.
Create a file called tools.py:
import json
from typing import Any
# Tool definitions in the format Claude's API expects
LEAD_TOOLS = [
{
"name": "score_lead",
"description": (
"Scores a lead based on budget, timeline, company size, and intent signals. "
"Returns a numeric score from 0 to 100 and a tier label."
),
"input_schema": {
"type": "object",
"properties": {
"budget_usd": {
"type": "number",
"description": "Estimated or stated budget in USD. Use 0 if unknown."
},
"timeline_days": {
"type": "number",
"description": "How soon they need the solution, in days. Use 365 if unknown."
},
"company_size": {
"type": "string",
"enum": ["solo", "small", "medium", "enterprise"],
"description": "Size of the prospect's company."
},
"intent_level": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "How strong the buying intent signals are."
}
},
"required": ["budget_usd", "timeline_days", "company_size", "intent_level"]
}
},
{
"name": "enrich_lead",
"description": (
"Enriches a lead record with industry fit, estimated deal value, "
"and recommended follow-up action based on available data."
),
"input_schema": {
"type": "object",
"properties": {
"industry": {
"type": "string",
"description": "The prospect's industry (e.g., real estate, healthcare, restaurant)."
},
"pain_point": {
"type": "string",
"description": "The main problem or challenge they described."
},
"lead_source": {
"type": "string",
"description": "Where this lead came from (e.g., website, referral, cold outreach)."
}
},
"required": ["industry", "pain_point", "lead_source"]
}
},
{
"name": "qualify_lead",
"description": (
"Makes a final binary qualification decision and assigns a priority routing. "
"Call this after scoring and enrichment."
),
"input_schema": {
"type": "object",
"properties": {
"score": {
"type": "number",
"description": "The numeric score returned by score_lead."
},
"industry_fit": {
"type": "string",
"enum": ["poor", "neutral", "good", "excellent"],
"description": "How well the prospect matches our ideal customer profile."
},
"reasoning": {
"type": "string",
"description": "One or two sentences explaining the qualification decision."
}
},
"required": ["score", "industry_fit", "reasoning"]
}
}
]
def execute_tool(tool_name: str, tool_input: dict[str, Any]) -> str:
"""Routes tool calls to their handler functions and returns JSON string results."""
if tool_name == "score_lead":
return json.dumps(_handle_score_lead(tool_input))
elif tool_name == "enrich_lead":
return json.dumps(_handle_enrich_lead(tool_input))
elif tool_name == "qualify_lead":
return json.dumps(_handle_qualify_lead(tool_input))
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
def _handle_score_lead(inputs: dict) -> dict:
"""Calculates a lead score from 0-100 based on four weighted factors."""
score = 0
# Budget scoring — worth up to 35 points
budget = inputs.get("budget_usd", 0)
if budget >= 10000:
score += 35
elif budget >= 5000:
score += 25
elif budget >= 1000:
score += 15
elif budget > 0:
score += 5
# Timeline scoring — worth up to 25 points
timeline = inputs.get("timeline_days", 365)
if timeline <= 30:
score += 25
elif timeline <= 90:
score += 18
elif timeline <= 180:
score += 10
else:
score += 3
# Company size scoring — worth up to 20 points
size_map = {"enterprise": 20, "medium": 15, "small": 10, "solo": 5}
score += size_map.get(inputs.get("company_size", "solo"), 5)
# Intent scoring — worth up to 20 points
intent_map = {"high": 20, "medium": 12, "low": 4}
score += intent_map.get(inputs.get("intent_level", "low"), 4)
# Assign a tier label based on final score
if score >= 70:
tier = "hot"
elif score >= 45:
tier = "warm"
else:
tier = "cold"
return {"score": score, "tier": tier, "max_possible": 100}
def _handle_enrich_lead(inputs: dict) -> dict:
"""Maps industry and pain point to estimated deal value and recommended action."""
# Industries we work with most — real values from Naples AI client base
high_fit_industries = {
"real estate", "healthcare", "restaurant",
"car dealership", "manufacturing", "hospitality"
}
industry = inputs.get("industry", "").lower()
industry_fit = "excellent" if industry in high_fit_industries else "neutral"
# Estimate deal value based on lead source quality
source_value_map = {
"referral": 8500,
"website": 5000,
"cold outreach": 3000,
"social media": 2500,
"event": 6000
}
lead_source = inputs.get("lead_source", "website").lower()
estimated_value = source_value_map.get(lead_source, 4000)
# Map pain points to recommended first action
pain_point = inputs.get("pain_point", "").lower()
if "automat" in pain_point:
action = "Schedule automation discovery call"
elif "chatbot" in pain_point or "assistant" in pain_point:
action = "Send chatbot demo video and book a demo"
elif "data" in pain_point or "analytic" in pain_point:
action = "Share predictive analytics case study"
elif "seo" in pain_point or "content" in pain_point:
action = "Offer free AI content audit"
else:
action = "Schedule general discovery call"
return {
"industry_fit": industry_fit,
"estimated_deal_value_usd": estimated_value,
"recommended_action": action,
"industry_detected": industry
}
def _handle_qualify_lead(inputs: dict) -> dict:
"""Makes the final yes/no qualification decision based on score and fit."""
score = inputs.get("score", 0)
industry_fit = inputs.get("industry_fit", "neutral")
reasoning = inputs.get("reasoning", "")
# Qualification threshold: score >= 45 OR excellent industry fit with score >= 35
fit_bonus = industry_fit in ("good", "excellent")
qualified = score >= 45 or (fit_bonus and score >= 35)
priority = "high" if score >= 70 else ("medium" if qualified else "low")
return {
"qualified": qualified,
"priority": priority,
"reasoning": reasoning,
"score_used": score,
"fit_used": industry_fit
}
Breaking qualification into discrete steps (score → enrich → decide) gives Claude structured checkpoints to reason through. It also makes each tool testable in isolation — a pattern that saves a lot of debugging time in production agents.
Step 3: Build the Main Agent Class with Claude API Integration
Now let's build the LeadQualifierAgent class. This is the core of the whole system — it holds the Claude client, manages conversation state, and knows how to kick off a qualification run for any lead you hand it.
Create a file called agent.py:
import os
import json
from typing import Any
import anthropic
from dotenv import load_dotenv
from tools import LEAD_TOOLS, execute_tool
load_dotenv()
class LeadQualifierAgent:
"""
An agentic lead qualifier that uses Claude to score, enrich,
and make qualification decisions via tool-use.
"""
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 cap to prevent infinite loops
self.system_prompt = """You are an expert B2B sales qualification agent for Naples AI,
a custom AI solutions agency in Southwest Florida. Your job is to evaluate inbound leads
and determine whether they are worth pursuing.
For every lead you receive, you MUST call these tools in order:
1. score_lead — to calculate a numeric score based on budget, timeline, company size, and intent
2. enrich_lead — to assess industry fit and determine the best follow-up action
3. qualify_lead — to make the final binary qualified/unqualified decision
Always reason step by step. After all three tools have been called, provide a concise
summary of the lead with your recommendation. Be direct and specific."""
def qualify(self, lead_data: dict[str, Any]) -> dict[str, Any]:
"""
Runs the full qualification pipeline for a single lead.
Returns a dict with the final qualification result and agent reasoning.
"""
# Format the lead into a clean prompt
lead_summary = self._format_lead_prompt(lead_data)
messages = [
{"role": "user", "content": lead_summary}
]
print(f"\n{'='*60}")
print(f"Processing lead: {lead_data.get('name', 'Unknown')}")
print(f"{'='*60}")
# Run the agentic loop — defined in the next step
result = self._run_agent_loop(messages)
return result
def _format_lead_prompt(self, lead: dict) -> str:
"""Turns a lead dictionary into a natural language prompt for Claude."""
return f"""Please qualify the following inbound lead:
Name: {lead.get('name', 'Not provided')}
Company: {lead.get('company', 'Not provided')}
Industry: {lead.get('industry', 'Not provided')}
Budget: ${lead.get('budget_usd', 0):,.0f}
Timeline: {lead.get('timeline_days', 365)} days
Company Size: {lead.get('company_size', 'small')}
Intent Level: {lead.get('intent_level', 'medium')}
Pain Point: {lead.get('pain_point', 'Not provided')}
Lead Source: {lead.get('lead_source', 'website')}
Notes: {lead.get('notes', 'None')}
Please score, enrich, and qualify this lead using the available tools."""
def _run_agent_loop(self, messages: list) -> dict[str, Any]:
"""
The main agentic loop. Keeps calling Claude until it stops requesting tools
or we hit the max iteration limit.
"""
iteration = 0
final_result = {
"qualified": False,
"priority": "low",
"score": 0,
"reasoning": "",
"recommended_action": "",
"agent_summary": "",
"tool_calls_made": []
}
while iteration < self.max_iterations:
iteration += 1
print(f"\n[Iteration {iteration}] Calling Claude...")
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=LEAD_TOOLS,
messages=messages
)
print(f"[Stop reason: {response.stop_reason}]")
# If Claude is done — no more tool calls — extract the final text
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
final_result["agent_summary"] = block.text
print(f"\n[Agent Final Summary]\n{block.text}")
break
# If Claude wants to use tools, process each tool call
if response.stop_reason == "tool_use":
# Add Claude's response (with tool use requests) to message history
messages.append({
"role": "assistant",
"content": response.content
})
# Process every tool call in this response
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
tool_use_id = block.id
print(f"[Tool call] {tool_name}({json.dumps(tool_input, indent=2)})")
# Execute the tool and capture the result
tool_output = execute_tool(tool_name, tool_input)
parsed_output = json.loads(tool_output)
print(f"[Tool result] {json.dumps(parsed_output, indent=2)}")
# Track what tools were called for our return value
final_result["tool_calls_made"].append({
"tool": tool_name,
"input": tool_input,
"output": parsed_output
})
# Pull key values out of tool results as they come in
if tool_name == "score_lead":
final_result["score"] = parsed_output.get("score", 0)
elif tool_name == "enrich_lead":
final_result["recommended_action"] = parsed_output.get(
"recommended_action", ""
)
elif tool_name == "qualify_lead":
final_result["qualified"] = parsed_output.get("qualified", False)
final_result["priority"] = parsed_output.get("priority", "low")
final_result["reasoning"] = parsed_output.get("reasoning", "")
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": tool_output
})
# Send all tool results back to Claude in a single user message
messages.append({
"role": "user",
"content": tool_results
})
else:
# Unexpected stop reason — break to avoid infinite loop
print(f"[Warning] Unexpected stop reason: {response.stop_reason}")
break
return final_result
Step 4: Implement the Run Loop to Process Leads in Real-Time
Now let's wire it all together with a main script that processes a batch of leads and prints a clean summary. This is what you'd eventually replace with a webhook handler, CRM integration, or scheduled job.
Create main.py:
import json
from agent import LeadQualifierAgent
def run_lead_qualification_batch(leads: list[dict]) -> list[dict]:
"""Processes a list of leads through the qualifier and returns all results."""
agent = LeadQualifierAgent()
results = []
for lead in leads:
result = agent.qualify(lead)
results.append({
"lead_name": lead.get("name"),
"company": lead.get("company"),
"qualified": result["qualified"],
"priority": result["priority"],
"score": result["score"],
"reasoning": result["reasoning"],
"recommended_action": result["recommended_action"],
"agent_summary": result["agent_summary"]
})
return results
def print_qualification_report(results: list[dict]) -> None:
"""Prints a formatted summary table of all qualification results."""
print("\n" + "="*70)
print("LEAD QUALIFICATION REPORT")
print("="*70)
qualified_leads = [r for r in results if r["qualified"]]
unqualified_leads = [r for r in results if not r["qualified"]]
print(f"\n✅ QUALIFIED LEADS ({len(qualified_leads)})")
print("-"*70)
for lead in qualified_leads:
print(f" Name: {lead['lead_name']} @ {lead['company']}")
print(f" Score: {lead['score']}/100 | Priority: {lead['priority'].upper()}")
print(f" Action: {lead['recommended_action']}")
print(f" Reason: {lead['reasoning']}")
print()
print(f"\n❌ UNQUALIFIED LEADS ({len(unqualified_leads)})")
print("-"*70)
for lead in unqualified_leads:
print(f" Name: {lead['lead_name']} @ {lead['company']}")
print(f" Score: {lead['score']}/100 | Priority: {lead['priority'].upper()}")
print(f" Reason: {lead['reasoning']}")
print()
print("="*70)
if __name__ == "__main__":
# Sample leads — mix of qualified and unqualified
sample_leads = [
{
"name": "Maria Gonzalez",
"company": "Coastal Realty Group",
"industry": "real estate",
"budget_usd": 12000,
"timeline_days": 45,
"company_size": "medium",
"intent_level": "high",
"pain_point": "We need to automate listing descriptions and lead follow-up emails",
"lead_source": "referral",
"notes": "Ready to move forward this quarter, has executive buy-in"
},
{
"name": "Derek Poole",
"company": "Gulf Coast Burgers",
"industry": "restaurant",
"budget_usd": 800,
"timeline_days": 300,
"company_size": "small",
"intent_level": "low",
"pain_point": "Just curious about AI chatbots for our website",
"lead_source": "social media",
"notes": "Just exploring, no budget approved yet"
},
{
"name": "Sandra Kim",
"company": "Naples Orthopedic Partners",
"industry": "healthcare",
"budget_usd": 25000,
"timeline_days": 60,
"company_size": "medium",
"intent_level": "high",
"pain_point": "Patient intake automation and analytics dashboard",