Skip to content

About

Lead Management System – A full-stack MERN application with secure authentication, lead CRUD operations, server-side pagination, filtering, and sorting. Built with React, Express, MongoDB, and AG Grid, and deployed on Vercel + Render.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lead Management System + AI Lead Intelligence

A full-stack CRM (React, Node/Express, MongoDB). On top of it sits a Python/FastAPI service that reads a lead's interaction history and answers three questions for a sales rep:

Which leads need attention, why, and what should I do next?

Each answer is explainable. The priority comes from a documented rule table, not from an AI score. Every AI-derived claim quotes the exact interaction text it comes from, and the system still produces a useful decision when the AI is unavailable.


Contents


The problem

Sales reps juggle dozens of leads. The useful signals are buried in emails, call notes and meetings. A lead asked for pricing two weeks ago and nobody replied. Another has looped in their procurement team. A third raised a price objection that was never addressed. A CRM list sorted by "created date" shows none of this.

A plain "AI lead score" doesn't help either. A number such as 87 can't be checked or explained, and the rep has no reason to trust it.

The original system

The base project was a MERN lead manager built as an SDE internship assignment:

  • JWT authentication in httpOnly cookies, with bcrypt-hashed passwords
  • Lead CRUD with server-side pagination, filtering and sorting
  • An AG Grid lead list
  • Deployed on Vercel (frontend) and Render (backend), with MongoDB Atlas

What was added

Area Addition
AI service (ai-service/) A new FastAPI service with a rules engine, LLM extraction, evidence verification and a decision engine
Backend An Interaction model (email / call / meeting / note), analysis history, an AI client, request-ID tracing and rate limiting
Frontend A Lead Details page with interaction history, an "Analyze lead" button and an explainable intelligence panel
Fixes Session restore on page refresh, a logout that really clears the cookie, a duplicate CORS registration, and last_activity_at kept in sync with logged interactions

What a rep sees on the Lead Details page:

HIGH PRIORITY                                  rule: buying signal
Why?
✓ Requested pricing (email): "could you send over pricing for 50 seats?"
✓ Stated a timeline (email): "We'd like something in place by end of Q4."
✓ Involved their decision process (call): "needs to run it past procurement and her CFO"
⚠ No contact for 10 days.

Recommended action: Follow up by email
The lead has shown verified buying signals.

▸ All evidence (5 quotes)

Architecture

┌──────────────┐  cookie / JWT   ┌──────────────────┐   Bearer service token   ┌─────────────────────────┐
│ React (Vite) │ ──────────────► │  Node / Express  │ ───────────────────────► │  FastAPI AI service     │
│              │ ◄────────────── │  auth, CRUD,     │ ◄─────────────────────── │  (stateless, no DB)     │
└──────────────┘                 │  AI client       │   LeadAnalysisResponse   │                         │
                                 └────────┬─────────┘                          │  1. Rules engine        │
                                          │                                    │  2. LLM extraction ─────┼──► OpenAI
                                          ▼                                    │  3. Evidence verifier   │
                                 ┌──────────────────┐                          │  4. Decision engine     │
                                 │     MongoDB      │                          └─────────────────────────┘
                                 │ leads, users,    │
                                 │ interactions,    │
                                 │ lead_analyses    │
                                 └──────────────────┘
  • The browser never calls the AI service. Node checks the user's session, loads the lead and its interactions, and calls FastAPI with a shared service token that only the servers know.
  • The AI service has no database. It receives everything it needs in the request, so it can't fetch extra customer data or invent facts about the lead.
  • One request ID (X-Request-ID) is carried from the browser through Node into FastAPI and stored with each analysis, so a single request can be traced across both services' logs.

How an analysis works

Lead + interactions
      │
      ▼
1. RULES ENGINE (deterministic, no LLM)
   • screens out future-dated records (reported as data issues, never silently dropped)
   • facts: last contact, days since contact, awaiting reply since, last inbound channel
   • risks: awaiting_reply · inactivity · stalled_new · no_history
      │
      ▼  only valid interactions
2. LLM EXTRACTION (OpenAI, strict JSON-schema output)
   • intent, buying signals and objections, each with {interaction_id, quote}
   • interaction text is sent as data; the prompt forbids following instructions in it
   • malformed output → retried; provider failure → "unavailable" (not an error)
      │
      ▼
3. EVIDENCE VERIFIER
   • the interaction ID must exist, and the quote must be an exact, case-sensitive
     substring of that interaction (no fuzzy matching) and at least 6 characters long
   • failed claims are discarded one by one and counted; the rest are kept
      │
      ▼
4. DECISION ENGINE (deterministic rule table)
   • priority: first matching named rule (e.g. reply_overdue → HIGH)
   • action and channel from a second table
   • reasons built from verified quotes and rule facts
      │
      ▼
