Skip to content

Commit a4a311c

Browse files
Sai Sridharclaude
andcommitted
Add all 3 tiers: relationships, revenue, SLA, sequences, knowledge, briefs, quotes + browser extension
Tier 1: - Client Relationship Heat Map (/relationships): health scores per contact, trend detection, at-risk alerts, 30/90-day email frequency tracking - Revenue Signal Extraction (/revenue): Claude Haiku scans emails for quotes, invoices, unpaid amounts, renewals, upsell opportunities; pipeline value summary - SLA Tracker (/sla): configurable response tiers per sender domain, breach/warning/ok dashboard with progress bars - Follow-up Sequences (/sequences): multi-step automated follow-up campaigns; enroll contacts from email view; APScheduler flushes steps every 4h Tier 2: - Knowledge Base (/knowledge): extracts decisions, agreements, prices, commitments from email history into searchable entries; bulk extract with one click - Meeting Briefs (/briefs): pre-meeting context brief from calendar event + email history with attendees; open action items, agenda suggestions, watch-outs - Quote Generator (/quotes): AI extracts project scope from email thread, generates line-item quote with GST; track draft→sent→won/lost lifecycle Tier 3: - Browser Extension (browser-extension/): Manifest V3 Chrome extension with AI compose popup, Gmail draft injection via content script, knowledge base search, Mailair API auth via storage Backend: 7 new services + 7 new routes, all wired into main.py Frontend: 7 new pages + Layout nav updated, all types + API methods added Supabase migrations required — see PR description for SQL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 76e72d3 commit a4a311c

31 files changed

Lines changed: 3301 additions & 10 deletions

backend/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from config import settings
1717
from limiter import limiter
18-
from routes import auth, emails, actions, replies, integrations, settings as settings_routes, billing, contacts, outlook, calendar, teams, webhooks as webhooks_routes, autoassign, crm_integrations, admin as admin_routes, waitlist as waitlist_routes, push as push_routes, scheduled as scheduled_routes, newsletter as newsletter_routes
18+
from routes import auth, emails, actions, replies, integrations, settings as settings_routes, billing, contacts, outlook, calendar, teams, webhooks as webhooks_routes, autoassign, crm_integrations, admin as admin_routes, waitlist as waitlist_routes, push as push_routes, scheduled as scheduled_routes, newsletter as newsletter_routes, relationships as relationships_routes, revenue as revenue_routes, sla as sla_routes, sequences as sequences_routes, knowledge as knowledge_routes, briefs as briefs_routes, quotes as quotes_routes
1919
from workers.email_listener import start_email_listener, stop_email_listener
2020

2121
# ---------------------------------------------------------------------------
@@ -171,6 +171,13 @@ async def generic_exception_handler(request: Request, exc: Exception):
171171
app.include_router(push_routes.router, prefix="/api")
172172
app.include_router(scheduled_routes.router, prefix="/api")
173173
app.include_router(newsletter_routes.router, prefix="/api")
174+
app.include_router(relationships_routes.router, prefix="/api")
175+
app.include_router(revenue_routes.router, prefix="/api")
176+
app.include_router(sla_routes.router, prefix="/api")
177+
app.include_router(sequences_routes.router, prefix="/api")
178+
app.include_router(knowledge_routes.router, prefix="/api")
179+
app.include_router(briefs_routes.router, prefix="/api")
180+
app.include_router(quotes_routes.router, prefix="/api")
174181

175182

176183
# ---------------------------------------------------------------------------

backend/routes/briefs.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastapi import APIRouter, Depends
2+
from pydantic import BaseModel
3+
from typing import List, Optional
4+
from middleware.auth import get_current_user
5+
from services.brief_service import generate_meeting_brief, list_briefs
6+
7+
router = APIRouter(prefix="/briefs", tags=["briefs"])
8+
9+
10+
@router.get("")
11+
async def get_briefs(current_user: dict = Depends(get_current_user)):
12+
return {"briefs": list_briefs(current_user["id"])}
13+
14+
15+
class BriefRequest(BaseModel):
16+
title: str
17+
start_time: str
18+
attendee_emails: List[str]
19+
description: Optional[str] = None
20+
21+
22+
@router.post("", status_code=201)
23+
async def create_brief(body: BriefRequest, current_user: dict = Depends(get_current_user)):
24+
brief = await generate_meeting_brief(
25+
user_id=current_user["id"],
26+
event=body.model_dump(),
27+
)
28+
return brief

backend/routes/knowledge.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from fastapi import APIRouter, Depends, Query
2+
from typing import Optional
3+
from middleware.auth import get_current_user
4+
from services.knowledge_service import search_knowledge, list_knowledge, delete_knowledge_entry, extract_knowledge_from_email, bulk_extract_knowledge
5+
6+
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
7+
8+
9+
@router.get("")
10+
async def get_knowledge(
11+
q: Optional[str] = Query(None),
12+
entry_type: Optional[str] = Query(None),
13+
current_user: dict = Depends(get_current_user),
14+
):
15+
if q:
16+
entries = search_knowledge(current_user["id"], q, entry_type)
17+
else:
18+
entries = list_knowledge(current_user["id"], entry_type)
19+
return {"entries": entries, "total": len(entries)}
20+
21+
22+
@router.post("/extract/{email_id}")
23+
async def extract_from_email(email_id: str, current_user: dict = Depends(get_current_user)):
24+
entries = await extract_knowledge_from_email(email_id, current_user["id"])
25+
return {"entries": entries, "count": len(entries)}
26+
27+
28+
@router.post("/bulk-extract")
29+
async def bulk_extract(current_user: dict = Depends(get_current_user)):
30+
count = await bulk_extract_knowledge(current_user["id"])
31+
return {"extracted": count}
32+
33+
34+
@router.delete("/{entry_id}", status_code=204)
35+
async def delete_entry(entry_id: str, current_user: dict = Depends(get_current_user)):
36+
delete_knowledge_entry(entry_id, current_user["id"])

backend/routes/quotes.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from pydantic import BaseModel
3+
from middleware.auth import get_current_user
4+
from services.quote_service import generate_quote_from_email, list_quotes, update_quote_status
5+
6+
router = APIRouter(prefix="/quotes", tags=["quotes"])
7+
8+
9+
@router.get("")
10+
async def get_quotes(current_user: dict = Depends(get_current_user)):
11+
return {"quotes": list_quotes(current_user["id"])}
12+
13+
14+
@router.post("/generate/{email_id}", status_code=201)
15+
async def generate_quote(email_id: str, current_user: dict = Depends(get_current_user)):
16+
quote = await generate_quote_from_email(email_id, current_user["id"])
17+
if not quote:
18+
raise HTTPException(status_code=404, detail="Email not found or quote generation failed")
19+
return quote
20+
21+
22+
class StatusUpdate(BaseModel):
23+
status: str # draft|sent|accepted|rejected
24+
25+
26+
@router.patch("/{quote_id}/status")
27+
async def patch_status(quote_id: str, body: StatusUpdate, current_user: dict = Depends(get_current_user)):
28+
ok = update_quote_status(quote_id, current_user["id"], body.status)
29+
if not ok:
30+
raise HTTPException(status_code=404, detail="Quote not found")
31+
return {"status": body.status}

backend/routes/relationships.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fastapi import APIRouter, Depends
2+
from middleware.auth import get_current_user
3+
from services.relationship_service import compute_relationship_scores, get_sentiment_history
4+
5+
router = APIRouter(prefix="/relationships", tags=["relationships"])
6+
7+
8+
@router.get("")
9+
async def get_relationships(current_user: dict = Depends(get_current_user)):
10+
scores = await compute_relationship_scores(current_user["id"])
11+
return {"contacts": scores, "total": len(scores)}
12+
13+
14+
@router.get("/{contact_email}/sentiment")
15+
async def get_sentiment(contact_email: str, days: int = 90, current_user: dict = Depends(get_current_user)):
16+
history = await get_sentiment_history(current_user["id"], contact_email, days)
17+
return {"contact_email": contact_email, "history": history}

backend/routes/revenue.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from pydantic import BaseModel
3+
from middleware.auth import get_current_user
4+
from services.revenue_service import (
5+
get_revenue_summary,
6+
extract_revenue_signals_from_email,
7+
scan_recent_emails_for_revenue,
8+
)
9+
from database import get_supabase
10+
11+
router = APIRouter(prefix="/revenue", tags=["revenue"])
12+
13+
14+
@router.get("/summary")
15+
async def revenue_summary(current_user: dict = Depends(get_current_user)):
16+
return get_revenue_summary(current_user["id"])
17+
18+
19+
@router.post("/scan")
20+
async def scan_emails(current_user: dict = Depends(get_current_user)):
21+
count = await scan_recent_emails_for_revenue(current_user["id"])
22+
return {"signals_found": count}
23+
24+
25+
@router.post("/signals/{email_id}")
26+
async def extract_from_email(email_id: str, current_user: dict = Depends(get_current_user)):
27+
signals = await extract_revenue_signals_from_email(email_id, current_user["id"])
28+
return {"signals": signals}
29+
30+
31+
class SignalUpdate(BaseModel):
32+
status: str # open|won|lost|dismissed
33+
34+
35+
@router.patch("/signals/{signal_id}")
36+
async def update_signal(signal_id: str, body: SignalUpdate, current_user: dict = Depends(get_current_user)):
37+
supabase = get_supabase()
38+
supabase.table("revenue_signals").update({"status": body.status}).eq("id", signal_id).eq("user_id", current_user["id"]).execute()
39+
return {"status": body.status}

