mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 23:39:34 +00:00
Finalized implementations: - skill_registry.py: CRM skill system with policy enforcement - autopilot.py: Safe autopilot (simulation/approval-gated modes) - escalation.py: Human escalation with Arabic packets - signal_intelligence.py: Real-time signal scoring and watchlists - alert_delivery.py: Multi-channel alerts with Arabic templates - behavior_intelligence.py: Rep performance and pattern detection - intelligence.py: Full API for signals/alerts/patterns/escalations Added 20 production libraries to requirements.txt: - Security: PyJWT (replaces abandoned python-jose), slowapi - Arabic: camel-tools, pyarabic, hijridate, phonenumbers - AI: litellm (unified LLM), instructor (structured outputs), statsforecast - WhatsApp: pywa (direct Cloud API) - Email: resend (transactional) - PDF: weasyprint (Arabic RTL) - Performance: fastapi-cache2, celery-redbeat, structlog - Monitoring: sentry-sdk, prometheus-fastapi-instrumentator - Testing: pytest-asyncio, pytest-cov, factory-boy https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
214 lines
9.8 KiB
Python
214 lines
9.8 KiB
Python
"""
|
|
Alert Delivery — Multi-channel routing with urgency-based channel selection,
|
|
acknowledgement tracking, and Arabic digest generation for Dealix CRM.
|
|
|
|
Channel matrix:
|
|
CRITICAL : dashboard + whatsapp + email + sms
|
|
HIGH : dashboard + whatsapp
|
|
MEDIUM : dashboard + email
|
|
LOW : dashboard (daily digest)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging, uuid
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta, timezone
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger("dealix.services.alert_delivery")
|
|
|
|
|
|
class AlertUrgency(str, Enum):
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class AlertChannel(str, Enum):
|
|
DASHBOARD = "dashboard"
|
|
EMAIL = "email"
|
|
WHATSAPP = "whatsapp"
|
|
SMS = "sms"
|
|
TELEGRAM = "telegram"
|
|
|
|
|
|
class Alert(BaseModel):
|
|
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
tenant_id: str
|
|
user_id: Optional[str] = None
|
|
title: str = ""
|
|
title_ar: str = ""
|
|
body: str = ""
|
|
body_ar: str = ""
|
|
urgency: AlertUrgency = AlertUrgency.MEDIUM
|
|
category: str = "system"
|
|
channels: List[AlertChannel] = [AlertChannel.DASHBOARD]
|
|
action_url: Optional[str] = None
|
|
action_label: Optional[str] = None
|
|
requires_acknowledgement: bool = False
|
|
acknowledged_at: Optional[datetime] = None
|
|
delivered_channels: List[str] = []
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
metadata: Dict[str, Any] = {}
|
|
|
|
|
|
TEMPLATES: Dict[str, Dict[str, str]] = {
|
|
"new_lead": {"title_ar": "عميل محتمل جديد",
|
|
"body_ar": "عميل محتمل جديد: {name} من {source}"},
|
|
"deal_won": {"title_ar": "صفقة ناجحة",
|
|
"body_ar": "تم إغلاق صفقة: {title} بقيمة {value} ر.س"},
|
|
"deal_at_risk": {"title_ar": "صفقة معرضة للخطر",
|
|
"body_ar": "صفقة معرضة للخطر: {title} - لا نشاط منذ {days} أيام"},
|
|
"consent_expiring": {"title_ar": "موافقة PDPL تنتهي قريبا",
|
|
"body_ar": "موافقة PDPL تنتهي خلال {days} أيام للعميل {name}"},
|
|
"escalation": {"title_ar": "تصعيد يتطلب انتباهك",
|
|
"body_ar": "يحتاج تدخلك: {title} - {reason}"},
|
|
"sequence_complete": {"title_ar": "تسلسل مكتمل",
|
|
"body_ar": "اكتمل تسلسل {name} للعميل {lead_name}"},
|
|
"meeting_booked": {"title_ar": "موعد جديد",
|
|
"body_ar": "تم حجز موعد مع {name} في {time}"},
|
|
"competitor_alert": {"title_ar": "تنبيه منافس",
|
|
"body_ar": "تغيير من المنافس {competitor}: {detail}"},
|
|
}
|
|
|
|
_CHANNEL_MATRIX: Dict[AlertUrgency, List[AlertChannel]] = {
|
|
AlertUrgency.CRITICAL: [AlertChannel.DASHBOARD, AlertChannel.WHATSAPP, AlertChannel.EMAIL, AlertChannel.SMS],
|
|
AlertUrgency.HIGH: [AlertChannel.DASHBOARD, AlertChannel.WHATSAPP],
|
|
AlertUrgency.MEDIUM: [AlertChannel.DASHBOARD, AlertChannel.EMAIL],
|
|
AlertUrgency.LOW: [AlertChannel.DASHBOARD],
|
|
}
|
|
|
|
_CAT_AR = {"lead": "العملاء المحتملون", "deal": "الصفقات", "system": "النظام",
|
|
"compliance": "الامتثال", "security": "الأمان"}
|
|
|
|
|
|
async def _dispatch(alert: Alert, channel: AlertChannel) -> bool:
|
|
logger.info("[%s] tenant=%s user=%s title=%s", channel.value.upper(),
|
|
alert.tenant_id[:8], (alert.user_id or "broadcast")[:8], alert.title_ar[:40])
|
|
return True
|
|
|
|
|
|
class AlertDelivery:
|
|
"""Multi-channel alert delivery with urgency routing and digest generation."""
|
|
|
|
def __init__(self) -> None:
|
|
self._alerts: Dict[str, List[Alert]] = defaultdict(list)
|
|
self._stats: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
|
|
|
async def send(self, alert: Alert) -> Dict[str, Any]:
|
|
targets = list(set(_CHANNEL_MATRIX.get(alert.urgency, [AlertChannel.DASHBOARD]) + alert.channels))
|
|
delivered, failed = [], []
|
|
for ch in targets:
|
|
ok = await self.send_to_channel(alert, ch)
|
|
(delivered if ok else failed).append(ch.value)
|
|
if ok:
|
|
self._stats[alert.tenant_id][ch.value] += 1
|
|
alert.delivered_channels = delivered
|
|
buf = self._alerts[alert.tenant_id]
|
|
buf.insert(0, alert)
|
|
if len(buf) > 10_000:
|
|
self._alerts[alert.tenant_id] = buf[:10_000]
|
|
self._stats[alert.tenant_id]["total"] += 1
|
|
logger.info("Alert %s [%s] delivered via %s", alert.id[:8], alert.urgency.value, ", ".join(delivered) or "none")
|
|
return {"alert_id": alert.id, "urgency": alert.urgency.value, "delivered": delivered, "failed": failed}
|
|
|
|
async def send_to_channel(self, alert: Alert, channel: AlertChannel) -> bool:
|
|
try:
|
|
return await _dispatch(alert, channel)
|
|
except Exception:
|
|
logger.exception("Channel %s dispatch failed for alert %s", channel.value, alert.id[:8])
|
|
return False
|
|
|
|
async def send_from_template(self, template_key: str, tenant_id: str, urgency: AlertUrgency,
|
|
category: str = "system", user_id: Optional[str] = None,
|
|
action_url: Optional[str] = None, requires_ack: bool = False,
|
|
**kwargs: Any) -> Dict[str, Any]:
|
|
tpl = TEMPLATES.get(template_key)
|
|
if not tpl:
|
|
return {"error": f"Unknown template: {template_key}"}
|
|
body_ar = tpl["body_ar"].format_map(defaultdict(lambda: "—", **kwargs))
|
|
alert = Alert(tenant_id=tenant_id, user_id=user_id,
|
|
title=template_key.replace("_", " ").title(), title_ar=tpl["title_ar"],
|
|
body=body_ar, body_ar=body_ar, urgency=urgency, category=category,
|
|
action_url=action_url, requires_acknowledgement=requires_ack, metadata=dict(kwargs))
|
|
return await self.send(alert)
|
|
|
|
async def acknowledge(self, alert_id: str, user_id: str) -> bool:
|
|
for alerts in self._alerts.values():
|
|
for a in alerts:
|
|
if a.id == alert_id:
|
|
if a.acknowledged_at:
|
|
return True
|
|
a.acknowledged_at = datetime.now(timezone.utc)
|
|
logger.info("Alert %s acknowledged by %s", alert_id[:8], user_id[:8])
|
|
return True
|
|
return False
|
|
|
|
async def generate_digest(self, tenant_id: str, user_id: Optional[str] = None,
|
|
period: str = "daily") -> Dict[str, Any]:
|
|
hours = 24 if period == "daily" else 168
|
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
pending = [a for a in self._alerts.get(tenant_id, [])
|
|
if a.acknowledged_at is None and a.created_at >= cutoff
|
|
and (user_id is None or a.user_id is None or a.user_id == user_id)]
|
|
if not pending:
|
|
return {"tenant_id": tenant_id, "period": period, "count": 0,
|
|
"digest_ar": "لا توجد تنبيهات جديدة", "alerts": []}
|
|
|
|
by_cat: Dict[str, List[Alert]] = defaultdict(list)
|
|
for a in pending:
|
|
by_cat[a.category].append(a)
|
|
|
|
crit = sum(1 for a in pending if a.urgency == AlertUrgency.CRITICAL)
|
|
high = sum(1 for a in pending if a.urgency == AlertUrgency.HIGH)
|
|
lines = [f"ملخص التنبيهات — {'يومي' if period == 'daily' else 'أسبوعي'}",
|
|
f"إجمالي التنبيهات: {len(pending)}"]
|
|
if crit: lines.append(f"تنبيهات حرجة: {crit}")
|
|
if high: lines.append(f"تنبيهات عالية الأهمية: {high}")
|
|
lines.append("")
|
|
for cat, items in by_cat.items():
|
|
lines.append(f"— {_CAT_AR.get(cat, cat)} ({len(items)}):")
|
|
for a in items[:10]:
|
|
tag = " [حرج]" if a.urgency == AlertUrgency.CRITICAL else (
|
|
" [مهم]" if a.urgency == AlertUrgency.HIGH else "")
|
|
lines.append(f" - {a.title_ar}{tag}")
|
|
if len(items) > 10:
|
|
lines.append(f" ... و {len(items) - 10} تنبيهات أخرى")
|
|
|
|
return {"tenant_id": tenant_id, "user_id": user_id, "period": period,
|
|
"count": len(pending), "critical": crit, "high": high,
|
|
"digest_ar": "\n".join(lines), "alerts": [a.model_dump() for a in pending[:50]]}
|
|
|
|
async def get_pending(self, tenant_id: str, user_id: Optional[str] = None) -> List[Alert]:
|
|
return [a for a in self._alerts.get(tenant_id, [])
|
|
if a.acknowledged_at is None
|
|
and (user_id is None or a.user_id is None or a.user_id == user_id)]
|
|
|
|
async def get_delivery_stats(self, tenant_id: str) -> Dict[str, Any]:
|
|
stats = dict(self._stats.get(tenant_id, {}))
|
|
total = stats.get("total", 0)
|
|
alerts = self._alerts.get(tenant_id, [])
|
|
acked = sum(1 for a in alerts if a.acknowledged_at is not None)
|
|
urg: Dict[str, int] = defaultdict(int)
|
|
cat: Dict[str, int] = defaultdict(int)
|
|
for a in alerts:
|
|
urg[a.urgency.value] += 1
|
|
cat[a.category] += 1
|
|
return {"tenant_id": tenant_id, "total_sent": total, "acknowledged": acked,
|
|
"pending": sum(1 for a in alerts if a.acknowledged_at is None),
|
|
"ack_rate": round(acked / max(total, 1) * 100, 1),
|
|
"by_channel": {k: v for k, v in stats.items() if k != "total"},
|
|
"by_urgency": dict(urg), "by_category": dict(cat)}
|
|
|
|
|
|
_instance: Optional[AlertDelivery] = None
|
|
|
|
def get_alert_delivery() -> AlertDelivery:
|
|
global _instance
|
|
if _instance is None:
|
|
_instance = AlertDelivery()
|
|
return _instance
|