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