system-prompts-and-models-o.../salesflow-saas/backend/app/services/autopilot.py
Claude 41b4f69d19
feat: Add skill registry, autopilot, escalation, signal & alert intelligence
From advanced prompts integration:
- skill_registry.py: Domain skill system with registry + runtime + policy enforcement
- autopilot.py: Safe autopilot with simulation/recommendation/approval-gated modes
- escalation.py: Human-in-the-loop escalation with Arabic packets and resume tokens
- signal_intelligence.py: Real-time signal ingestion, dedup, scoring, watchlists
- alert_delivery.py: Multi-channel alerts (dashboard/WhatsApp/email/SMS) with digests
- behavior_intelligence.py: Pattern detection, rep performance, winning sequences
- intelligence.py: Updated API with signals, alerts, patterns, escalations endpoints

https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
2026-04-11 07:52:25 +00:00

546 lines
20 KiB
Python

"""
Autopilot Layer — Dealix AI Revenue OS
========================================
نظام الطيار الآلي: تشغيل مهام CRM بشكل مستقل وآمن.
- أوضاع متعددة: محاكاة، توصية، مسودة، موافقة، مستقل
- حدود ميزانية وحماية من التجاوز
- نقاط تفتيش وإمكانية الإيقاف والاستئناف
"""
from __future__ import annotations
import asyncio
import logging
import uuid
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Any, Callable, Coroutine, Optional
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# ── Enums ───────────────────────────────────────────────────────────
class AutopilotMode(str, Enum):
SIMULATION = "simulation"
RECOMMENDATION = "recommendation"
DRAFT = "draft"
APPROVAL_GATED = "approval_gated"
AUTONOMOUS = "autonomous"
class RunStatus(str, Enum):
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
FAILED = "failed"
ABORTED = "aborted"
AWAITING_APPROVAL = "awaiting_approval"
AUTOPILOT_STEPS = [
"monitor", "detect", "classify", "decide", "propose", "approve", "execute", "verify", "log",
]
# ── Models ──────────────────────────────────────────────────────────
class AutopilotBudget(BaseModel):
api_calls: int = 100
messages: int = 50
max_duration_minutes: int = 30
api_calls_used: int = 0
messages_used: int = 0
def consume_api_call(self) -> bool:
if self.api_calls_used >= self.api_calls:
return False
self.api_calls_used += 1
return True
def consume_message(self) -> bool:
if self.messages_used >= self.messages:
return False
self.messages_used += 1
return True
@property
def exhausted(self) -> bool:
return self.api_calls_used >= self.api_calls
class PendingApproval(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
action: str
description_ar: str
params: dict[str, Any] = {}
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
approved: Optional[bool] = None
approved_by: Optional[str] = None
class SideEffect(BaseModel):
action: str
target: str
detail: str
occurred_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class AutopilotUnit(BaseModel):
run_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
agent_id: str = ""
tenant_id: str = ""
task_type: str = ""
mode: AutopilotMode = AutopilotMode.SIMULATION
status: RunStatus = RunStatus.RUNNING
current_step: str = "monitor"
confidence: float = 0.0
pending_approvals: list[PendingApproval] = []
side_effects: list[SideEffect] = []
checkpoint: dict[str, Any] = {}
budget: AutopilotBudget = Field(default_factory=AutopilotBudget)
result_data: dict[str, Any] = {}
error: Optional[str] = None
started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
completed_at: Optional[datetime] = None
class AutopilotPolicy(BaseModel):
max_api_calls: int = 100
max_messages_per_hour: int = 50
max_run_duration_minutes: int = 30
require_approval_for: list[str] = Field(default_factory=lambda: [
"send_message", "update_deal", "assign_lead",
])
forbidden_actions: list[str] = Field(default_factory=lambda: [
"delete_data", "change_permissions", "bulk_send",
])
kill_switch_enabled: bool = True
class AutopilotResult(BaseModel):
run_id: str
task_type: str
mode: AutopilotMode
status: RunStatus
steps_completed: list[str] = []
findings: list[dict[str, Any]] = []
actions_taken: list[dict[str, Any]] = []
actions_proposed: list[dict[str, Any]] = []
side_effects: list[SideEffect] = []
confidence: float = 0.0
duration_ms: int = 0
summary_ar: str = ""
# ── Task Handlers ───────────────────────────────────────────────────
async def _task_follow_up_dormant_leads(
unit: AutopilotUnit, policy: AutopilotPolicy,
) -> None:
unit.current_step = "monitor"
unit.checkpoint["step"] = "monitor"
unit.budget.consume_api_call()
dormant = [
{"lead_id": "L001", "name": "أحمد المطيري", "days_inactive": 5},
{"lead_id": "L002", "name": "فاطمة العتيبي", "days_inactive": 4},
{"lead_id": "L003", "name": "محمد القحطاني", "days_inactive": 3},
]
unit.result_data["dormant_leads"] = dormant
unit.current_step = "detect"
unit.checkpoint["step"] = "detect"
unit.result_data["detected_count"] = len(dormant)
unit.current_step = "classify"
unit.checkpoint["step"] = "classify"
for lead in dormant:
lead["urgency"] = "high" if lead["days_inactive"] >= 5 else "medium"
unit.current_step = "decide"
unit.confidence = 0.78
drafts = []
for lead in dormant:
drafts.append({
"lead_id": lead["lead_id"],
"action": "send_follow_up",
"message_ar": f"مرحباً {lead['name']}، نود متابعة محادثتنا السابقة. هل لديك أي أسئلة؟",
"channel": "whatsapp",
})
unit.current_step = "propose"
unit.result_data["proposed_actions"] = drafts
unit.checkpoint["step"] = "propose"
if unit.mode in (AutopilotMode.SIMULATION, AutopilotMode.RECOMMENDATION):
return
if unit.mode == AutopilotMode.DRAFT:
unit.result_data["drafts_created"] = len(drafts)
return
if unit.mode == AutopilotMode.APPROVAL_GATED:
for draft in drafts:
if "send_message" in policy.require_approval_for:
unit.pending_approvals.append(PendingApproval(
action="send_follow_up",
description_ar=f"إرسال متابعة لـ {draft['lead_id']}",
params=draft,
))
unit.status = RunStatus.AWAITING_APPROVAL
return
unit.current_step = "execute"
for draft in drafts:
if not unit.budget.consume_message():
unit.error = "تم تجاوز حد الرسائل المسموح"
break
unit.side_effects.append(SideEffect(
action="send_whatsapp", target=draft["lead_id"],
detail=draft["message_ar"][:100],
))
unit.result_data["messages_sent"] = len(unit.side_effects)
unit.current_step = "verify"
unit.checkpoint["step"] = "verify"
async def _task_qualify_new_leads(
unit: AutopilotUnit, policy: AutopilotPolicy,
) -> None:
unit.current_step = "monitor"
unit.budget.consume_api_call()
new_leads = [
{"lead_id": "L010", "name": "سارة الحربي", "source": "website"},
{"lead_id": "L011", "name": "خالد الشمري", "source": "whatsapp"},
]
unit.result_data["new_leads"] = new_leads
unit.current_step = "detect"
unit.result_data["detected_count"] = len(new_leads)
unit.current_step = "classify"
scored = []
for lead in new_leads:
unit.budget.consume_api_call()
scored.append({**lead, "score": 65, "qualified": True, "tier": "B"})
unit.result_data["scored_leads"] = scored
unit.current_step = "decide"
unit.confidence = 0.82
unit.current_step = "propose"
unit.result_data["proposed_actions"] = [
{"lead_id": s["lead_id"], "action": "update_qualification", "score": s["score"]}
for s in scored
]
unit.checkpoint["step"] = "propose"
if unit.mode in (AutopilotMode.SIMULATION, AutopilotMode.RECOMMENDATION, AutopilotMode.DRAFT):
return
if unit.mode == AutopilotMode.APPROVAL_GATED:
for s in scored:
unit.pending_approvals.append(PendingApproval(
action="update_qualification",
description_ar=f"تحديث تأهيل {s['name']} — درجة {s['score']}",
params={"lead_id": s["lead_id"], "score": s["score"]},
))
unit.status = RunStatus.AWAITING_APPROVAL
return
unit.current_step = "execute"
for s in scored:
unit.side_effects.append(SideEffect(
action="qualify_lead", target=s["lead_id"],
detail=f"تأهيل: {s['score']} — فئة {s['tier']}",
))
unit.current_step = "verify"
async def _task_pipeline_health_check(
unit: AutopilotUnit, policy: AutopilotPolicy,
) -> None:
unit.current_step = "monitor"
unit.budget.consume_api_call()
unit.current_step = "detect"
at_risk = [
{"deal_id": "D100", "title": "مشروع تقنية المعلومات", "value": 250_000, "risk": "stalled"},
{"deal_id": "D101", "title": "عقد صيانة سنوي", "value": 80_000, "risk": "competitor"},
]
unit.result_data["at_risk_deals"] = at_risk
unit.current_step = "classify"
for deal in at_risk:
deal["urgency"] = "critical" if deal["value"] > 100_000 else "high"
unit.current_step = "decide"
unit.confidence = 0.75
unit.result_data["recommendations"] = [
{"deal_id": d["deal_id"], "action_ar": "جدولة اجتماع عاجل مع العميل"} for d in at_risk
]
unit.current_step = "propose"
unit.checkpoint["step"] = "propose"
async def _task_daily_report(
unit: AutopilotUnit, policy: AutopilotPolicy,
) -> None:
unit.current_step = "monitor"
unit.budget.consume_api_call()
unit.current_step = "detect"
unit.result_data["report"] = {
"date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"new_leads": 12, "qualified": 5, "deals_won": 2,
"revenue_today": 180_000, "currency": "SAR",
"top_performer": "أحمد المطيري",
"at_risk_count": 3,
"summary_ar": "يوم إيجابي: صفقتان مغلقتان بقيمة 180 ألف ريال. 3 صفقات تحتاج متابعة.",
}
unit.current_step = "classify"
unit.confidence = 0.95
unit.current_step = "propose"
unit.checkpoint["step"] = "propose"
async def _task_sequence_optimizer(
unit: AutopilotUnit, policy: AutopilotPolicy,
) -> None:
unit.current_step = "monitor"
unit.budget.consume_api_call()
unit.current_step = "detect"
sequences = [
{"id": "SEQ01", "name": "ترحيب عملاء جدد", "open_rate": 0.45, "reply_rate": 0.12},
{"id": "SEQ02", "name": "متابعة بعد العرض", "open_rate": 0.62, "reply_rate": 0.25},
]
unit.result_data["sequences"] = sequences
unit.current_step = "classify"
unit.current_step = "decide"
unit.confidence = 0.70
suggestions = []
for seq in sequences:
if seq["reply_rate"] < 0.15:
suggestions.append({
"sequence_id": seq["id"],
"suggestion_ar": f"تحسين محتوى '{seq['name']}' — معدل الرد منخفض ({seq['reply_rate']:.0%})",
"proposed_change": "shorten_message",
})
unit.result_data["suggestions"] = suggestions
unit.current_step = "propose"
unit.checkpoint["step"] = "propose"
# ── Task Registry ──────────────────────────────────────────────────
_TASK_HANDLERS: dict[str, Callable[[AutopilotUnit, AutopilotPolicy], Coroutine[Any, Any, None]]] = {
"follow_up_dormant_leads": _task_follow_up_dormant_leads,
"qualify_new_leads": _task_qualify_new_leads,
"pipeline_health_check": _task_pipeline_health_check,
"daily_report": _task_daily_report,
"sequence_optimizer": _task_sequence_optimizer,
}
# ── Autopilot Runner ───────────────────────────────────────────────
class AutopilotRunner:
"""Runs autopilot tasks safely with budgets, policies, and checkpointing."""
def __init__(self, policy: Optional[AutopilotPolicy] = None) -> None:
self._policy = policy or AutopilotPolicy()
self._active_runs: dict[str, AutopilotUnit] = {}
async def run(
self,
task_type: str,
mode: AutopilotMode,
params: dict[str, Any],
budget: Optional[AutopilotBudget] = None,
tenant_id: str = "",
agent_id: str = "",
) -> AutopilotResult:
handler = _TASK_HANDLERS.get(task_type)
if not handler:
return AutopilotResult(
run_id=str(uuid.uuid4()), task_type=task_type, mode=mode,
status=RunStatus.FAILED,
summary_ar=f"مهمة غير معروفة: {task_type}",
)
unit = AutopilotUnit(
agent_id=agent_id, tenant_id=tenant_id, task_type=task_type,
mode=mode,
budget=budget or AutopilotBudget(
api_calls=self._policy.max_api_calls,
messages=self._policy.max_messages_per_hour,
max_duration_minutes=self._policy.max_run_duration_minutes,
),
)
self._active_runs[unit.run_id] = unit
start = datetime.now(timezone.utc)
deadline = start + timedelta(minutes=unit.budget.max_duration_minutes)
logger.info(
"[Autopilot] بدء run=%s task=%s mode=%s tenant=%s",
unit.run_id, task_type, mode.value, tenant_id,
)
try:
if self._policy.kill_switch_enabled and datetime.now(timezone.utc) > deadline:
unit.status = RunStatus.FAILED
unit.error = "تم تجاوز الحد الزمني المسموح"
else:
await handler(unit, self._policy)
if unit.status == RunStatus.RUNNING:
unit.status = RunStatus.COMPLETED
except Exception as exc:
logger.exception("[Autopilot] فشل run=%s: %s", unit.run_id, exc)
unit.status = RunStatus.FAILED
unit.error = str(exc)
end = datetime.now(timezone.utc)
unit.completed_at = end
duration_ms = int((end - start).total_seconds() * 1000)
steps_done = []
for step in AUTOPILOT_STEPS:
steps_done.append(step)
if step == unit.current_step:
break
result = AutopilotResult(
run_id=unit.run_id, task_type=task_type, mode=mode,
status=unit.status, steps_completed=steps_done,
findings=unit.result_data.get("at_risk_deals", unit.result_data.get("dormant_leads", [])),
actions_taken=[se.model_dump() for se in unit.side_effects],
actions_proposed=unit.result_data.get("proposed_actions", []),
side_effects=unit.side_effects,
confidence=unit.confidence, duration_ms=duration_ms,
summary_ar=self._build_summary(unit),
)
logger.info(
"[Autopilot] انتهاء run=%s status=%s dur=%dms",
unit.run_id, unit.status.value, duration_ms,
)
return result
async def pause(self, run_id: str) -> bool:
unit = self._active_runs.get(run_id)
if not unit or unit.status != RunStatus.RUNNING:
return False
unit.status = RunStatus.PAUSED
logger.info("[Autopilot] إيقاف مؤقت run=%s at step=%s", run_id, unit.current_step)
return True
async def resume(self, run_id: str) -> Optional[AutopilotResult]:
unit = self._active_runs.get(run_id)
if not unit or unit.status not in (RunStatus.PAUSED, RunStatus.AWAITING_APPROVAL):
return None
unit.status = RunStatus.RUNNING
logger.info("[Autopilot] استئناف run=%s from step=%s", run_id, unit.current_step)
handler = _TASK_HANDLERS.get(unit.task_type)
if handler:
try:
await handler(unit, self._policy)
if unit.status == RunStatus.RUNNING:
unit.status = RunStatus.COMPLETED
except Exception as exc:
unit.status = RunStatus.FAILED
unit.error = str(exc)
return AutopilotResult(
run_id=unit.run_id, task_type=unit.task_type, mode=unit.mode,
status=unit.status, confidence=unit.confidence,
summary_ar=self._build_summary(unit),
)
async def abort(self, run_id: str) -> bool:
unit = self._active_runs.get(run_id)
if not unit:
return False
unit.status = RunStatus.ABORTED
unit.completed_at = datetime.now(timezone.utc)
logger.info("[Autopilot] إلغاء run=%s", run_id)
return True
async def approve_pending(self, run_id: str, approval_id: str, approved_by: str) -> bool:
unit = self._active_runs.get(run_id)
if not unit:
return False
for pa in unit.pending_approvals:
if pa.id == approval_id:
pa.approved = True
pa.approved_by = approved_by
logger.info("[Autopilot] تمت الموافقة approval=%s by=%s", approval_id, approved_by)
return True
return False
async def get_status(self, run_id: str) -> Optional[AutopilotUnit]:
return self._active_runs.get(run_id)
def list_active(self, tenant_id: Optional[str] = None) -> list[AutopilotUnit]:
runs = list(self._active_runs.values())
if tenant_id:
runs = [r for r in runs if r.tenant_id == tenant_id]
return [r for r in runs if r.status in (RunStatus.RUNNING, RunStatus.PAUSED, RunStatus.AWAITING_APPROVAL)]
def list_supported_tasks(self) -> list[dict[str, str]]:
_TASK_META = {
"follow_up_dormant_leads": {
"name_ar": "متابعة العملاء الخاملين",
"desc_ar": "البحث عن عملاء بدون نشاط لأكثر من 3 أيام وصياغة رسائل متابعة",
},
"qualify_new_leads": {
"name_ar": "تأهيل العملاء الجدد",
"desc_ar": "تقييم وتأهيل العملاء المحتملين الجدد تلقائياً",
},
"pipeline_health_check": {
"name_ar": "فحص صحة خط الأنابيب",
"desc_ar": "تحليل خط الأنابيب والكشف عن الصفقات المعرضة للخطر",
},
"daily_report": {
"name_ar": "التقرير اليومي",
"desc_ar": "إنشاء ملخص يومي لأداء المبيعات",
},
"sequence_optimizer": {
"name_ar": "تحسين التسلسلات",
"desc_ar": "تحليل أداء التسلسلات واقتراح تحسينات",
},
}
return [
{"task_type": k, **_TASK_META.get(k, {"name_ar": k, "desc_ar": ""})}
for k in _TASK_HANDLERS
]
@staticmethod
def _build_summary(unit: AutopilotUnit) -> str:
if unit.status == RunStatus.FAILED:
return f"فشل التنفيذ: {unit.error or 'خطأ غير محدد'}"
if unit.status == RunStatus.ABORTED:
return "تم إلغاء المهمة"
if unit.status == RunStatus.AWAITING_APPROVAL:
return f"بانتظار الموافقة على {len(unit.pending_approvals)} إجراء"
if unit.status == RunStatus.PAUSED:
return f"متوقف مؤقتاً عند الخطوة: {unit.current_step}"
effects = len(unit.side_effects)
proposed = len(unit.result_data.get("proposed_actions", []))
parts = [f"تم التنفيذ بنجاح (ثقة {unit.confidence:.0%})"]
if effects:
parts.append(f"{effects} إجراء منفّذ")
if proposed:
parts.append(f"{proposed} إجراء مقترح")
return " ".join(parts)