backend/routes/sequences.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from pydantic import BaseModel
3+
from typing import List, Optional
4+
from middleware.auth import get_current_user
5+
from services.sequence_service import (
6+
list_sequences, create_sequence, delete_sequence,
7+
enroll_in_sequence, list_enrollments, cancel_enrollment,
8+
)
9+
10+
router = APIRouter(prefix="/sequences", tags=["sequences"])
11+
12+
13+
@router.get("")
14+
async def get_sequences(current_user: dict = Depends(get_current_user)):
15+
return {"sequences": list_sequences(current_user["id"])}
16+
17+
18+
class SequenceStep(BaseModel):
19+
delay_days: int
20+
subject_template: str
21+
body_template: str
22+
23+
24+
class CreateSequenceBody(BaseModel):
25+
name: str
26+
steps: List[SequenceStep]
27+
28+
29+
@router.post("", status_code=201)
30+
async def post_sequence(body: CreateSequenceBody, current_user: dict = Depends(get_current_user)):
31+
seq = create_sequence(
32+
user_id=current_user["id"],
33+
name=body.name,
34+
steps=[s.model_dump() for s in body.steps],
35+
)
36+
return seq
37+
38+
39+
@router.delete("/{seq_id}", status_code=204)
40+
async def del_sequence(seq_id: str, current_user: dict = Depends(get_current_user)):
41+
ok = delete_sequence(seq_id, current_user["id"])
42+
if not ok:
43+
raise HTTPException(status_code=404, detail="Sequence not found")
44+
45+
46+
class EnrollBody(BaseModel):
47+
contact_email: str
48+
email_id: Optional[str] = None
49+
50+
51+
@router.post("/{seq_id}/enroll", status_code=201)
52+
async def enroll(seq_id: str, body: EnrollBody, current_user: dict = Depends(get_current_user)):
53+
enrollment = enroll_in_sequence(
54+
user_id=current_user["id"],
55+
sequence_id=seq_id,
56+
contact_email=body.contact_email,
57+
email_id=body.email_id,
58+
)
59+
if not enrollment:
60+
raise HTTPException(status_code=404, detail="Sequence not found")
61+
return enrollment
62+
63+
64+
@router.get("/enrollments")
65+
async def get_enrollments(current_user: dict = Depends(get_current_user)):
66+
return {"enrollments": list_enrollments(current_user["id"])}
67+
68+
69+
@router.post("/enrollments/{enrollment_id}/cancel", status_code=204)
70+
async def cancel(enrollment_id: str, current_user: dict = Depends(get_current_user)):
71+
cancel_enrollment(enrollment_id, current_user["id"])

backend/routes/sla.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from pydantic import BaseModel
3+
from typing import List
4+
from middleware.auth import get_current_user
5+
from services.sla_service import get_sla_configs, create_sla_config, delete_sla_config, compute_sla_status
6+
7+
router = APIRouter(prefix="/sla", tags=["sla"])
8+
9+
10+
@router.get("/status")
11+
async def sla_status(current_user: dict = Depends(get_current_user)):
12+
return compute_sla_status(current_user["id"])
13+
14+
15+
@router.get("/configs")
16+
async def list_configs(current_user: dict = Depends(get_current_user)):
17+
return {"configs": get_sla_configs(current_user["id"])}
18+
19+
20+
class SLAConfigBody(BaseModel):
21+
tier_name: str
22+
max_response_hours: int
23+
sender_patterns: List[str] = []
24+
25+
26+
@router.post("/configs", status_code=201)
27+
async def create_config(body: SLAConfigBody, current_user: dict = Depends(get_current_user)):
28+
config = create_sla_config(
29+
user_id=current_user["id"],
30+
tier_name=body.tier_name,
31+
max_response_hours=body.max_response_hours,
32+
sender_patterns=body.sender_patterns,
33+
)
34+
return config
35+
36+
37+
@router.delete("/configs/{config_id}", status_code=204)
38+
async def del_config(config_id: str, current_user: dict = Depends(get_current_user)):
39+
ok = delete_sla_config(config_id, current_user["id"])
40+
if not ok:
41+
raise HTTPException(status_code=404, detail="Config not found")

0 commit comments

Comments
 (0)