LeadAnalysisResponse { rules, insights, decision }  →  stored in MongoDB

The LLM decides nothing. It reads text and proposes claims. The verifier checks every claim against the source text, and only verified claims reach the decision engine.

Full policies, written so they can be explained in an interview:

Tech stack

Layer Technology
Frontend React 19, Vite, React Router, Tailwind CSS, AG Grid, Axios
Backend Node.js, Express, Mongoose, JWT, bcrypt, express-rate-limit
Database MongoDB (Atlas in production)
AI service Python 3.12+, FastAPI, Pydantic v2, pydantic-settings
LLM OpenAI (gpt-4o-mini by default, configurable), behind an LLMProvider interface
Testing Pytest, pytest-asyncio, Ruff (Python); node:test and Supertest (Node)

API

Backend (used by the browser; all routes require login)

Method Endpoint Description
GET /api/leads/:id/interactions The lead's interaction history, oldest first
POST /api/leads/:id/interactions Log an interaction: {type, direction?, content, occurred_at?}
GET /api/leads/:id/analysis The latest stored analysis. Never calls the LLM.
POST /api/leads/:id/analysis Run a new analysis and store it (rate-limited per user)

These sit alongside the original /api/auth/* and /api/leads CRUD routes, which are unchanged.

Errors keep the existing { "error": "..." } format:

Situation Status
Lead not found 404
Too many analysis requests 429
AI service timed out 504
AI service unavailable 503
AI service rejected the data 502
Database failure 500

Internal details are logged, never returned to the browser.

AI service (internal; called only by the backend)

Method Endpoint Description
GET /api/v1/health Liveness check (no auth)
POST /api/v1/leads/analyze Full analysis. Requires Authorization: Bearer <AI_SERVICE_TOKEN>.

Interactive OpenAPI docs, with request, response and error schemas, are at http://localhost:8000/docs while the service is running.

Running locally

Prerequisites: Node 18+, Python 3.12+, and a MongoDB instance (Atlas, a local install, or docker run -d -p 27017:27017 mongo:7).

1. Shared service token (one time):

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

2. AI service (port 8000):

cd ai-service
python -m venv .venv
.venv/Scripts/pip install -r requirements-dev.txt   # macOS/Linux: .venv/bin/pip
cp .env.example .env    # set AI_SERVICE_TOKEN and OPENAI_API_KEY
.venv/Scripts/python -m uvicorn app.main:app --port 8000 --reload

No OpenAI key? Set LLM__PROVIDER=mock. The AI part then finds nothing, the UI says so, and the rules-based decisions still work fully.

3. Backend (port 5000):

cd backend
npm install
cp .env.example .env    # set MONGO_URI, JWT_SECRET and the same AI_SERVICE_TOKEN
npm start

Optional demo data: node seed.js creates the user demo / demo123 and 100 random leads. ⚠️ It deletes all existing leads first; never run it against a real database.

4. Frontend (port 5173):

cd frontend
npm install
cp .env.example .env    # VITE_API_URL=http://localhost:5000/api
npm run dev

Open http://localhost:5173 in Chrome or Edge. The auth cookie is Secure, and these browsers accept that on http://localhost. Then open a lead with View, log a few interactions and click Analyze lead.

Environment variables

ai-service/.env (see .env.example for all options)

Variable Purpose
AI_SERVICE_TOKEN Shared secret with the backend. Analysis endpoints reject every request while it is unset.
OPENAI_API_KEY Required when LLM__PROVIDER=openai
LLM__PROVIDER / LLM__MODEL openai or mock; the model name
LLM__TIMEOUT_SECONDS, LLM__MAX_RETRIES Timeout and retry policy for the LLM call
LLM__MIN_QUOTE_CHARS Shortest quote accepted as evidence
RULES__INACTIVITY_DAYS, RULES__STALLED_NEW_DAYS, RULES__REPLY_OVERDUE_HOURS Business-rule thresholds

backend/.env (see .env.example)

Variable Purpose
MONGO_URI, JWT_SECRET, CLIENT_URL, PORT As in the original app
AI_SERVICE_URL, AI_SERVICE_TOKEN Where the AI service is, and the shared token
AI_SERVICE_TIMEOUT_MS Upstream timeout (default 60 s)
AI_ANALYSIS_RATE_LIMIT, AI_ANALYSIS_RATE_WINDOW_MS Analysis requests allowed per user per window

frontend/.env: VITE_API_URL. When unset it falls back to the deployed Render backend.

No secrets are committed. Each service has a .env.example containing placeholders only.

Testing

cd ai-service && .venv/Scripts/python -m pytest      # 295 tests, fully offline
cd ai-service && .venv/Scripts/python -m ruff check . && .venv/Scripts/python -m ruff format --check .
cd backend && npm test                               # 54 tests, no database needed
cd frontend && npm run build

What the tests cover:

  • Schemas: validation limits, timezone handling, and evidence that must always be present.
  • Rules: inactivity at 0, 7, 8, 14 and 30 days; reply overdue at 24 vs 25 hours; closed leads; future-dated data; several risks at once.
  • Extraction: a scripted mock LLM covering malformed output, retries, every provider failure, a bad claim discarded while valid claims survive, prompt injection, and making sure logs contain no customer text.
  • Decisions: every row of the policy table, conflicting signals, and the rules-only fallback.
  • HTTP: service-token auth, error mapping, request-ID forwarding, rate limiting, and a check that the service token never reaches the browser.

Live OpenAI check (opt-in; makes a real API call):

cd ai-service
RUN_LIVE_LLM_TESTS=1 OPENAI_API_KEY=sk-... .venv/Scripts/python -m pytest -m live

Verified with gpt-4o-mini:

  • OpenAI accepts the strict schema.
  • Extracted claims pass verification.
  • A typical analysis uses about 1.1–1.2k prompt tokens, 20–140 completion tokens and around 3 s.

The full stack was also exercised in a headless browser: login, adding interactions, analysis, reload persistence, logout and a mobile-width layout.

What these tests do not prove. They check that each claim is grounded, that the rules are correct and that the system handles failures. They do not measure how accurate the AI's judgement is. A handful of hand-written scenarios is not a dataset, so no accuracy figure is claimed.

Design decisions

Why a separate Python service? The AI work has its own dependencies, its own failure modes (timeouts, rate limits, malformed output) and a strongly typed contract that Pydantic handles well. Keeping it behind an internal HTTP interface leaves the existing Node app's auth and data ownership untouched, and lets the AI service be tested and scaled on its own.

Why rules and AI together, instead of only AI? Arithmetic such as "14 days since last contact" or "the email has had no reply" should never be guessed by an LLM: rules make it exact, cheap and reproducible. The LLM handles only what rules can't, which is understanding free text. Because the rules run first, a lead still gets a useful decision when OpenAI is down.

Why no AI score? There is no data to calibrate one, so a number like "87" would be invented. Priority comes from a short, documented table (for example reply_overdue → HIGH), and every decision records which rule fired.

Why evidence grounding? LLMs can state things the text doesn't support. Requiring an exact quote from a real interaction, and checking it with an exact substring match, makes every AI claim verifiable by the rep and by tests. Failed claims are dropped one at a time, so one bad claim doesn't throw away the good ones.

Why Pydantic and structured output? The model must return JSON that matches a strict schema, with no extra fields, so the LLM can't even return a priority field. Parsing free text would be fragile and hard to test.

Why stored analyses? An analysis costs an LLM call. It runs only when the rep asks, and the result (with its request ID, model and timestamp) is stored, so reopening a lead doesn't call OpenAI again and past recommendations can be audited.

Limitations

  • LLM variability. Identical input can give slightly different signals between runs. In testing this did not change the priority or action, but it can.
  • No proven accuracy. The decision table is reasoned policy, not learned from sales outcomes. The RecommendationFeedback model exists so that useful / not-useful feedback can be collected later for evaluation; it is not training data.
  • Contact is not engagement. The CRM can't tell whether an email was opened or a call connected, so outbound messages count as contact. A lead who ignores repeated emails isn't flagged as inactive.
  • Channel knowledge. The service isn't told which contact details exist. It recommends phone only if the lead has called in; otherwise it recommends email.
  • Cost and latency. Each analysis is one LLM call, about 3 s with gpt-4o-mini. Analysis runs only on demand and is rate-limited per user.
  • Rate limits are in memory, per Node process. Several instances would need a shared store.
  • No per-user lead access. The original app doesn't scope leads to users, so any logged-in user can see any lead's interactions and analyses.
  • Stored analyses contain quotes from customer conversations, as their evidence.
  • Not yet built: batch analysis of a whole page of leads, the feedback UI, Docker Compose, and deploying the AI service.

Known issues in the original code

These come from the original assignment code. They don't affect the AI workflow and were deliberately left out of scope:

  • DELETE /api/leads/:id passes req.body to findByIdAndDelete.
  • LeadForm shows a success toast ("Failed") when a save fails.
  • Some lead routes return raw err.message to the client.
  • The sort query parameter isn't whitelisted.
  • backend/.gitignore has comment lines without #.
  • The frontend linter reports 5 unused-variable errors in the original pages.

Contact

Created by Amrit Singh

About

Lead Management System – A full-stack MERN application with secure authentication, lead CRUD operations, server-side pagination, filtering, and sorting. Built with React, Express, MongoDB, and AG Grid, and deployed on Vercel + Render.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages