Building a Lead Qualification Chat Agent: Collecting and Scoring User Information
Build a complete lead qualification chat agent with structured question flows, conditional branching logic, real-time lead scoring models, and CRM integration to convert website visitors into qualified sales opportunities.
From Traffic to Pipeline
Most B2B websites convert less than 3% of visitors into leads. A well-built lead qualification chat agent changes that equation by engaging visitors in real time, asking the right questions, scoring their fit, and routing qualified leads to sales — all without human intervention. The best qualification agents feel like a helpful conversation, not a form to fill out.
The architecture has three layers: a question flow engine that guides the conversation, a scoring model that evaluates fit in real time, and a CRM integration that delivers qualified leads to the right salesperson.
The Lead Data Model
Start by defining what a qualified lead looks like:
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
class LeadScore(str, Enum):
HOT = "hot" # 80-100 points, route to sales immediately
WARM = "warm" # 50-79 points, nurture sequence
COLD = "cold" # 0-49 points, marketing list
class LeadData(BaseModel):
session_id: str
name: str | None = None
email: str | None = None
company: str | None = None
company_size: str | None = None
role: str | None = None
budget: str | None = None
timeline: str | None = None
use_case: str | None = None
pain_points: list[str] = Field(default_factory=list)
score: int = 0
grade: LeadScore = LeadScore.COLD
source_page: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow)
def calculate_score(self) -> int:
score = 0
# Company size scoring
size_scores = {
"1-10": 5, "11-50": 10, "51-200": 20,
"201-1000": 30, "1000+": 40
}
score += size_scores.get(self.company_size or "", 0)
# Role scoring - decision makers score higher
role_keywords = {
"cto": 25, "vp": 25, "director": 20, "head": 20,
"manager": 15, "engineer": 5, "developer": 5,
}
if self.role:
role_lower = self.role.lower()
for keyword, points in role_keywords.items():
if keyword in role_lower:
score += points
break
# Budget scoring
budget_scores = {
"under_1k": 5, "1k_5k": 15, "5k_20k": 25,
"20k_50k": 35, "50k_plus": 40
}
score += budget_scores.get(self.budget or "", 0)
# Timeline scoring - urgency increases score
timeline_scores = {
"immediate": 20, "this_month": 15,
"this_quarter": 10, "exploring": 5
}
score += timeline_scores.get(self.timeline or "", 0)
self.score = min(score, 100)
if self.score >= 80:
self.grade = LeadScore.HOT
elif self.score >= 50:
self.grade = LeadScore.WARM
else:
self.grade = LeadScore.COLD
return self.score
Conversational Question Flow
Use a state machine to guide the conversation while keeping it natural. The agent asks questions in a logical sequence but adapts based on previous answers:
from dataclasses import dataclass, field
@dataclass
class QualificationStep:
field_name: str
question: str
follow_ups: dict[str, str] = field(default_factory=dict)
skip_if: str | None = None
QUALIFICATION_FLOW = [
QualificationStep(
field_name="use_case",
question="What brings you to our site today? I'd love to understand what you're looking for.",
),
QualificationStep(
field_name="company",
question="Which company are you with? That will help me tailor my recommendations.",
),
QualificationStep(
field_name="company_size",
question="How large is your team?",
),
QualificationStep(
field_name="role",
question="And what's your role there?",
),
QualificationStep(
field_name="timeline",
question="What's your timeline for getting started?",
follow_ups={
"immediate": "Great, sounds like this is a priority. Let me make sure I connect you with the right person.",
"exploring": "No rush at all. Let me share some resources that might help your evaluation.",
},
),
QualificationStep(
field_name="email",
question="What's the best email to reach you at? I'll send over the details we discussed.",
),
]
class QualificationEngine:
def __init__(self):
self.lead = LeadData(session_id="")
self.current_step = 0
self.completed_fields: set[str] = set()
def get_next_question(self) -> str | None:
while self.current_step < len(QUALIFICATION_FLOW):
step = QUALIFICATION_FLOW[self.current_step]
if step.field_name not in self.completed_fields:
return step.question
self.current_step += 1
return None
def process_answer(self, field_name: str, value: str) -> str | None:
setattr(self.lead, field_name, value)
self.completed_fields.add(field_name)
step = QUALIFICATION_FLOW[self.current_step]
follow_up = step.follow_ups.get(value)
self.current_step += 1
next_q = self.get_next_question()
if follow_up and next_q:
return f"{follow_up} {next_q}"
return next_q
AI-Powered Extraction
Instead of rigid multiple-choice answers, let the LLM extract structured data from natural conversation:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from agents import Agent, function_tool, Runner
@function_tool
def update_lead_field(field_name: str, value: str) -> str:
"""Update a lead qualification field with extracted information.
field_name: one of name, email, company, company_size, role, budget, timeline, use_case
value: the extracted value from the conversation
"""
return f"Updated {field_name} to {value}"
@function_tool
def get_qualification_status() -> str:
"""Check which qualification fields have been collected and what to ask next."""
return "Returns current lead state and next question"
qualification_agent = Agent(
name="Lead Qualifier",
instructions="""You are a friendly lead qualification agent. Your goal is to
understand the visitor's needs and collect qualification information naturally.
Rules:
- Never ask more than one question at a time
- Extract information from natural responses (if they say "I'm a CTO at Acme",
extract both role and company)
- Be conversational, not robotic
- After collecting use_case, company, role, and timeline, ask for email
- Use update_lead_field to store extracted information
- If the lead score is high, express enthusiasm and offer to book a demo""",
tools=[update_lead_field, get_qualification_status],
)
CRM Integration
When the lead is qualified, push it to your CRM. Here is a HubSpot integration example:
interface HubSpotContact {
email: string;
firstname?: string;
lastname?: string;
company?: string;
jobtitle?: string;
hs_lead_status: string;
lead_score: number;
}
async function pushToHubSpot(lead: LeadData): Promise<string> {
const nameParts = (lead.name || "").split(" ");
const contact: HubSpotContact = {
email: lead.email!,
firstname: nameParts[0],
lastname: nameParts.slice(1).join(" "),
company: lead.company || undefined,
jobtitle: lead.role || undefined,
hs_lead_status: lead.grade === "hot" ? "NEW" : "OPEN",
lead_score: lead.score,
};
const response = await fetch(
"https://api.hubapi.com/crm/v3/objects/contacts",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
},
body: JSON.stringify({ properties: contact }),
},
);
if (!response.ok) {
const error = await response.json();
if (error.category === "CONFLICT") {
return await updateExistingContact(lead.email!, contact);
}
throw new Error(`HubSpot API error: ${error.message}`);
}
const result = await response.json();
return result.id;
}
Real-Time Score Display
Give the user a sense of progress by subtly showing qualification advancement. On the backend, recalculate the score after each field update and send it to the frontend:
async def on_field_update(ws, engine: QualificationEngine):
score = engine.lead.calculate_score()
await ws.send_json({
"type": "lead_progress",
"fields_collected": len(engine.completed_fields),
"total_fields": len(QUALIFICATION_FLOW),
"score": score,
"grade": engine.lead.grade.value,
})
FAQ
How do I prevent the agent from feeling like an interrogation?
Interleave value-giving with questions. After the user shares their use case, provide a relevant insight or case study before asking the next question. Limit consecutive questions to two before offering something helpful. The conversation should feel like a consultative discussion, not a form with a chatbot face.
What if the user provides their email early — should I skip ahead?
Yes. Always extract information opportunistically. If the user says "I'm John at john@acme.com, we need help with X," extract the name, email, and use case in one pass, skip those steps in the flow, and move directly to the questions you still need answered. Rigid flows that repeat already-answered questions frustrate users and reduce conversion rates.
How do I handle leads who refuse to share their email?
Respect the boundary. Offer alternatives: "No problem at all. Would you prefer I share some resources you can review on your own?" Provide value regardless — a useful recommendation or link. Track these sessions separately as anonymous qualified leads. Often, users return later and are more willing to share contact information after seeing the agent is genuinely helpful.
#LeadQualification #Sales #CRM #Scoring #ChatAgent #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.