d553d6f31e
- Added alerts processing logic in src/alerts.py to analyze threads and send WhatsApp alerts based on configured time frames. - Created FastAPI application in src/app.py to manage threads, display configurations, and trigger alert processing. - Developed database models and utility functions in src/database.py for managing threads and messages. - Integrated Twilio API for sending WhatsApp messages in src/whatsapp_sender.py. - Implemented Zoho email client in src/zoho_client.py to fetch emails and check for replies. - Added configuration management for email settings and alert parameters. - Established auto-processing loop for periodic email syncing and alert generation.
167 lines
5.0 KiB
Python
167 lines
5.0 KiB
Python
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from typing import List, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.ai import analyze_thread
|
|
from src.database import (
|
|
Message,
|
|
Thread,
|
|
get_last_incoming_outgoing,
|
|
get_thread_messages,
|
|
)
|
|
from src.whatsapp_sender import WhatsAppSender
|
|
|
|
|
|
@dataclass
|
|
class TimeFrame:
|
|
name: str
|
|
hours: int
|
|
alert_level: int
|
|
|
|
|
|
def _utc(dt: datetime) -> datetime:
|
|
if dt.tzinfo is None:
|
|
return dt.replace(tzinfo=timezone.utc)
|
|
return dt.astimezone(timezone.utc)
|
|
|
|
|
|
def _load_frames_from_config(cfg: dict) -> List[TimeFrame]:
|
|
frames = []
|
|
for f in cfg.get("time_frames", []):
|
|
try:
|
|
frames.append(
|
|
TimeFrame(
|
|
name=f.get("name", ""),
|
|
hours=int(f.get("hours", 0)),
|
|
alert_level=int(f.get("alert_level", 0)),
|
|
)
|
|
)
|
|
except Exception:
|
|
continue
|
|
# sort ascending by hours
|
|
frames.sort(key=lambda x: x.hours)
|
|
return frames
|
|
|
|
|
|
def _format_alert(
|
|
level: int, hours: int, thread: Thread, ai: dict, last_in: Optional[Message]
|
|
) -> str:
|
|
title = {
|
|
1: "LEVEL 1 ALERT (24 Hours)",
|
|
2: "LEVEL 2 ALERT (48 Hours - URGENT)",
|
|
3: "LEVEL 3 ALERT (72 Hours - CRITICAL)",
|
|
}.get(level, f"LEVEL {level} ALERT ({hours} Hours)")
|
|
sender = last_in.from_email if last_in else "unknown"
|
|
subject = thread.subject or "(no subject)"
|
|
summary = ai.get("summary") or thread.ai_summary or ""
|
|
conf_pct = int(round((ai.get("confidence") or thread.ai_confidence or 0) * 100))
|
|
return (
|
|
f"🚨 {title} 🚨\n\n"
|
|
f"Conversation with: {sender}\n"
|
|
f"Subject: {subject}\n"
|
|
f"Contextual Summary: {summary}\n\n"
|
|
f"Thread ID: {thread.id}\n"
|
|
f"Confidence: {conf_pct}%"
|
|
)
|
|
|
|
|
|
def process_alerts(db: Session, cfg: dict) -> List[int]:
|
|
"""Analyze threads and send WhatsApp alerts when thresholds are met.
|
|
|
|
Returns list of thread IDs that had alerts sent this run.
|
|
"""
|
|
account_email = (cfg.get("email_address") or cfg.get("zoho_email") or "").lower()
|
|
if not account_email:
|
|
return []
|
|
|
|
frames = _load_frames_from_config(cfg)
|
|
if not frames:
|
|
return []
|
|
|
|
# Ensure thresholds unique by level
|
|
level_to_hours = {f.alert_level: f.hours for f in frames}
|
|
|
|
# Find candidate threads: requires_reply True and account matches
|
|
threads: List[Thread] = (
|
|
db.query(Thread)
|
|
.filter(Thread.account_email == account_email, Thread.requires_reply.is_(True))
|
|
.order_by(Thread.updated_at.desc())
|
|
.limit(200)
|
|
.all()
|
|
)
|
|
|
|
to_number = cfg.get("whatsapp_to") or None
|
|
sender = WhatsAppSender(to_number=to_number)
|
|
alerted = []
|
|
now = datetime.now(timezone.utc)
|
|
|
|
for t in threads:
|
|
last_in, last_out = get_last_incoming_outgoing(db, t.id)
|
|
if not last_in:
|
|
continue
|
|
last_in_dt = _utc(last_in.date_sent) if last_in.date_sent else None
|
|
last_out_dt = (
|
|
_utc(last_out.date_sent) if last_out and last_out.date_sent else None
|
|
)
|
|
|
|
# Only if last message is incoming or last_in is later than last_out
|
|
if last_out_dt and last_out_dt > last_in_dt:
|
|
# There's a reply from us after the last incoming; skip
|
|
continue
|
|
|
|
hours_since_last_in = (
|
|
(now - last_in_dt).total_seconds() / 3600.0 if last_in_dt else 0
|
|
)
|
|
|
|
# Determine highest frame crossed
|
|
target_level = 0
|
|
target_hours = 0
|
|
for level, hours in sorted(level_to_hours.items(), key=lambda x: x[1]):
|
|
if hours_since_last_in >= hours:
|
|
target_level = level
|
|
target_hours = hours
|
|
|
|
if target_level == 0:
|
|
continue
|
|
|
|
# Avoid re-sending same or lower level
|
|
if (t.last_alert_level_sent or 0) >= target_level:
|
|
continue
|
|
|
|
# Build AI input messages
|
|
msgs = [
|
|
{
|
|
"date_sent": m.date_sent.isoformat() if m.date_sent else None,
|
|
"subject": m.subject,
|
|
"from_email": m.from_email,
|
|
"to_email": m.to_email,
|
|
"body": m.body,
|
|
"is_incoming": m.is_incoming,
|
|
}
|
|
for m in get_thread_messages(db, t.id)[-4:]
|
|
]
|
|
ai = analyze_thread(t.subject or "", msgs)
|
|
|
|
# Persist AI decision on thread
|
|
t.actionable = bool(ai.get("actionable", False))
|
|
t.ai_summary = ai.get("summary")
|
|
t.ai_confidence = ai.get("confidence")
|
|
t.last_analyzed_at = now
|
|
|
|
if not t.actionable:
|
|
# Don't alert on non-actionable
|
|
continue
|
|
|
|
# Compose and send WhatsApp
|
|
msg = _format_alert(target_level, target_hours, t, ai, last_in)
|
|
res = sender.send_alert(msg, thread_id=str(t.id))
|
|
if res.get("status") == "success":
|
|
t.last_alert_level_sent = target_level
|
|
t.last_alert_sent_at = now
|
|
alerted.append(t.id)
|
|
|
|
db.commit()
|
|
return alerted
|