mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
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
444 lines
17 KiB
Python
444 lines
17 KiB
Python
"""
|
|
Escalation Service — Dealix AI Revenue OS
|
|
============================================
|
|
نظام التصعيد: إدارة حلقة الإنسان في العملية (Human-in-the-Loop).
|
|
- إنشاء حزم تصعيد مع سياق كامل
|
|
- تعيين ومتابعة وحل التصعيدات
|
|
- قواعد تصعيد تلقائية لحالات محددة
|
|
- استئناف سير العمل بعد الحل
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import uuid
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta, timezone
|
|
from enum import Enum
|
|
from typing import Any, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ── Enums ───────────────────────────────────────────────────────────
|
|
|
|
class EscalationReason(str, Enum):
|
|
VALIDATION_FAILURE = "validation_failure"
|
|
MISSING_DATA = "missing_data"
|
|
PERMISSION_ISSUE = "permission_issue"
|
|
TIMEOUT = "timeout"
|
|
AMBIGUOUS_DATA = "ambiguous_data"
|
|
LOW_CONFIDENCE = "low_confidence"
|
|
HIGH_VALUE_DEAL = "high_value_deal"
|
|
CUSTOMER_COMPLAINT = "customer_complaint"
|
|
CONSENT_EXPIRED = "consent_expired"
|
|
DELIVERY_FAILURE = "delivery_failure"
|
|
|
|
|
|
class EscalationPriority(str, Enum):
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class EscalationStatus(str, Enum):
|
|
PENDING = "pending"
|
|
IN_PROGRESS = "in_progress"
|
|
RESOLVED = "resolved"
|
|
EXPIRED = "expired"
|
|
|
|
|
|
# ── Arabic labels ───────────────────────────────────────────────────
|
|
|
|
_REASON_AR: dict[EscalationReason, str] = {
|
|
EscalationReason.VALIDATION_FAILURE: "فشل التحقق من البيانات",
|
|
EscalationReason.MISSING_DATA: "بيانات مفقودة",
|
|
EscalationReason.PERMISSION_ISSUE: "مشكلة في الصلاحيات",
|
|
EscalationReason.TIMEOUT: "انتهاء المهلة الزمنية",
|
|
EscalationReason.AMBIGUOUS_DATA: "بيانات غامضة تحتاج توضيح",
|
|
EscalationReason.LOW_CONFIDENCE: "ثقة منخفضة في النتيجة",
|
|
EscalationReason.HIGH_VALUE_DEAL: "صفقة عالية القيمة",
|
|
EscalationReason.CUSTOMER_COMPLAINT: "شكوى عميل",
|
|
EscalationReason.CONSENT_EXPIRED: "انتهاء صلاحية الموافقة (PDPL)",
|
|
EscalationReason.DELIVERY_FAILURE: "فشل متكرر في التوصيل",
|
|
}
|
|
|
|
_PRIORITY_AR: dict[EscalationPriority, str] = {
|
|
EscalationPriority.CRITICAL: "حرج",
|
|
EscalationPriority.HIGH: "عالي",
|
|
EscalationPriority.MEDIUM: "متوسط",
|
|
EscalationPriority.LOW: "منخفض",
|
|
}
|
|
|
|
|
|
# ── Models ──────────────────────────────────────────────────────────
|
|
|
|
class EscalationArtifact(BaseModel):
|
|
type: str = "text"
|
|
name: str = ""
|
|
content: str = ""
|
|
url: Optional[str] = None
|
|
|
|
|
|
class EscalationPacket(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
tenant_id: str = ""
|
|
title: str
|
|
title_ar: str
|
|
entity_type: str
|
|
entity_id: str
|
|
workflow_name: str = ""
|
|
failed_step: str = ""
|
|
reason: EscalationReason
|
|
missing_data: list[str] = []
|
|
priority: EscalationPriority = EscalationPriority.MEDIUM
|
|
due_at: datetime = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc) + timedelta(hours=4)
|
|
)
|
|
risk_if_delayed: str = ""
|
|
risk_if_delayed_ar: str = ""
|
|
artifacts: list[EscalationArtifact] = []
|
|
resume_token: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
suggested_action: str = ""
|
|
suggested_action_ar: str = ""
|
|
confidence: float = 0.0
|
|
status: EscalationStatus = EscalationStatus.PENDING
|
|
assigned_to: Optional[str] = None
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
resolved_at: Optional[datetime] = None
|
|
resolution_data: dict[str, Any] = {}
|
|
resolution_notes: str = ""
|
|
|
|
|
|
class ResolutionInput(BaseModel):
|
|
action_taken: str
|
|
notes: str = ""
|
|
override_data: dict[str, Any] = {}
|
|
resume_workflow: bool = False
|
|
|
|
|
|
class EscalationStats(BaseModel):
|
|
total: int = 0
|
|
by_priority: dict[str, int] = Field(default_factory=dict)
|
|
by_status: dict[str, int] = Field(default_factory=dict)
|
|
by_reason: dict[str, int] = Field(default_factory=dict)
|
|
avg_resolution_minutes: float = 0.0
|
|
oldest_pending_hours: float = 0.0
|
|
overdue_count: int = 0
|
|
|
|
|
|
# ── Auto-escalation rules ──────────────────────────────────────────
|
|
|
|
class AutoEscalationRule(BaseModel):
|
|
id: str
|
|
name_ar: str
|
|
condition: str
|
|
priority: EscalationPriority
|
|
target_role: str
|
|
reason: EscalationReason
|
|
suggested_action_ar: str
|
|
|
|
|
|
DEFAULT_RULES: list[AutoEscalationRule] = [
|
|
AutoEscalationRule(
|
|
id="rule_high_value_deal",
|
|
name_ar="صفقة تتجاوز 100 ألف ريال",
|
|
condition="deal_value_sar > 100000",
|
|
priority=EscalationPriority.HIGH,
|
|
target_role="manager",
|
|
reason=EscalationReason.HIGH_VALUE_DEAL,
|
|
suggested_action_ar="مراجعة الصفقة والموافقة على استراتيجية التفاوض",
|
|
),
|
|
AutoEscalationRule(
|
|
id="rule_no_response_5d",
|
|
name_ar="عدم رد لأكثر من 5 أيام",
|
|
condition="days_since_last_response > 5",
|
|
priority=EscalationPriority.MEDIUM,
|
|
target_role="assigned_rep",
|
|
reason=EscalationReason.TIMEOUT,
|
|
suggested_action_ar="الاتصال بالعميل عبر قناة بديلة أو تصعيد للمدير",
|
|
),
|
|
AutoEscalationRule(
|
|
id="rule_low_confidence",
|
|
name_ar="ثقة ذكاء اصطناعي منخفضة",
|
|
condition="ai_confidence < 0.3",
|
|
priority=EscalationPriority.HIGH,
|
|
target_role="human_reviewer",
|
|
reason=EscalationReason.LOW_CONFIDENCE,
|
|
suggested_action_ar="مراجعة يدوية للقرار — الذكاء الاصطناعي غير واثق من النتيجة",
|
|
),
|
|
AutoEscalationRule(
|
|
id="rule_consent_expired",
|
|
name_ar="انتهاء موافقة PDPL",
|
|
condition="consent_expired == true",
|
|
priority=EscalationPriority.CRITICAL,
|
|
target_role="compliance",
|
|
reason=EscalationReason.CONSENT_EXPIRED,
|
|
suggested_action_ar="إيقاف جميع الاتصالات فوراً وطلب تجديد الموافقة",
|
|
),
|
|
AutoEscalationRule(
|
|
id="rule_delivery_failed_3x",
|
|
name_ar="فشل التوصيل 3 مرات متتالية",
|
|
condition="delivery_failures >= 3",
|
|
priority=EscalationPriority.MEDIUM,
|
|
target_role="assigned_rep",
|
|
reason=EscalationReason.DELIVERY_FAILURE,
|
|
suggested_action_ar="التحقق من رقم العميل واستخدام قناة بديلة (بريد إلكتروني أو SMS)",
|
|
),
|
|
]
|
|
|
|
|
|
# ── Workflow resume registry ────────────────────────────────────────
|
|
|
|
_workflow_resume_handlers: dict[str, Any] = {}
|
|
|
|
|
|
def register_workflow_resume(workflow_name: str, handler: Any) -> None:
|
|
_workflow_resume_handlers[workflow_name] = handler
|
|
logger.info("تسجيل معالج استئناف لسير العمل: %s", workflow_name)
|
|
|
|
|
|
# ── Escalation Service ──────────────────────────────────────────────
|
|
|
|
class EscalationService:
|
|
"""Manages human-in-the-loop escalation packets."""
|
|
|
|
def __init__(self, rules: Optional[list[AutoEscalationRule]] = None) -> None:
|
|
self._store: dict[str, EscalationPacket] = {}
|
|
self._rules = rules or DEFAULT_RULES
|
|
self._history: list[EscalationPacket] = []
|
|
self._max_history = 10_000
|
|
|
|
async def create(self, packet: EscalationPacket) -> EscalationPacket:
|
|
packet.status = EscalationStatus.PENDING
|
|
if not packet.id:
|
|
packet.id = str(uuid.uuid4())
|
|
if not packet.resume_token:
|
|
packet.resume_token = str(uuid.uuid4())
|
|
self._store[packet.id] = packet
|
|
logger.info(
|
|
"[Escalation] إنشاء تصعيد id=%s priority=%s reason=%s entity=%s/%s tenant=%s",
|
|
packet.id, packet.priority.value, packet.reason.value,
|
|
packet.entity_type, packet.entity_id, packet.tenant_id,
|
|
)
|
|
return packet
|
|
|
|
async def assign(self, escalation_id: str, user_id: str) -> Optional[EscalationPacket]:
|
|
packet = self._store.get(escalation_id)
|
|
if not packet:
|
|
logger.warning("[Escalation] تصعيد غير موجود: %s", escalation_id)
|
|
return None
|
|
if packet.status == EscalationStatus.RESOLVED:
|
|
logger.warning("[Escalation] محاولة تعيين تصعيد محلول: %s", escalation_id)
|
|
return packet
|
|
packet.assigned_to = user_id
|
|
packet.status = EscalationStatus.IN_PROGRESS
|
|
logger.info("[Escalation] تعيين %s إلى %s", escalation_id, user_id)
|
|
return packet
|
|
|
|
async def resolve(
|
|
self,
|
|
escalation_id: str,
|
|
resolution: ResolutionInput,
|
|
user_id: str,
|
|
) -> Optional[EscalationPacket]:
|
|
packet = self._store.get(escalation_id)
|
|
if not packet:
|
|
logger.warning("[Escalation] تصعيد غير موجود: %s", escalation_id)
|
|
return None
|
|
if packet.status == EscalationStatus.RESOLVED:
|
|
return packet
|
|
|
|
now = datetime.now(timezone.utc)
|
|
packet.status = EscalationStatus.RESOLVED
|
|
packet.resolved_at = now
|
|
packet.resolution_data = {
|
|
"action_taken": resolution.action_taken,
|
|
"override_data": resolution.override_data,
|
|
"resolved_by": user_id,
|
|
}
|
|
packet.resolution_notes = resolution.notes
|
|
|
|
self._history.append(packet)
|
|
if len(self._history) > self._max_history:
|
|
self._history = self._history[-self._max_history:]
|
|
|
|
logger.info(
|
|
"[Escalation] حل تصعيد id=%s by=%s dur=%s",
|
|
escalation_id, user_id,
|
|
str(now - packet.created_at) if packet.created_at else "N/A",
|
|
)
|
|
|
|
if resolution.resume_workflow:
|
|
await self._try_resume_workflow(packet)
|
|
|
|
return packet
|
|
|
|
async def resume_workflow(self, escalation_id: str) -> dict[str, Any]:
|
|
packet = self._store.get(escalation_id)
|
|
if not packet:
|
|
return {"success": False, "error": "تصعيد غير موجود"}
|
|
if packet.status != EscalationStatus.RESOLVED:
|
|
return {"success": False, "error": "التصعيد لم يُحل بعد"}
|
|
return await self._try_resume_workflow(packet)
|
|
|
|
async def _try_resume_workflow(self, packet: EscalationPacket) -> dict[str, Any]:
|
|
handler = _workflow_resume_handlers.get(packet.workflow_name)
|
|
if not handler:
|
|
logger.info(
|
|
"[Escalation] لا يوجد معالج استئناف لسير العمل: %s",
|
|
packet.workflow_name,
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"لا يوجد معالج استئناف لـ {packet.workflow_name}",
|
|
}
|
|
try:
|
|
result = await handler(
|
|
resume_token=packet.resume_token,
|
|
entity_type=packet.entity_type,
|
|
entity_id=packet.entity_id,
|
|
resolution_data=packet.resolution_data,
|
|
)
|
|
logger.info(
|
|
"[Escalation] استئناف سير العمل %s للتصعيد %s",
|
|
packet.workflow_name, packet.id,
|
|
)
|
|
return {"success": True, "result": result}
|
|
except Exception as exc:
|
|
logger.exception("[Escalation] فشل استئناف سير العمل: %s", exc)
|
|
return {"success": False, "error": str(exc)}
|
|
|
|
async def expire_overdue(self, tenant_id: str) -> int:
|
|
now = datetime.now(timezone.utc)
|
|
count = 0
|
|
for packet in self._store.values():
|
|
if (
|
|
packet.tenant_id == tenant_id
|
|
and packet.status in (EscalationStatus.PENDING, EscalationStatus.IN_PROGRESS)
|
|
and packet.due_at < now
|
|
):
|
|
packet.status = EscalationStatus.EXPIRED
|
|
count += 1
|
|
logger.info("[Escalation] انتهاء صلاحية تصعيد: %s", packet.id)
|
|
return count
|
|
|
|
async def list_pending(
|
|
self,
|
|
tenant_id: str,
|
|
priority: Optional[EscalationPriority] = None,
|
|
) -> list[EscalationPacket]:
|
|
results = [
|
|
p for p in self._store.values()
|
|
if p.tenant_id == tenant_id and p.status in (
|
|
EscalationStatus.PENDING, EscalationStatus.IN_PROGRESS,
|
|
)
|
|
]
|
|
if priority:
|
|
results = [p for p in results if p.priority == priority]
|
|
results.sort(key=lambda p: (
|
|
list(EscalationPriority).index(p.priority), p.created_at,
|
|
))
|
|
return results
|
|
|
|
async def get(self, escalation_id: str) -> Optional[EscalationPacket]:
|
|
return self._store.get(escalation_id)
|
|
|
|
async def get_stats(self, tenant_id: str) -> EscalationStats:
|
|
now = datetime.now(timezone.utc)
|
|
tenant_packets = [p for p in self._store.values() if p.tenant_id == tenant_id]
|
|
|
|
by_priority: dict[str, int] = defaultdict(int)
|
|
by_status: dict[str, int] = defaultdict(int)
|
|
by_reason: dict[str, int] = defaultdict(int)
|
|
resolution_times: list[float] = []
|
|
oldest_pending_hours = 0.0
|
|
overdue = 0
|
|
|
|
for p in tenant_packets:
|
|
by_priority[p.priority.value] += 1
|
|
by_status[p.status.value] += 1
|
|
by_reason[p.reason.value] += 1
|
|
|
|
if p.status == EscalationStatus.RESOLVED and p.resolved_at and p.created_at:
|
|
resolution_times.append(
|
|
(p.resolved_at - p.created_at).total_seconds() / 60.0
|
|
)
|
|
|
|
if p.status in (EscalationStatus.PENDING, EscalationStatus.IN_PROGRESS):
|
|
age_h = (now - p.created_at).total_seconds() / 3600.0
|
|
oldest_pending_hours = max(oldest_pending_hours, age_h)
|
|
if p.due_at < now:
|
|
overdue += 1
|
|
|
|
# Include resolved history for this tenant
|
|
for p in self._history:
|
|
if p.tenant_id == tenant_id and p.id not in self._store:
|
|
if p.resolved_at and p.created_at:
|
|
resolution_times.append(
|
|
(p.resolved_at - p.created_at).total_seconds() / 60.0
|
|
)
|
|
|
|
return EscalationStats(
|
|
total=len(tenant_packets),
|
|
by_priority=dict(by_priority),
|
|
by_status=dict(by_status),
|
|
by_reason=dict(by_reason),
|
|
avg_resolution_minutes=(
|
|
sum(resolution_times) / len(resolution_times) if resolution_times else 0.0
|
|
),
|
|
oldest_pending_hours=round(oldest_pending_hours, 2),
|
|
overdue_count=overdue,
|
|
)
|
|
|
|
async def check_auto_escalation(
|
|
self,
|
|
tenant_id: str,
|
|
context: dict[str, Any],
|
|
) -> Optional[EscalationPacket]:
|
|
for rule in self._rules:
|
|
if self._evaluate_rule(rule, context):
|
|
packet = EscalationPacket(
|
|
tenant_id=tenant_id,
|
|
title=f"Auto-escalation: {rule.id}",
|
|
title_ar=rule.name_ar,
|
|
entity_type=context.get("entity_type", "unknown"),
|
|
entity_id=context.get("entity_id", ""),
|
|
workflow_name=context.get("workflow_name", ""),
|
|
failed_step=context.get("current_step", ""),
|
|
reason=rule.reason,
|
|
priority=rule.priority,
|
|
risk_if_delayed_ar=rule.suggested_action_ar,
|
|
suggested_action=rule.suggested_action_ar,
|
|
suggested_action_ar=rule.suggested_action_ar,
|
|
confidence=context.get("confidence", 0.0),
|
|
artifacts=[EscalationArtifact(
|
|
type="context", name="auto_escalation_context",
|
|
content=str(context),
|
|
)],
|
|
)
|
|
created = await self.create(packet)
|
|
logger.info(
|
|
"[Escalation] تصعيد تلقائي rule=%s entity=%s/%s",
|
|
rule.id, packet.entity_type, packet.entity_id,
|
|
)
|
|
return created
|
|
return None
|
|
|
|
@staticmethod
|
|
def _evaluate_rule(rule: AutoEscalationRule, context: dict[str, Any]) -> bool:
|
|
cond = rule.condition
|
|
if "deal_value_sar > 100000" in cond:
|
|
return context.get("deal_value_sar", 0) > 100_000
|
|
if "days_since_last_response > 5" in cond:
|
|
return context.get("days_since_last_response", 0) > 5
|
|
if "ai_confidence < 0.3" in cond:
|
|
return context.get("confidence", 1.0) < 0.3
|
|
if "consent_expired == true" in cond:
|
|
return context.get("consent_expired", False) is True
|
|
if "delivery_failures >= 3" in cond:
|
|
return context.get("delivery_failures", 0) >= 3
|
|
return False
|