system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/intelligence.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

460 lines
14 KiB
Python

"""
Intelligence API — Signals, alerts, behaviour patterns, recommendations,
escalations. Wires the signal_intelligence, alert_delivery and
behavior_intelligence services into FastAPI endpoints.
"""
from __future__ import annotations
import logging
import uuid
from datetime import datetime, timezone
from typing import List, Optional
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
from pydantic import BaseModel, Field
from app.services.signal_intelligence import (
SignalSource,
SignalEvent,
SignalFilter,
Watchlist,
get_signal_intelligence,
)
from app.services.alert_delivery import (
AlertUrgency,
get_alert_delivery,
)
from app.services.behavior_intelligence import (
get_behavior_intelligence,
)
logger = logging.getLogger("dealix.api.intelligence")
router = APIRouter(prefix="/intelligence", tags=["Intelligence"])
# ---------------------------------------------------------------------------
# Request / Response schemas
# ---------------------------------------------------------------------------
class IngestRequest(BaseModel):
source: SignalSource
payload: dict
tenant_id: str
class WatchlistCreate(BaseModel):
tenant_id: str
name: str
name_ar: str
entity_type: str
entity_ids: List[str] = []
keywords: List[str] = []
alert_threshold: float = 0.5
channels: List[str] = Field(default=["dashboard"])
class AcknowledgeRequest(BaseModel):
user_id: str
class EscalationResolveRequest(BaseModel):
user_id: str
resolution: str
resolution_ar: str = ""
class _Escalation(BaseModel):
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
tenant_id: str
title: str
title_ar: str
reason: str
reason_ar: str
entity_type: str = ""
entity_id: str = ""
assigned_to: Optional[str] = None
status: str = "open" # open, resolved
resolved_by: Optional[str] = None
resolved_at: Optional[datetime] = None
resolution: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# In-memory escalation store (per-tenant)
_escalations: dict[str, list[_Escalation]] = {}
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
@router.post("/signals/ingest", summary="Ingest a raw signal event")
async def ingest_signal(req: IngestRequest):
"""Normalise, score, deduplicate and store a signal from any source."""
engine = get_signal_intelligence()
event = await engine.ingest(req.source, req.payload, req.tenant_id)
return event.model_dump()
@router.get("/signals", summary="List recent signals with filters")
async def list_signals(
tenant_id: str,
source: Optional[SignalSource] = None,
entity_type: Optional[str] = None,
entity_id: Optional[str] = None,
min_importance: float = 0.0,
sentiment: Optional[str] = None,
limit: int = Query(default=50, le=200),
):
"""Return signals matching the provided filters, most recent first."""
engine = get_signal_intelligence()
filters = SignalFilter(
source=source,
entity_type=entity_type,
entity_id=entity_id,
min_importance=min_importance,
sentiment=sentiment,
limit=limit,
)
events = await engine.get_signals(tenant_id, filters)
return {
"count": len(events),
"signals": [e.model_dump() for e in events],
}
@router.get(
"/signals/{entity_type}/{entity_id}",
summary="Signal summary for a specific entity",
)
async def entity_signal_summary(
entity_type: str,
entity_id: str,
tenant_id: str,
hours: int = Query(default=24, le=720),
):
"""Summarise all signals for a given entity in the last N hours."""
engine = get_signal_intelligence()
summary = await engine.get_entity_summary(entity_type, entity_id, tenant_id, hours)
return summary
# ---------------------------------------------------------------------------
# Watchlists
# ---------------------------------------------------------------------------
@router.post("/watchlists", summary="Create a signal watchlist")
async def create_watchlist(body: WatchlistCreate):
"""Create a watchlist that triggers alerts when matching signals arrive."""
engine = get_signal_intelligence()
wl = Watchlist(
tenant_id=body.tenant_id,
name=body.name,
name_ar=body.name_ar,
entity_type=body.entity_type,
entity_ids=body.entity_ids,
keywords=body.keywords,
alert_threshold=body.alert_threshold,
channels=body.channels,
)
created = await engine.create_watchlist(wl)
return created.model_dump()
@router.get("/watchlists", summary="List active watchlists")
async def list_watchlists(tenant_id: str):
engine = get_signal_intelligence()
items = await engine.get_watchlists(tenant_id)
return {"count": len(items), "watchlists": [w.model_dump() for w in items]}
# ---------------------------------------------------------------------------
# Alerts
# ---------------------------------------------------------------------------
@router.get("/alerts", summary="Pending alerts for a user")
async def pending_alerts(
tenant_id: str,
user_id: Optional[str] = None,
):
"""Return all unacknowledged alerts, optionally filtered by user."""
delivery = get_alert_delivery()
alerts = await delivery.get_pending(tenant_id, user_id)
return {
"count": len(alerts),
"alerts": [a.model_dump() for a in alerts],
}
@router.post("/alerts/{alert_id}/acknowledge", summary="Acknowledge an alert")
async def acknowledge_alert(alert_id: str, body: AcknowledgeRequest):
delivery = get_alert_delivery()
ok = await delivery.acknowledge(alert_id, body.user_id)
if not ok:
raise HTTPException(404, "التنبيه غير موجود")
return {"acknowledged": True, "alert_id": alert_id}
@router.get("/alerts/stats", summary="Alert delivery statistics")
async def alert_stats(tenant_id: str):
delivery = get_alert_delivery()
return await delivery.get_delivery_stats(tenant_id)
# ---------------------------------------------------------------------------
# Digest
# ---------------------------------------------------------------------------
@router.get("/digest", summary="Generate Arabic alert digest")
async def generate_digest(
tenant_id: str,
user_id: Optional[str] = None,
period: str = Query(default="daily", regex="^(daily|weekly)$"),
):
"""Compile unacknowledged alerts into an Arabic summary."""
delivery = get_alert_delivery()
return await delivery.generate_digest(tenant_id, user_id, period)
# ---------------------------------------------------------------------------
# Behavior Patterns
# ---------------------------------------------------------------------------
@router.get("/patterns", summary="Detected behaviour patterns")
async def detected_patterns(tenant_id: str):
"""Return all detected patterns: rep performance, sequences, risks."""
bi = get_behavior_intelligence()
rep = await bi.analyze_rep_performance(tenant_id)
seq = await bi.analyze_winning_sequences(tenant_id)
risk = await bi.detect_at_risk_patterns(tenant_id)
timing = await bi.analyze_best_contact_times(tenant_id)
all_patterns = [p.model_dump() for p in rep + seq + risk]
return {
"count": len(all_patterns),
"patterns": all_patterns,
"best_contact_times": timing,
}
@router.get("/recommendations", summary="AI recommendations in Arabic")
async def recommendations(tenant_id: str):
"""Generate actionable Arabic recommendations based on detected patterns."""
bi = get_behavior_intelligence()
recs = await bi.get_recommendations(tenant_id)
return {
"count": len(recs),
"recommendations": recs,
}
# ---------------------------------------------------------------------------
# Escalations
# ---------------------------------------------------------------------------
@router.get("/escalations", summary="Pending escalations")
async def pending_escalations(tenant_id: str):
"""List all open escalations for a tenant."""
items = [e for e in _escalations.get(tenant_id, []) if e.status == "open"]
return {
"count": len(items),
"escalations": [e.model_dump() for e in items],
}
@router.post("/escalations/{escalation_id}/resolve", summary="Resolve an escalation")
async def resolve_escalation(escalation_id: str, body: EscalationResolveRequest):
"""Mark an escalation as resolved with a resolution note."""
for esc_list in _escalations.values():
for esc in esc_list:
if esc.id == escalation_id:
if esc.status == "resolved":
return {"already_resolved": True, "id": escalation_id}
esc.status = "resolved"
esc.resolved_by = body.user_id
esc.resolved_at = datetime.now(timezone.utc)
esc.resolution = body.resolution or body.resolution_ar
logger.info(
"Escalation %s resolved by %s",
escalation_id[:8], body.user_id[:8],
)
return {"resolved": True, "id": escalation_id}
raise HTTPException(404, "التصعيد غير موجود")
# ---------------------------------------------------------------------------
# Helper: create escalation (called internally by signal/alert pipeline)
# ---------------------------------------------------------------------------
async def create_escalation(
tenant_id: str,
title: str,
title_ar: str,
reason: str,
reason_ar: str,
entity_type: str = "",
entity_id: str = "",
assigned_to: Optional[str] = None,
) -> dict:
"""Programmatic escalation creation (not exposed as public endpoint)."""
esc = _Escalation(
tenant_id=tenant_id,
title=title,
title_ar=title_ar,
reason=reason,
reason_ar=reason_ar,
entity_type=entity_type,
entity_id=entity_id,
assigned_to=assigned_to,
)
_escalations.setdefault(tenant_id, []).insert(0, esc)
# Fire an alert via the delivery service
delivery = get_alert_delivery()
await delivery.send_from_template(
template_key="escalation",
tenant_id=tenant_id,
urgency=AlertUrgency.HIGH,
category="compliance",
user_id=assigned_to,
requires_ack=True,
title=title_ar,
reason=reason_ar,
)
logger.info("Escalation created: %s for tenant %s", esc.id[:8], tenant_id[:8])
return esc.model_dump()
# ---------------------------------------------------------------------------
# Legacy endpoints preserved for backward compatibility
# ---------------------------------------------------------------------------
class LeadInput(BaseModel):
id: str = "lead_001"
contact_name: str
contact_phone: str
contact_title: Optional[str] = None
company_name: str
company_website: Optional[str] = None
source: str = "whatsapp"
class MeetingReport(BaseModel):
lead_id: str
contact_name: str
company_name: str
contact_phone: str
meeting_notes: str
outcome: str = "follow_up_needed"
def _groq_key():
import os
key = os.getenv("GROQ_API_KEY", "")
if not key:
raise HTTPException(500, "GROQ_API_KEY missing")
return key
@router.post("/run-pipeline")
async def run_lead_pipeline(lead_input: LeadInput):
from app.services.lead_pipeline import DealixLeadPipeline, Lead, Company
pipeline = DealixLeadPipeline(_groq_key())
lead = Lead(
id=lead_input.id,
contact_name=lead_input.contact_name,
contact_phone=lead_input.contact_phone,
contact_title=lead_input.contact_title,
company=Company(name=lead_input.company_name, website=lead_input.company_website),
source=lead_input.source,
)
return await pipeline.run_full_pipeline(lead)
@router.post("/executive-report")
async def generate_executive_report(report_data: MeetingReport):
from app.services.lead_pipeline import DealixLeadPipeline, Lead, Company
pipeline = DealixLeadPipeline(_groq_key())
lead = Lead(
id=report_data.lead_id,
contact_name=report_data.contact_name,
contact_phone=report_data.contact_phone,
company=Company(name=report_data.company_name),
)
return await pipeline.generate_executive_report(
lead, report_data.meeting_notes, report_data.outcome,
)
@router.get("/system-report")
async def get_system_intelligence_report():
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
return await core.get_full_intelligence_report()
@router.post("/improve")
async def trigger_self_improvement(background_tasks: BackgroundTasks):
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
async def run_improvement():
await core.improver.analyze_and_improve({"triggered": "manual"})
background_tasks.add_task(run_improvement)
return {"status": "improvement_cycle_started", "message": "النظام يحلل نفسه ويتحسن..."}
@router.get("/financial-forecast")
async def get_financial_forecast():
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
return await core.financial.generate_financial_forecast({
"timestamp": "now", "pipeline": "active",
})
@router.get("/market-expansion")
async def get_expansion_opportunities():
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
return await core.strategic.analyze_market_opportunity({
"market": "Saudi Arabia",
"current_sectors": ["عقارات", "تقنية", "صحة"],
})
@router.get("/growth-plan")
async def get_90_day_growth_plan():
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
return await core.strategic.generate_growth_plan({
"current_stage": "early_growth", "market": "KSA",
})
@router.get("/health")
async def system_health():
from app.services.autonomous_core import get_autonomous_core
core = get_autonomous_core(_groq_key())
return {
"health": core.healer.get_system_health(),
"autonomous_cycle": core._cycle_count,
"improvements_applied": len(core.improver.improvements_log),
"status": "AUTONOMOUS_RUNNING",
}