What You'll Build
If you've ever wasted time chasing leads that were never going to close, this tutorial is for you. You're going to build an autonomous AI lead qualifier agent in Python using the Claude API — one that scores incoming leads, evaluates fit, and returns a structured qualification report without you lifting a finger.
By the end, you'll have a fully working agent that accepts lead data, runs it through a scoring tool chain, and outputs a lead score, qualification tier, and recommended next action. The whole thing runs in under 50 lines of core logic.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python functions and dictionaries
anthropicSDK installed:pip install anthropic- A
.envfile or environment variable set forANTHROPIC_API_KEY
Step 1: Set Up Your Claude API Client and Environment
First, let's get the Anthropic client wired up and your environment variables loaded. This is the foundation everything else sits on.
I always use python-dotenv for local development so I'm not hardcoding keys anywhere. Install it with pip install python-dotenv if you don't have it yet.
import os
import json
import anthropic
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Initialize the Anthropic client
client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY")
)
MODEL = "claude-sonnet-4-5"
# Quick sanity check — prints "Client ready" if the key loaded correctly
if client.api_key:
print("Client ready.")
else:
raise ValueError("ANTHROPIC_API_KEY not found. Check your .env file.")
Your .env file should look like this:
ANTHROPIC_API_KEY=sk-ant-your-key-here
.gitignore immediately. This is the most common mistake I see from developers new to API projects.
Step 2: Define Lead Qualification Tools
This is where the agent gets its actual capabilities. Claude's tool use feature lets you define functions the model can call when it needs to perform a specific task — in our case, scoring a lead.
We're defining three tools: one to score budget fit, one to score company size fit, and one to generate the final qualification summary. Claude decides which tools to call and in what order based on the lead data you hand it.
tools.py
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# Tool definitions follow the Anthropic tool_use schema exactly
tools = [
{
"name": "score_budget_fit",
"description": (
"Evaluates whether the lead's stated budget aligns with your service pricing. "
"Returns a numeric score from 0 to 100 and a short rationale."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {
"type": "number",
"description": "The lead's stated monthly or project budget in USD."
},
"minimum_engagement": {
"type": "number",
"description": "Your agency's minimum engagement cost in USD."
}
},
"required": ["budget", "minimum_engagement"]
}
},
{
"name": "score_company_size_fit",
"description": (
"Evaluates whether the lead's company size (number of employees) "
"fits your ideal customer profile. Returns a score from 0 to 100."
),
"input_schema": {
"type": "object",
"properties": {
"employee_count": {
"type": "integer",
"description": "Number of employees at the lead's company."
},
"ideal_min": {
"type": "integer",
"description": "Minimum employee count in your ideal customer profile."
},
"ideal_max": {
"type": "integer",
"description": "Maximum employee count in your ideal customer profile."
}
},
"required": ["employee_count", "ideal_min", "ideal_max"]
}
},
{
"name": "generate_qualification_summary",
"description": (
"Takes individual scores and lead metadata, then produces a final "
"qualification tier (Hot, Warm, Cold), composite score, and recommended next action."
),
"input_schema": {
"type": "object",
"properties": {
"lead_name": {
"type": "string",
"description": "Full name of the lead contact."
},
"company": {
"type": "string",
"description": "Name of the lead's company."
},
"budget_score": {
"type": "number",
"description": "Budget fit score from 0 to 100."
},
"size_score": {
"type": "number",
"description": "Company size fit score from 0 to 100."
},
"industry": {
"type": "string",
"description": "Industry the lead operates in."
},
"pain_point": {
"type": "string",
"description": "The primary problem or need the lead described."
}
},
"required": [
"lead_name", "company", "budget_score",
"size_score", "industry", "pain_point"
]
}
}
]
These tool definitions are just Python dictionaries following Anthropic's schema. The input_schema block tells Claude exactly what parameters each function expects so it can fill them in from the lead data you provide.
Step 3: Create the Lead Qualifier Agent Loop
Now we build the agent class itself. The core of any Claude tool-use agent is the loop: send a message, check if Claude wants to call a tool, execute that tool, send the result back, repeat until Claude gives you a final text response.
This pattern is sometimes called an "agentic loop" and it's the same pattern we use across all the agent systems we build at Naples AI.
agent_loop.py
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# -- Tool definitions (same as tools.py above, included here for completeness) --
tools = [
{
"name": "score_budget_fit",
"description": (
"Evaluates whether the lead's stated budget aligns with your service pricing. "
"Returns a numeric score from 0 to 100 and a short rationale."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {"type": "number", "description": "Lead's budget in USD."},
"minimum_engagement": {"type": "number", "description": "Your minimum engagement cost."}
},
"required": ["budget", "minimum_engagement"]
}
},
{
"name": "score_company_size_fit",
"description": (
"Evaluates company size fit against ideal customer profile. "
"Returns a score from 0 to 100."
),
"input_schema": {
"type": "object",
"properties": {
"employee_count": {"type": "integer", "description": "Number of employees."},
"ideal_min": {"type": "integer", "description": "Min employees in ICP."},
"ideal_max": {"type": "integer", "description": "Max employees in ICP."}
},
"required": ["employee_count", "ideal_min", "ideal_max"]
}
},
{
"name": "generate_qualification_summary",
"description": "Produces final qualification tier, composite score, and next action.",
"input_schema": {
"type": "object",
"properties": {
"lead_name": {"type": "string", "description": "Lead contact name."},
"company": {"type": "string", "description": "Company name."},
"budget_score": {"type": "number", "description": "Budget fit score 0-100."},
"size_score": {"type": "number", "description": "Size fit score 0-100."},
"industry": {"type": "string", "description": "Lead's industry."},
"pain_point": {"type": "string", "description": "Primary pain point described."}
},
"required": ["lead_name", "company", "budget_score", "size_score", "industry", "pain_point"]
}
}
]
class LeadQualifierAgent:
"""
Autonomous agent that qualifies sales leads using Claude's tool-use API.
Runs a full agentic loop until a final qualification report is produced.
"""
def __init__(self, client, model, tools):
self.client = client
self.model = model
self.tools = tools
self.messages = []
def run(self, lead_data: dict) -> str:
"""
Accepts a lead dictionary, runs the qualification loop,
and returns the final qualification report as a string.
"""
# Build the initial prompt from lead data
user_prompt = self._build_prompt(lead_data)
self.messages = [{"role": "user", "content": user_prompt}]
print("\n--- Starting Lead Qualification Agent ---\n")
# Agentic loop: keep going until Claude stops requesting tool calls
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
tools=self.tools,
messages=self.messages
)
# If Claude is done and returning a final answer, break the loop
if response.stop_reason == "end_turn":
final_text = self._extract_text(response)
print("--- Qualification Complete ---\n")
return final_text
# If Claude wants to use tools, process all tool calls in this response
if response.stop_reason == "tool_use":
# Append Claude's response (including tool_use blocks) to message history
self.messages.append({"role": "assistant", "content": response.content})
# Build tool results for every tool Claude called
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"→ Calling tool: {block.name}")
result = self._execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Send all tool results back to Claude in a single user message
self.messages.append({"role": "user", "content": tool_results})
def _build_prompt(self, lead: dict) -> str:
return (
f"Please qualify this sales lead for our AI agency.\n\n"
f"Lead Name: {lead.get('name')}\n"
f"Company: {lead.get('company')}\n"
f"Industry: {lead.get('industry')}\n"
f"Employees: {lead.get('employees')}\n"
f"Monthly Budget (USD): {lead.get('budget')}\n"
f"Pain Point: {lead.get('pain_point')}\n\n"
f"Our minimum engagement is $3,000/month. Our ideal client has 10–200 employees. "
f"Use the available tools to score this lead and generate a full qualification summary."
)
def _execute_tool(self, tool_name: str, tool_input: dict) -> dict:
"""Routes tool calls to the correct Python function."""
if tool_name == "score_budget_fit":
return score_budget_fit(**tool_input)
elif tool_name == "score_company_size_fit":
return score_company_size_fit(**tool_input)
elif tool_name == "generate_qualification_summary":
return generate_qualification_summary(**tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}
def _extract_text(self, response) -> str:
"""Pulls plain text out of Claude's final response."""
for block in response.content:
if hasattr(block, "text"):
return block.text
return "No text response returned."
Step 4: Implement Tool Execution and Scoring Logic
The tools defined above are just schemas — Claude knows what to ask for, but your Python code actually does the work. Here's the implementation of all three scoring functions, plus the full runnable example with sample output.
The scoring math here is deliberately simple so you can follow the logic. In production, you'd swap these for CRM lookups, weighted rubrics, or even ML model predictions.
lead_qualifier_agent.py
import os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# ─────────────────────────────────────────
# TOOL IMPLEMENTATIONS
# ─────────────────────────────────────────
def score_budget_fit(budget: float, minimum_engagement: float) -> dict:
"""
Scores how well the lead's budget fits our minimum engagement.
A budget at or above minimum scores 100. Below minimum scales down proportionally.
"""
if budget >= minimum_engagement:
score = 100
rationale = f"Budget of ${budget:,.0f} meets or exceeds minimum of ${minimum_engagement:,.0f}."
else:
# Proportional score — e.g. $1,500 budget vs $3,000 min = 50
score = round((budget / minimum_engagement) * 100)
rationale = (
f"Budget of ${budget:,.0f} is below minimum of ${minimum_engagement:,.0f}. "
f"May require upsell conversation."
)
return {"score": score, "rationale": rationale}
def score_company_size_fit(employee_count: int, ideal_min: int, ideal_max: int) -> dict:
"""
Scores company size fit against our ICP range.
Inside range = 100. Outside range penalized by distance.
"""
if ideal_min <= employee_count <= ideal_max:
score = 100
note = f"Company size of {employee_count} employees is within ideal range ({ideal_min}–{ideal_max})."
elif employee_count < ideal_min:
# Too small — score drops the further below minimum
ratio = employee_count / ideal_min
score = round(ratio * 100)
note = f"Company is smaller than ideal ({employee_count} vs min {ideal_min}). May lack budget authority."
else:
# Too large — enterprise prospects often need longer sales cycles
overage = employee_count - ideal_max
score = max(0, 100 - (overage // 10))
note = f"Company exceeds ideal size range ({employee_count} vs max {ideal_max}). Longer sales cycle likely."
return {"score": score, "note": note}
def generate_qualification_summary(
lead_name: str,
company: str,
budget_score: float,
size_score: float,
industry: str,
pain_point: str
) -> dict:
"""
Aggregates individual scores into a final qualification report.
Tier logic: Hot >= 80, Warm >= 50, Cold < 50.
"""
composite_score = round((budget_score + size_score) / 2)
if composite_score >= 80:
tier = "Hot"
next_action = "Book a discovery call within 24 hours."
elif composite_score >= 50:
tier = "Warm"
next_action = "Send a personalized case study and follow up in 3 days."
else:
tier = "Cold"
next_action = "Add to nurture sequence. Revisit in 30 days."
return {
"lead_name": lead_name,
"company": company,
"industry": industry,
"pain_point": pain_point,
"budget_score": budget_score,
"size_score": size_score,
"composite_score": composite_score,
"qualification_tier": tier,
"recommended_action": next_action
}
# ─────────────────────────────────────────
# TOOL DEFINITIONS (Anthropic schema)
# ─────────────────────────────────────────
tools = [
{
"name": "score_budget_fit",
"description": (
"Evaluates whether the lead's stated budget aligns with your service pricing. "
"Returns a numeric score from 0 to 100 and a short rationale."
),
"input_schema": {
"type": "object",
"properties": {
"budget": {"type": "number", "description": "Lead's budget in USD."},
"minimum_engagement": {"type": "number", "description": "Your minimum engagement cost in USD."}
},
"required": ["budget", "minimum_engagement"]
}
},
{
"name": "score_company_size_fit",
"description": (
"Evaluates company size fit against ideal customer profile. "
"Returns a score from 0 to 100."
),
"input_schema": {
"type": "object",
"properties": {
"employee_count": {"type": "integer", "description": "Number of employees."},
"ideal_min": {"type": "integer", "description": "Min employees in your ICP."},
"ideal_max": {"type": "integer", "description": "Max employees in your ICP."}
},
"required": ["employee_count", "ideal_min", "ideal_max"]
}
},
{
"name": "generate_qualification_summary",
"description": "Produces final qualification tier, composite score, and recommended next action.",
"input_schema": {
"type": "object",
"properties": {
"lead_name": {"type": "string", "description": "Lead contact name."},
"company": {"type": "string", "description": "Company name."},
"budget_score": {"type": "number", "description": "Budget fit score 0-100."},
"size_score": {"type": "number", "description": "Size fit score 0-100."},
"industry": {"type": "string", "description": "Lead's industry."},
"pain_point": {"type": "string", "description": "Primary pain point described."}
},
"required": ["lead_name", "company", "budget_score", "size_score", "industry", "pain_point"]
}
}
]
# ─────────────────────────────────────────
# AGENT CLASS
# ─────────────────────────────────────────
class LeadQualifierAgent:
def __init__(self, client, model, tools):
self.client = client
self.model = model
self.tools = tools
self.messages = []
def run(self, lead_data: dict) -> str:
user_prompt = self._build_prompt(lead_data)
self.messages = [{"role": "user", "content": user_prompt}]
print("\n--- Starting Lead Qualification Agent ---\n")
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
tools=self.tools,
messages=self.messages
)
if response.stop_reason == "end_turn":
print("--- Qualification Complete ---\n")
return self._extract_text(response)
if response.stop_reason == "tool_use":
self.messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"→ Calling tool: {block.name}")
result = self._execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
self.messages.append({"role": "user", "content": tool_results})
def _build_prompt(self, lead: dict) -> str:
return (
f"Please qualify this sales lead for our AI agency.\n\n"