Build an AI Lead Qualification Agent in Python With FastAPI and Pydantic
Turn raw leads into qualified ones: structured LLM extraction, Pydantic validation, rule-based scoring, PostgreSQL storage, queues and a human review lane.
This article is published in English.
Sources: I Built an AI Lead Filtering Agent in Python — From Raw Leads to Qualified Customers
Most companies don't suffer from a shortage of leads. Their trouble is qualifying them. Hundreds of enquiries can arrive through web forms, email campaigns and marketing tools, and somebody still has to open each one, read the message, work out what the customer wants, check the budget and decide whether it deserves attention. As volume grows, that manual triage gets costly.
The practical question is whether an AI agent can take incoming leads, grasp their intent, test them against business rules and route the right ones to sales. It can, but not by pasting every lead into an LLM and trusting the reply. A dependable system needs structured output, explicit rules, validation, storage and a sound workflow. The sections below build one in Python.
What the system does
The flow is short: a lead comes in, the system interprets it, evaluates it, saves the result and triggers the next step.
Take this message from a prospective customer:
"Our sales team needs a tailor-made CRM, about 30 seats, delivered inside two months. What would it roughly cost?"
A person would pick out several facts at once:
- there's a clear business need
- the project is custom software
- the team has roughly 30 users
- a delivery window exists
- cost is the question
An agent should pull out the same facts and help sales choose what to do next. It shouldn't assume that every detailed message is a good lead. It has to judge the information against the company's real qualification criteria.
The architecture
The planned workflow is drawn below:
┌──────────────────┐
│ Incoming Lead │
│ Website / Email │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Lead Ingestion │
│ Validate & Store │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ AI Extraction │
│ Intent, Need, │
│ Budget, Timeline │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Business Rules │
│ Qualification │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Lead Scoring │
│ Qualified / │
│ Review / Reject │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ PostgreSQL │
│ Store Result │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Automation │
│ CRM / Email / │
│ Sales Notification│
└──────────────────┘
One design choice matters above the rest: keep AI understanding apart from business decisions. The LLM extracts information from the lead, while your own code decides how that information affects qualification. With that split, the system is easier to test, debug and change.
Step 1: Decide what qualifies a lead
Business rules come before code. Assume an example software development agency. It could treat a lead as qualified when:
- the customer has a genuine software requirement
- the requirement matches services the agency offers
- the message contains enough detail to understand the project
- budget or buying intent meets the agency's bar
- the message isn't spam or an obviously irrelevant enquiry
These rules are illustrative, and a real business should set its own thresholds. A filled-in record might read: service is custom CRM development, budget is ₹5,00,000, timeline is two months, company size is 30 users, intent is high, and the decision is qualified.
The AI should extract the data. The business rules should make the final call.
Step 2: Create the Python project
Python suits this job thanks to its ecosystem for APIs, LLM integrations, data processing and automation. The project layout looks like this:
ai-lead-agent/
│
├── app/
│ ├── main.py
│ ├── config.py
│ │
│ ├── agents/
│ │ └── lead_agent.py
│ │
│ ├── schemas/
│ │ └── lead.py
│ │
│ ├── services/
│ │ ├── lead_service.py
│ │ └── scoring_service.py
│ │
│ └── db/
│ └── database.py
│
├── tests/
│ └── test_scoring.py
│
├── requirements.txt
└── .env
Install the required packages:
pip install openai pydantic fastapi uvicorn python-dotenv
The pieces are:
- FastAPI for the backend API
- Pydantic for structured validation
- the OpenAI API for the LLM example
- PostgreSQL as the production database
- plain Python for lead scoring
The LLM provider can be swapped for another one later.
Step 3: Define a structured lead schema
A common mistake in AI applications is accepting free text as the final output. A reply like this is hard to process reliably:
This looks like a good lead.
The customer wants a CRM and seems interested.
Predictable JSON is the goal. Create app/schemas/lead.py and begin with the imports:
from typing import Optional, Literal
from pydantic import BaseModel, Field
Then define the models, starting with the incoming lead and continuing with the analysis the model must return:
class LeadInput(BaseModel):
name: str
email: str
company: Optional[str] = None
message: str
class LeadAnalysis(BaseModel):
summary: str service_type: Optional[str] = None intent: Literal[
"high",
"medium",
"low",
"unknown"
] budget: Optional[float] = None currency: Optional[str] = None timeline: Optional[str] = None is_spam: bool = False missing_information: list[str] = Field(
default_factory=list
) qualification_notes: str recommended_action: Literal[
"contact_sales",
"request_more_information",
"reject",
"manual_review"
]
Now the AI response has a defined shape. If the model returns an invalid value, the application can reject it or handle it, instead of quietly storing wrong data.
Step 4: Build the lead analysis function
Next comes app/agents/lead_agent.py. The model should interpret what the lead says and must not fabricate anything that is absent. Start with the imports:
import json
import os
The analysis function itself comes next. It sends the lead to the model, asks for output matching the schema and validates what comes back:
from openai import OpenAIfrom app.schemas.lead import (
LeadInput,
LeadAnalysis
)
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY")
)
SYSTEM_PROMPT = """
You are a lead analysis assistant for a software
development agency.Your task is to analyze incoming customer leads.Extract:
- Customer requirement
- Service type
- Buying intent
- Budget if explicitly mentioned
- Timeline if explicitly mentioned
- Spam indicators
- Missing informationRules:
1. Never invent a budget.
2. Never invent a timeline.
3. If information is missing, return null.
4. Return valid structured JSON.
5. Do not make a final business decision based
only on assumptions.
"""
def analyze_lead(lead: LeadInput) -> LeadAnalysis: user_prompt = f"""
Analyze this incoming lead: Name: {lead.name}
Email: {lead.email}
Company: {lead.company}
Message: {lead.message}
""" response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
response_format={
"type": "json_object"
},
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT
},
{
"role": "user",
"content": user_prompt
}
]
) result = json.loads(
response.choices[0].message.content
) return LeadAnalysis.model_validate(result)
Why the temperature is zero
For extraction you want consistency, not creative prose. A low temperature reduces variation between responses but doesn't guarantee correctness. That's why schema validation and business rules are still needed.
Model names and API features also change over time. When you implement this for production, pick a model and a structured-output method your provider currently supports.
Step 5: Add the qualification rules
This part shouldn't be handed to the LLM entirely. Suppose the agency's example criteria are:
- the project is relevant software work
- when a budget is given, it's at least ₹2,00,000
- the message isn't spam
- there's enough information to understand the requirement
Create app/services/scoring_service.py, beginning with the import:
from app.schemas.lead import LeadAnalysis
Then the scoring function, which starts at zero, collects the reasons behind each adjustment and returns an immediate zero for spam:
def score_lead(analysis: LeadAnalysis) -> dict: score = 0
reasons = [] if analysis.is_spam:
return {
"score": 0,
"status": "rejected",
"reasons": ["Spam detected"]
} if analysis.service_type:
score += 25
reasons.append(
"Service requirement identified"
) if analysis.intent == "high":
score += 30
reasons.append(
"High buying intent"
) elif analysis.intent == "medium":
score += 15
reasons.append(
"Medium buying intent"
) if analysis.budget is not None:
if analysis.budget >= 200000:
score += 25
reasons.append(
"Budget meets example threshold"
)
else:
reasons.append(
"Budget below example threshold"
) if analysis.timeline:
score += 10
reasons.append(
"Timeline provided"
) if analysis.missing_information:
score -= 10
reasons.append(
"Some information is missing"
) score = max(0, min(score, 100)) if score >= 70:
status = "qualified" elif score >= 40:
status = "review" else:
status = "unqualified" return {
"score": score,
"status": status,
"reasons": reasons
}
This is a simple, illustrative scoring scheme. A real agency might weigh other factors:
- project size
- industry
- location
- whether the service is available
- how urgent the customer is
- prior interaction
- confidence in the stated budget
- duplicate leads
Test the scoring function against real examples before letting it reject leads automatically.
Step 6: Connect the agent to FastAPI
Create app/main.py and import FastAPI:
from fastapi import FastAPI
The rest of the file ties the schema, the analysis function and the scoring service together behind an endpoint:
from app.schemas.lead import LeadInput
from app.agents.lead_agent import analyze_lead
from app.services.scoring_service import score_lead
app = FastAPI(
title="AI Lead Filtering Agent"
)
@app.get("/health")
def health_check():
return {
"status": "ok"
}
@app.post("/analyze-lead")
def analyze_incoming_lead(
lead: LeadInput
): analysis = analyze_lead(lead) score = score_lead(analysis) return {
"lead": lead,
"analysis": analysis,
"qualification": score
}
Start the server:
uvicorn app.main:app --reload
Then open the interactive API docs:
http://127.0.0.1:8000/docs
From there you can submit a lead and inspect the structured response.
Step 7: Try a realistic lead
Send a sample request like this one:
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"company": "Example Technologies",
"message": "We need a custom CRM for around 30 users. Our budget is approximately 500000 INR and we want to launch within 2 months."
}
A plausible extraction result looks like the following:
{
"summary": "Customer needs a custom CRM.",
"service_type": "CRM development",
"intent": "high",
"budget": 500000,
"currency": "INR",
"timeline": "2 months",
"is_spam": false,
"missing_information": [],
"qualification_notes": "Clear requirement, budget, and timeline.",
"recommended_action": "contact_sales"
}
The scoring service then evaluates the extracted data, with output such as:
{
"score": 90,
"status": "qualified",
"reasons": [
"Service requirement identified",
"High buying intent",
"Budget meets example threshold",
"Timeline provided"
]
}
Keep in mind that this output is an illustration of what to expect, not a measurement from a live API run.
Step 8: Store leads in PostgreSQL
For real business use, keeping results only in memory isn't enough, so persistence is required. A simple table starts like this:
CREATE TABLE leads (
id BIGSERIAL PRIMARY KEY,
The remaining columns cover the lead's details and the analysis:
name VARCHAR(150) NOT NULL, email VARCHAR(255) NOT NULL, company VARCHAR(255), message TEXT NOT NULL, service_type VARCHAR(255), intent VARCHAR(20), budget NUMERIC(14, 2), currency VARCHAR(10), timeline VARCHAR(100), is_spam BOOLEAN DEFAULT FALSE, ai_score INTEGER, qualification_status VARCHAR(30), analysis JSONB, created_at TIMESTAMPTZ DEFAULT NOW()
);
The analysis column preserves the original structured AI output, which helps when you need to debug why a lead received a certain classification.
For production, also plan for:
- a unique identifier per lead
- a way to catch duplicates
- indexes covering status and creation time
- encryption and tight access control, since lead data is sensitive
- schema migrations
- an audit trail
Step 9: Add automation
At this point the agent becomes more than a simple API. Picture this flow:
New Lead
│
▼
Save in PostgreSQL
│
▼
Analyze with AI
│
▼
Calculate Qualification
│
├── Qualified ────────► Notify Sales
│
├── Review ───────────► Human Review Queue
│
└── Unqualified ─────► Store for Reporting
When a qualified lead arrives, the system could:
- store the lead in the database
- notify the sales team
- create a CRM record
- assign a sales representative
- schedule a follow-up
- update the lead's status
The automation layer might be n8n, a background worker or a queue-based system. In production it's unwise to make the whole process depend on one synchronous HTTP request. A sturdier design places a queue in the middle:
Lead API
│
▼
Message Queue
│
▼
Background Worker
│
▼
AI Analysis
│
▼
Database
│
▼
Automation
Leads are then processed asynchronously and failed jobs can be retried.
Step 10: Add human review
This may be the most important piece. AI can misread a lead. Consider this message:
"Right now we're comparing CRM options and have no budget set, though we'd like a sense of pricing."
The model might label the intent as medium or high, while the business would rather route the lead to a person than mark it qualified automatically. That's the reason for three statuses:
- Qualified: the lead fits the rules and goes to the sales team.
- Review: the lead has useful content, but a human should decide.
- Unqualified: the lead fails the configured criteria.
Human review helps most when:
- no budget was stated
- the need is vague
- the model seems unsure of its own output
- details in the message contradict each other
- the sender operates in an industry the company doesn't serve
A good AI system doesn't hide uncertainty. It makes uncertainty visible.
What makes this an agent
A plain LLM call is just input, model, output:
Input → LLM → Output
An agent workflow does more. It understands the incoming task, extracts the relevant information, applies tools or business rules, picks the next step, stores state, triggers an action, and retries or escalates when something breaks. For the lead filter, the full path looks like this:
Incoming Lead
│
▼
AI Analysis
│
▼
Business Rules
│
▼
Qualification Decision
│
▼
Action / Human Review
The AI is one component. The backend application makes the whole process reliable, and that distinction matters.
Improvements before production
A first version is enough to see the workflow, but it shouldn't ship as is.
Background processing. Run lead analysis through a task queue such as Celery with Redis, RQ, a cloud-managed queue or a database-backed job system. That helps with retries and high volume.
Duplicate detection. One lead can arrive twice, once from the website and once from an email campaign. Match on email, company and other identifiers.
Prompt versioning. Store the prompt version beside each AI result. When qualification changes, you can then tell which prompt and rules produced an earlier result.
Observability. Track AI latency, API errors, token usage, cost per lead, the distribution of qualification outcomes and how often humans override the agent.
An evaluation dataset. Collect real leads with human-reviewed labels and test the system against them. That says far more than judging the agent from a single good example.
Security. Keep API keys out of frontend code entirely, check every incoming request, safeguard personal data and limit who may view leads.
The lesson of the architecture
Building an AI agent isn't only about calling an LLM. The real engineering lies around the model: a clear business problem, structured data, reliable validation, deterministic rules, persistence, automation, human review and monitoring. Skip those, and the result is a chatbot that sounds smart but doesn't solve the business problem.
A lead filter shouldn't just produce an answer. It should produce a useful, traceable workflow. The model understands messy customer messages, Python runs the flow, business rules define qualification, PostgreSQL holds state and automation links the result to the sales team. Together they turn an AI demo into a working application.
A natural follow-up is wiring the API to a live lead source, adding background jobs and creating a dashboard for sales to review what qualifies. The goal isn't to replace every sales decision with AI. It's to cut repetitive work so people can concentrate on the leads that deserve it.