Tracker Agent#
Tech Stack Used#
Tech |
Purpose |
|---|---|
FastAPI + Uvicorn |
Webhook HTTP server — listens for SendGrid events on port 8002 |
SendGrid Inbound Parse |
Delivers reply/open/click/bounce/unsubscribe webhooks |
HMAC SHA-256 ( |
Validates SendGrid webhook signatures |
LangChain / Ollama / OpenAI |
LLM classifies reply sentiment + generates 2-line sales summary |
SQLAlchemy ORM |
Reads/writes |
|
Sends approval reminder to Slack webhook (tracker_agent) |
Python |
Cleans raw reply text — strips quoted chains, signatures |
Agentic Concepts Used#
Concept |
Tool / Tech |
Where |
|---|---|---|
Event-Driven Processing |
FastAPI webhook + SendGrid Inbound |
|
LLM Reply Classification |
LLM → |
|
Rule-Based Fallback |
Keyword matching |
|
Automated Status Transitions |
SQLAlchemy ORM updates |
|
Stuck Lead Health Check |
Daily scheduler job |
|
Sales Alert Routing |
SendGrid email via |
|
File-by-File Breakdown#
1. agents/tracker/webhook_listener.py — HTTP Webhook Server#
Agentic concept: Event-Driven Processing
start_listener(port=8002) at line 43 — spins up a FastAPI app served by Uvicorn:
@app.post("/webhooks/email")
async def _email_webhook(request: Request) -> JSONResponse:
return await receive_webhook(request)
receive_webhook(request) at line 57:
1. Read raw body bytes
2. validate_webhook() → HMAC SHA-256 signature check
3. parse_sendgrid_event() → normalize JSON payload → list of event dicts
4. for each event:
tracker_agent.process_event(event)
5. Always return HTTP 200 → prevents SendGrid retry storms on errors
parse_sendgrid_event(raw_payload) at line 81 — maps SendGrid event types to internal types:
SendGrid |
Internal |
|---|---|
|
|
|
|
|
|
|
|
|
|
For replied events, calls extract_reply_content() to pull clean reply text.
validate_webhook(headers, body) at line 128 — HMAC-SHA256 check:
Reads
X-Twilio-Email-Event-Webhook-Signature/X-SendGrid-SignatureheaderComputes
HMAC(SENDGRID_API_KEY, body, sha256)and compares viahmac.compare_digest()Validation failure is logged but does not drop the event in phase 1
extract_reply_content(event) at line 151 — cleans raw inbound email text:
Strips quoted reply chains (lines starting with
>)Stops at common separators:
"On {date} ... wrote:","--","---"Collapses 3+ blank lines → 2, normalizes whitespace with
re.sub
2. agents/tracker/reply_classifier.py — LLM + Rule-Based Classification#
Agentic concept: LLM-First with Rule-Based Fallback
classify_reply(reply_text) at line 23 — two-pass strategy:
1. Try LLM classifier (agents.tracker.llm_connector.classify_reply_sentiment)
→ validate result with _is_valid_classification()
→ normalize and return if valid
2. Fallback: rule_based_classify() ← always works, no LLM required
rule_based_classify(reply_text) at line 40 — keyword matching in priority order:
Priority |
Keywords |
Sentiment |
Intent |
|---|---|---|---|
1st |
“unsubscribe”, “remove me”, “stop”, “opt out” |
|
|
2nd |
“interested”, “schedule”, “call”, “sounds good”, “yes” |
|
|
3rd |
“more information”, “send me”, “details”, “how does it work” |
|
|
4th |
“not interested”, “no thank you”, “wrong person” |
|
|
Default |
(no match) |
|
|
Each result includes confidence (0.6–0.98).
generate_reply_summary(reply_text, company_name, contact_name, sentiment) at line 126:
Sends reply + context to LLM: “Summarize in 2 lines: Line 1 what they said, Line 2 recommended next action”
Falls back to
agents.writer.llm_connectorif tracker connector unavailableFalls back to static template if both LLM connectors fail
should_alert_sales(sentiment, intent) at line 170:
Returns
Trueonly for positive sentiment ORwants_meeting/wants_infointentReturns
Falsefornegative,not_interested,unsubscribe— no noise for sales team
3. agents/tracker/status_updater.py — Status Transitions#
All status changes flow through here. Valid statuses at line 29:
new → enriched → scored → approved → contacted → replied → meeting_booked → won
→ lost
→ no_response
→ archived
update_lead_status(company_id, new_status, db_session) at line 52 — validates against _VALID_STATUSES, updates company.status + company.updated_at.
mark_replied(company_id, reply_content, sentiment, db_session) at line 67:
1. update_lead_status() → "replied"
2. Find all sent/followup_sent/opened/clicked events for this company
3. Set reply_content + reply_sentiment on all of them
4. Set event_type = "replied" on all
5. followup_scheduler.cancel_followups() → stop future follow-ups
mark_unsubscribed(contact_id, db_session) at line 95:
1. contact.unsubscribed = True
2. followup_scheduler.cancel_followups()
3. Check if any other active contacts remain for this company
4. If none: company.status = "archived"
mark_bounced(contact_id, db_session) at line 128:
Sets
contact.verified = FalseWrites
OutreachEvent(event_type="bounced")with note to find alternative contactDoes not change company status — bounce triggers contact re-search, not lead close
mark_opened(company_id, contact_id, db_session) at line 197:
Writes
OutreachEvent(event_type="opened")only — does not change lead statusUsed for engagement tracking
4. agents/tracker/tracker_agent.py — Daily Health Monitor#
run_daily_checks(db_session) at line 137:
1. check_stuck_leads() → find companies stale 5+ days in non-terminal status
2. for each stuck company:
resolve_stuck_lead() → decide action based on current status
3. Return summary: {stuck_found, resolved, needs_attention}
check_stuck_leads(db_session) at line 51:
SQLAlchemy query:
company.updated_at < (now - 5 days)ANDstatus NOT IN terminal_statusesTerminal statuses:
won,lost,no_response,archived,unsubscribed
resolve_stuck_lead(company_id, db_session) at line 71 — decision tree by status:
Status |
Condition |
Action |
|---|---|---|
|
Last sent > 14 days ago, no reply |
|
|
No EmailDraft row exists |
Log warning → |
|
No approved draft after 5 days |
|
_send_approval_reminder() at line 159 — requests.post to ALERT_EMAIL Slack webhook with company name + dashboard deep-link.
5. agents/tracker/alert_sender.py — Sales Alert Email#
send_email_alert(...) at line 27:
Builds subject:
"HOT LEAD: {company_name} replied — action needed"Calls
build_alert_message()→ multiline body with score, savings, sentiment, LLM summary, dashboard linkDelivers via
email_sender.send_via_sendgrid()toALERT_EMAILsetting
should_alert(event_type, sentiment, intent) at line 97:
Only fires for
event_type="replied"— delegates toreply_classifier.should_alert_sales()Open/click events do not trigger sales alerts
Event → Action Routing#
SendGrid Webhook → POST /webhooks/email
└─ parse_sendgrid_event() → normalize event type + extract reply text
│
├─ event_type = "opened"
│ └─ status_updater.mark_opened() → OutreachEvent(opened)
│
├─ event_type = "clicked"
│ └─ OutreachEvent(clicked) logged only
│
├─ event_type = "replied"
│ ├─ extract_reply_content() → strip quoted chains
│ ├─ reply_classifier.classify_reply() → LLM → rule fallback
│ │ → {sentiment, intent, summary, confidence}
│ ├─ status_updater.mark_replied() → company.status="replied"
│ │ cancel_followups()
│ ├─ reply_classifier.generate_reply_summary() → LLM 2-line summary
│ └─ should_alert_sales() → True?
│ └─ alert_sender.send_email_alert() → SendGrid email to sales team
│
├─ event_type = "unsubscribed"
│ └─ status_updater.mark_unsubscribed() → contact.unsubscribed=True
│ cancel_followups()
│ company.status="archived" if last contact
│
└─ event_type = "bounced"
└─ status_updater.mark_bounced() → contact.verified=False
OutreachEvent(bounced)
What Gets Written to DB#
Table |
Written by |
Contents |
|---|---|---|
|
|
|
|
|
|
|
|
Updates existing events → |
|
|
Updates |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|