Home / Articles

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.

Publisher: REACTAPP.TOP12 min read2818 words

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:

  1. the customer has a genuine software requirement
  2. the requirement matches services the agency offers
  3. the message contains enough detail to understand the project
  4. budget or buying intent meets the agency's bar
  5. 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:

  1. store the lead in the database
  2. notify the sales team
  3. create a CRM record
  4. assign a sales representative
  5. schedule a follow-up
  6. 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.