mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
feat(platform+intelligence): Growth Control Tower + Growth Neural Network — 20 modules + 25 endpoints + 60 tests
Platform Services Layer (10 modules) — برج التحكم بالنمو - event_bus: 27 typed events (whatsapp/email/calendar/lead/payment/review/social/partner/sheet/crm/action) - identity_resolution: cross-channel merge (phone+email+CRM+social) with confidence scoring - channel_registry: 11 channels (WA, Gmail, Calendar, Moyasar, LinkedIn, X, IG, GBP, Sheets, CRM, Forms) with capabilities/risk/PDPL notes - action_policy: 9 rules (block_cold_whatsapp, block_payment_no_confirm, block_secrets, external_send_needs_approval, calendar_insert_needs_approval, social_dm_needs_explicit, unknown_source_review, high_value_deal_review, draft_only_safe) - tool_gateway: single execution chokepoint, env-flag-gated live actions (default OFF) - unified_inbox: 8 card types, ≤3 buttons enforced, Arabic - action_ledger: requested→approved→executed audit trail - proof_ledger: leads/meetings/drafts/sends/payments/revenue/risks_blocked/time_saved per channel - service_catalog: 12 sellable services - router api/routers/platform_services.py — 13 endpoints under /api/v1/platform/ Intelligence Layer (10 modules) — الشبكة العصبية للنمو - growth_brain: per-customer Brain + is_ready_for_autopilot() (≥30 signals + ≥40% accept) - command_feed: 9 daily card types (opportunity/revenue_leak/partner_suggestion/meeting_prep/review_response/competitive_move/customer_reactivation/ai_visibility_alert/action_required) - action_graph: 10 typed edges (signal→action→outcome) with what_works_summary - mission_engine: 7 missions, KILL FEATURE first_10_opportunities (10 فرص في 10 دقائق) - decision_memory: learns from accept/skip/edit/block, returns preferences (channels, tones, sectors, rejected actions, accept_rate) - trust_score: composite 0-100 (source+opt_in+channel+content+freq+approval) → safe/needs_review/blocked - revenue_dna: best_channel/segment/angle + common_objection + avg_cycle_days - opportunity_simulator: 9 Saudi sectors, expected_replies/meetings/deals/pipeline_sar + risk_score - competitive_moves: 8 move types with Arabic recommended_action_ar - board_brief: weekly Founder Shadow Board (3 decisions + 3 opportunities + 3 risks + relationship + experiment + metric) - router api/routers/intelligence_layer.py — 12 endpoints under /api/v1/intelligence/ Tests - tests/unit/test_platform_services.py — 31 tests covering catalog/channels/events/policy/gateway/identity/inbox/ledger/proof - tests/unit/test_intelligence_layer.py — 29 tests covering brain/feed/graph/missions/memory/trust/dna/simulator/competitive/brief - 60/60 new tests pass; full suite 587 passed, 2 skipped Docs - docs/PLATFORM_SERVICES_STRATEGY.md (Arabic) - docs/INTELLIGENCE_LAYER_STRATEGY.md (Arabic) - docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md — added §32 Platform Services + §33 Intelligence Layer Safety - No live send by default (all WA/Gmail/Calendar/Moyasar guarded by env flags, all OFF) - All external actions go through Tool Gateway → Action Policy → draft/approval_required - No secrets allowed in payloads (block_secrets policy) - PDPL-aware: cold WhatsApp without consent is hard-blocked - Existing 477+ tests untouched (no breaking changes) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8942c6e84c
commit
4e969131c7
@ -30,9 +30,11 @@ from api.routers import (
|
||||
growth_operator,
|
||||
health,
|
||||
innovation,
|
||||
intelligence_layer,
|
||||
leads,
|
||||
outreach,
|
||||
personal_operator,
|
||||
platform_services,
|
||||
pricing,
|
||||
prospect,
|
||||
public,
|
||||
@ -148,6 +150,8 @@ def create_app() -> FastAPI:
|
||||
app.include_router(business.router)
|
||||
app.include_router(personal_operator.router)
|
||||
app.include_router(growth_operator.router)
|
||||
app.include_router(platform_services.router)
|
||||
app.include_router(intelligence_layer.router)
|
||||
app.include_router(public.router)
|
||||
app.include_router(admin.router)
|
||||
|
||||
|
||||
140
dealix/api/routers/intelligence_layer.py
Normal file
140
dealix/api/routers/intelligence_layer.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""Intelligence Layer router — growth brain + missions + DNA + simulator + brief."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from auto_client_acquisition.intelligence_layer import (
|
||||
DecisionMemory,
|
||||
analyze_competitive_move,
|
||||
build_board_brief,
|
||||
build_command_feed_demo,
|
||||
build_growth_brain,
|
||||
build_revenue_dna_demo,
|
||||
compute_trust_score,
|
||||
extract_revenue_dna,
|
||||
learn_from_decision,
|
||||
list_intel_missions,
|
||||
recommend_missions,
|
||||
simulate_opportunity,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence-layer"])
|
||||
|
||||
# Per-customer in-memory decision memory (demo; production = Supabase)
|
||||
_MEMORY: dict[str, DecisionMemory] = {}
|
||||
|
||||
|
||||
def _memory_for(customer_id: str) -> DecisionMemory:
|
||||
if customer_id not in _MEMORY:
|
||||
_MEMORY[customer_id] = DecisionMemory(customer_id=customer_id)
|
||||
return _MEMORY[customer_id]
|
||||
|
||||
|
||||
# ── Growth Brain ──────────────────────────────────────────────
|
||||
@router.post("/growth-brain/build")
|
||||
async def growth_brain_build(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
brain = build_growth_brain(payload)
|
||||
return {**brain.to_dict(), "ready_for_autopilot": brain.is_ready_for_autopilot()}
|
||||
|
||||
|
||||
# ── Command Feed ──────────────────────────────────────────────
|
||||
@router.get("/command-feed/demo")
|
||||
async def command_feed_demo() -> dict[str, Any]:
|
||||
return build_command_feed_demo()
|
||||
|
||||
|
||||
# ── Missions ──────────────────────────────────────────────────
|
||||
@router.get("/missions")
|
||||
async def missions_list() -> dict[str, Any]:
|
||||
return list_intel_missions()
|
||||
|
||||
|
||||
@router.post("/missions/recommend")
|
||||
async def missions_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
brain_payload = payload.get("growth_brain") or payload
|
||||
brain = build_growth_brain(brain_payload) if brain_payload else None
|
||||
return recommend_missions(brain, limit=int(payload.get("limit", 3)))
|
||||
|
||||
|
||||
# ── Trust Score ───────────────────────────────────────────────
|
||||
@router.post("/trust-score")
|
||||
async def trust_score(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return compute_trust_score(
|
||||
source_quality=payload.get("source_quality", "unknown"),
|
||||
opt_in=bool(payload.get("opt_in", False)),
|
||||
channel=payload.get("channel", "whatsapp"),
|
||||
message_text=payload.get("message_text", ""),
|
||||
frequency_count_this_week=int(payload.get("frequency_count_this_week", 0)),
|
||||
weekly_cap=int(payload.get("weekly_cap", 2)),
|
||||
approval_status=payload.get("approval_status", "pending"),
|
||||
)
|
||||
|
||||
|
||||
# ── Revenue DNA ───────────────────────────────────────────────
|
||||
@router.get("/revenue-dna/demo")
|
||||
async def revenue_dna_demo() -> dict[str, Any]:
|
||||
return build_revenue_dna_demo()
|
||||
|
||||
|
||||
@router.post("/revenue-dna")
|
||||
async def revenue_dna_post(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return extract_revenue_dna(
|
||||
customer_id=payload.get("customer_id", "unknown"),
|
||||
won_deals=payload.get("won_deals", []),
|
||||
replies=payload.get("replies", []),
|
||||
objections=payload.get("objections", []),
|
||||
)
|
||||
|
||||
|
||||
# ── Opportunity Simulator ─────────────────────────────────────
|
||||
@router.post("/simulate-opportunity")
|
||||
async def simulate_opportunity_endpoint(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return simulate_opportunity(
|
||||
target_count=int(payload.get("target_count", 100)),
|
||||
sector=payload.get("sector", "saas"),
|
||||
avg_deal_value_sar=float(payload.get("avg_deal_value_sar", 25_000)),
|
||||
channel=payload.get("channel", "whatsapp"),
|
||||
cold_pct=float(payload.get("cold_pct", 0)),
|
||||
quality_lift=float(payload.get("quality_lift", 1.0)),
|
||||
)
|
||||
|
||||
|
||||
# ── Competitive Moves ─────────────────────────────────────────
|
||||
@router.post("/competitive-move/analyze")
|
||||
async def competitive_move_analyze(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return analyze_competitive_move(
|
||||
competitor_name=payload.get("competitor_name", "?"),
|
||||
move_type=payload.get("move_type", "new_offer"),
|
||||
payload=payload.get("payload", {}),
|
||||
)
|
||||
|
||||
|
||||
# ── Board Brief ───────────────────────────────────────────────
|
||||
@router.get("/board-brief/demo")
|
||||
async def board_brief_demo() -> dict[str, Any]:
|
||||
return build_board_brief()
|
||||
|
||||
|
||||
# ── Decision Memory ───────────────────────────────────────────
|
||||
@router.post("/decisions/record")
|
||||
async def decisions_record(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
customer_id = payload.get("customer_id", "demo")
|
||||
mem = _memory_for(customer_id)
|
||||
return learn_from_decision(
|
||||
memory=mem,
|
||||
decision=payload.get("decision", "skip"),
|
||||
action_type=payload.get("action_type", "send_whatsapp"),
|
||||
channel=payload.get("channel", "whatsapp"),
|
||||
sector=payload.get("sector"),
|
||||
tone=payload.get("tone"),
|
||||
objection_id=payload.get("objection_id"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/decisions/preferences")
|
||||
async def decisions_preferences(customer_id: str) -> dict[str, Any]:
|
||||
mem = _memory_for(customer_id)
|
||||
return {"customer_id": customer_id, "preferences": mem.preferences()}
|
||||
203
dealix/api/routers/platform_services.py
Normal file
203
dealix/api/routers/platform_services.py
Normal file
@ -0,0 +1,203 @@
|
||||
"""Platform Services router — channel registry + events + inbox + policy + proof."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Query
|
||||
|
||||
from auto_client_acquisition.platform_services import (
|
||||
ALL_CHANNELS,
|
||||
POLICY_RULES,
|
||||
SELLABLE_SERVICES,
|
||||
build_card_from_event,
|
||||
build_demo_feed,
|
||||
build_demo_platform_proof,
|
||||
evaluate_action,
|
||||
get_channel,
|
||||
invoke_tool,
|
||||
list_services,
|
||||
make_event,
|
||||
resolve_identity,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.action_ledger import ActionLedger
|
||||
from auto_client_acquisition.platform_services.channel_registry import channels_summary
|
||||
|
||||
router = APIRouter(prefix="/api/v1/platform", tags=["platform-services"])
|
||||
|
||||
_LEDGER = ActionLedger()
|
||||
|
||||
|
||||
# ── Catalog ────────────────────────────────────────────────────
|
||||
@router.get("/services/catalog")
|
||||
async def services_catalog() -> dict[str, Any]:
|
||||
return list_services()
|
||||
|
||||
|
||||
@router.get("/channels")
|
||||
async def channels() -> dict[str, Any]:
|
||||
return {
|
||||
"summary": channels_summary(),
|
||||
"channels": [
|
||||
{
|
||||
"key": c.key, "label_ar": c.label_ar, "label_en": c.label_en,
|
||||
"capabilities": list(c.capabilities), "beta_status": c.beta_status,
|
||||
"required_permissions": list(c.required_permissions),
|
||||
"allowed_actions": list(c.allowed_actions),
|
||||
"blocked_actions": list(c.blocked_actions),
|
||||
"risk_level": c.risk_level, "notes_ar": c.notes_ar,
|
||||
}
|
||||
for c in ALL_CHANNELS
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/channels/{channel_key}")
|
||||
async def channel_detail(channel_key: str) -> dict[str, Any]:
|
||||
c = get_channel(channel_key)
|
||||
if c is None:
|
||||
return {"error": f"unknown channel: {channel_key}"}
|
||||
return {
|
||||
"key": c.key, "label_ar": c.label_ar, "label_en": c.label_en,
|
||||
"capabilities": list(c.capabilities), "beta_status": c.beta_status,
|
||||
"required_permissions": list(c.required_permissions),
|
||||
"allowed_actions": list(c.allowed_actions),
|
||||
"blocked_actions": list(c.blocked_actions),
|
||||
"risk_level": c.risk_level, "notes_ar": c.notes_ar,
|
||||
}
|
||||
|
||||
|
||||
# ── Policy ─────────────────────────────────────────────────────
|
||||
@router.get("/policy/rules")
|
||||
async def policy_rules() -> dict[str, Any]:
|
||||
return {"count": len(POLICY_RULES), "rules": POLICY_RULES}
|
||||
|
||||
|
||||
@router.post("/actions/evaluate")
|
||||
async def actions_evaluate(
|
||||
action: str = Body(..., embed=True),
|
||||
context: dict[str, Any] = Body(default_factory=dict, embed=True),
|
||||
) -> dict[str, Any]:
|
||||
d = evaluate_action(action=action, context=context)
|
||||
return {
|
||||
"decision": d.decision,
|
||||
"matched_rule_id": d.matched_rule_id,
|
||||
"reasons_ar": d.reasons_ar,
|
||||
"suggested_next_action_ar": d.suggested_next_action_ar,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/actions/approve")
|
||||
async def actions_approve(
|
||||
customer_id: str = Body(..., embed=True),
|
||||
action_type: str = Body(..., embed=True),
|
||||
channel: str = Body(..., embed=True),
|
||||
actor: str = Body(default="user", embed=True),
|
||||
payload: dict[str, Any] = Body(default_factory=dict, embed=True),
|
||||
correlation_id: str | None = Body(default=None, embed=True),
|
||||
) -> dict[str, Any]:
|
||||
entry = _LEDGER.append(
|
||||
customer_id=customer_id,
|
||||
action_type=action_type,
|
||||
channel=channel,
|
||||
stage="approved",
|
||||
actor=actor,
|
||||
payload=payload,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return {"approved": True, "entry": entry.to_dict()}
|
||||
|
||||
|
||||
@router.get("/ledger/summary")
|
||||
async def ledger_summary(customer_id: str = Query(...)) -> dict[str, Any]:
|
||||
return _LEDGER.summary(customer_id=customer_id)
|
||||
|
||||
|
||||
# ── Events + Inbox ─────────────────────────────────────────────
|
||||
@router.post("/events/ingest")
|
||||
async def events_ingest(
|
||||
event_type: str = Body(..., embed=True),
|
||||
channel: str = Body(..., embed=True),
|
||||
customer_id: str = Body(..., embed=True),
|
||||
payload: dict[str, Any] = Body(default_factory=dict, embed=True),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
evt = make_event(
|
||||
event_type=event_type, channel=channel,
|
||||
customer_id=customer_id, payload=payload,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return {"error": str(exc)}
|
||||
card = build_card_from_event(evt)
|
||||
return {
|
||||
"event": evt.to_dict(),
|
||||
"card": card.to_dict() if card else None,
|
||||
"actionable": card is not None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/inbox/feed")
|
||||
async def inbox_feed() -> dict[str, Any]:
|
||||
"""Demo unified-inbox feed; production version reads from event store."""
|
||||
return build_demo_feed()
|
||||
|
||||
|
||||
# ── Identity + Tool gateway ───────────────────────────────────
|
||||
@router.post("/identity/resolve")
|
||||
async def identity_resolve(
|
||||
signals: list[dict[str, Any]] = Body(..., embed=True),
|
||||
) -> dict[str, Any]:
|
||||
out = resolve_identity(signals=signals)
|
||||
return {
|
||||
"identity_id": out.identity_id,
|
||||
"primary_phone": out.primary_phone,
|
||||
"primary_email": out.primary_email,
|
||||
"company": out.company,
|
||||
"crm_id": out.crm_id,
|
||||
"social_handles": out.social_handles,
|
||||
"confidence": out.confidence,
|
||||
"sources": out.sources,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/identity/resolve-demo")
|
||||
async def identity_resolve_demo() -> dict[str, Any]:
|
||||
"""Sample multi-source identity resolution."""
|
||||
out = resolve_identity(signals=[
|
||||
{"phone": "+966500000001", "company": "شركة العقار الذهبي", "source": "whatsapp"},
|
||||
{"email": "ali@example.sa", "company": "شركة العقار الذهبي", "source": "gmail"},
|
||||
{"crm_id": "crm_5421", "company": "شركة العقار الذهبي", "source": "crm"},
|
||||
{"social_handles": {"linkedin": "ali-realestate"}, "source": "linkedin_lead_forms"},
|
||||
])
|
||||
return {
|
||||
"identity_id": out.identity_id,
|
||||
"primary_phone": out.primary_phone,
|
||||
"primary_email": out.primary_email,
|
||||
"company": out.company,
|
||||
"crm_id": out.crm_id,
|
||||
"social_handles": out.social_handles,
|
||||
"confidence": out.confidence,
|
||||
"sources": out.sources,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tools/invoke")
|
||||
async def tools_invoke(
|
||||
tool: str = Body(..., embed=True),
|
||||
payload: dict[str, Any] = Body(default_factory=dict, embed=True),
|
||||
context: dict[str, Any] = Body(default_factory=dict, embed=True),
|
||||
) -> dict[str, Any]:
|
||||
r = invoke_tool(tool=tool, payload=payload, context=context)
|
||||
return {
|
||||
"status": r.status,
|
||||
"tool": r.tool,
|
||||
"matched_policy_rule": r.matched_policy_rule,
|
||||
"reasons_ar": r.reasons_ar,
|
||||
"next_action_ar": r.next_action_ar,
|
||||
}
|
||||
|
||||
|
||||
# ── Proof ──────────────────────────────────────────────────────
|
||||
@router.get("/proof-ledger/demo")
|
||||
async def proof_ledger_demo() -> dict[str, Any]:
|
||||
return build_demo_platform_proof().to_dict()
|
||||
@ -0,0 +1,67 @@
|
||||
"""
|
||||
Intelligence Layer — the decision brain on top of platform_services.
|
||||
|
||||
Turns Dealix from "channels + actions" into a **Growth Neural Network**:
|
||||
the system understands the customer fully, watches the market, decides,
|
||||
executes (with approval), and learns from every outcome.
|
||||
|
||||
Modules:
|
||||
- growth_brain : per-customer brain (context + preferences + priorities)
|
||||
- command_feed : Arabic decision cards (what to do now)
|
||||
- action_graph : signal→action→outcome typed relationships
|
||||
- mission_engine : 7 outcome-shaped missions (durable workflows)
|
||||
- decision_memory : learns from Accept/Skip/Edit signals
|
||||
- trust_score : per-action safety verdict (safe/review/blocked)
|
||||
- revenue_dna : best-channel/segment/angle/objection per customer
|
||||
- opportunity_simulator: forward simulation before sending
|
||||
- competitive_moves : detect + respond to competitor signals
|
||||
- board_brief : weekly founder/board-ready brief
|
||||
"""
|
||||
|
||||
from auto_client_acquisition.intelligence_layer.action_graph import (
|
||||
ActionEdge,
|
||||
ActionGraph,
|
||||
EDGE_TYPES,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief
|
||||
from auto_client_acquisition.intelligence_layer.command_feed import (
|
||||
INTEL_CARD_TYPES,
|
||||
build_command_feed_demo,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.competitive_moves import (
|
||||
analyze_competitive_move,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.decision_memory import (
|
||||
DecisionMemory,
|
||||
learn_from_decision,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.growth_brain import (
|
||||
GrowthBrain,
|
||||
build_growth_brain,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.mission_engine import (
|
||||
INTEL_MISSIONS,
|
||||
list_intel_missions,
|
||||
recommend_missions,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.opportunity_simulator import (
|
||||
simulate_opportunity,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.revenue_dna import (
|
||||
build_revenue_dna_demo,
|
||||
extract_revenue_dna,
|
||||
)
|
||||
from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
|
||||
|
||||
__all__ = [
|
||||
"GrowthBrain", "build_growth_brain",
|
||||
"INTEL_CARD_TYPES", "build_command_feed_demo",
|
||||
"ActionGraph", "ActionEdge", "EDGE_TYPES",
|
||||
"INTEL_MISSIONS", "list_intel_missions", "recommend_missions",
|
||||
"DecisionMemory", "learn_from_decision",
|
||||
"compute_trust_score",
|
||||
"extract_revenue_dna", "build_revenue_dna_demo",
|
||||
"simulate_opportunity",
|
||||
"analyze_competitive_move",
|
||||
"build_board_brief",
|
||||
]
|
||||
@ -0,0 +1,90 @@
|
||||
"""Action Graph — typed signal→action→approval→outcome→proof relationships."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
EDGE_TYPES: tuple[str, ...] = (
|
||||
"signal_created_opportunity",
|
||||
"message_triggered_reply",
|
||||
"reply_created_meeting",
|
||||
"meeting_created_followup",
|
||||
"followup_influenced_payment",
|
||||
"objection_required_proof",
|
||||
"partner_introduced_customer",
|
||||
"review_created_recovery_task",
|
||||
"approval_allowed_send",
|
||||
"blocked_action_prevented_risk",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionEdge:
|
||||
"""One typed edge in the action graph."""
|
||||
|
||||
edge_id: str
|
||||
edge_type: str
|
||||
src_id: str
|
||||
dst_id: str
|
||||
customer_id: str
|
||||
occurred_at: datetime
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"edge_id": self.edge_id,
|
||||
"edge_type": self.edge_type,
|
||||
"src_id": self.src_id,
|
||||
"dst_id": self.dst_id,
|
||||
"customer_id": self.customer_id,
|
||||
"occurred_at": self.occurred_at.isoformat(),
|
||||
"payload": self.payload,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionGraph:
|
||||
"""In-memory action graph for the customer's decision history."""
|
||||
|
||||
edges: list[ActionEdge] = field(default_factory=list)
|
||||
|
||||
def add_edge(
|
||||
self,
|
||||
*,
|
||||
edge_type: str,
|
||||
src_id: str,
|
||||
dst_id: str,
|
||||
customer_id: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> ActionEdge:
|
||||
if edge_type not in EDGE_TYPES:
|
||||
raise ValueError(f"unknown edge_type: {edge_type}")
|
||||
e = ActionEdge(
|
||||
edge_id=f"edge_{uuid.uuid4().hex[:16]}",
|
||||
edge_type=edge_type,
|
||||
src_id=src_id,
|
||||
dst_id=dst_id,
|
||||
customer_id=customer_id,
|
||||
occurred_at=datetime.now(timezone.utc).replace(tzinfo=None),
|
||||
payload=payload or {},
|
||||
)
|
||||
self.edges.append(e)
|
||||
return e
|
||||
|
||||
def what_works_summary(self, customer_id: str) -> dict[str, Any]:
|
||||
"""Roll-up: which signal types led to outcomes?"""
|
||||
by_type: dict[str, int] = {}
|
||||
for e in self.edges:
|
||||
if e.customer_id != customer_id:
|
||||
continue
|
||||
by_type[e.edge_type] = by_type.get(e.edge_type, 0) + 1
|
||||
winning = sorted(by_type.items(), key=lambda x: x[1], reverse=True)
|
||||
return {
|
||||
"total_edges": sum(by_type.values()),
|
||||
"by_edge_type": by_type,
|
||||
"top_winning_relationships": winning[:5],
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
"""Founder Shadow Board — weekly brief for founder/board."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_board_brief(
|
||||
*,
|
||||
customer_id: str = "demo",
|
||||
customer_name: str = "Demo Saudi B2B Co.",
|
||||
week_label: str = "May W1 2026",
|
||||
pipeline_added_sar: float = 185_000,
|
||||
revenue_won_sar: float = 30_000,
|
||||
meetings_booked: int = 14,
|
||||
risks_blocked: int = 21,
|
||||
leak_recovered_sar: float = 12_000,
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the founder/board-ready weekly brief."""
|
||||
return {
|
||||
"customer_id": customer_id,
|
||||
"customer_name": customer_name,
|
||||
"week_label": week_label,
|
||||
"decisions_required_ar": [
|
||||
"اعتماد رفع price على الـ Growth tier 10% — منافس رفع 15%.",
|
||||
"الموافقة على Partnership Sprint مع وكالة B2B في جدة.",
|
||||
"اختيار pilot vertical للشهر القادم (clinics vs training).",
|
||||
],
|
||||
"top_opportunities_ar": [
|
||||
f"شركة العقار الذهبي — اجتماع غداً ({250_000:,} ريال محتمل).",
|
||||
f"3 leads inbound من LinkedIn Lead Forms ({36_000:,} ريال).",
|
||||
f"Reactivation campaign على 12 عميل خامل ({80_000:,} ريال).",
|
||||
],
|
||||
"top_risks_ar": [
|
||||
"صفقة 250K معرضة (single-threaded) — تحتاج multi-thread.",
|
||||
"تأخر في الرد على 7 leads خلال 72+ ساعة.",
|
||||
"تقييم Google 2-نجوم بدون رد — يحتاج ≤24 ساعة.",
|
||||
],
|
||||
"key_relationship_ar": (
|
||||
"خالد ع. (شريك في وكالة B2B جدة) — اقترح اجتماع 20 دقيقة الأسبوع القادم."
|
||||
),
|
||||
"experiment_to_run_ar": (
|
||||
"اختبر رسالة قصيرة (≤4 سطور) بدلاً من النسخة الحالية على قطاع real_estate."
|
||||
),
|
||||
"metric_to_watch_ar": (
|
||||
f"approve_rate الأسبوعي: الهدف ≥45% (آخر أسبوع 38%)."
|
||||
),
|
||||
"money_summary": {
|
||||
"pipeline_added_sar": pipeline_added_sar,
|
||||
"revenue_won_sar": revenue_won_sar,
|
||||
"leak_recovered_sar": leak_recovered_sar,
|
||||
"risks_blocked_count": risks_blocked,
|
||||
"meetings_booked": meetings_booked,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
"""Intelligence Command Feed — Arabic decision cards with ≤3 buttons."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
INTEL_CARD_TYPES: tuple[str, ...] = (
|
||||
"opportunity",
|
||||
"revenue_leak",
|
||||
"approval_needed",
|
||||
"meeting_prep",
|
||||
"payment_followup",
|
||||
"partner_suggestion",
|
||||
"social_signal",
|
||||
"review_response",
|
||||
"competitive_move",
|
||||
)
|
||||
|
||||
|
||||
def build_command_feed_demo() -> dict[str, Any]:
|
||||
"""Deterministic Arabic command feed for demo + tests."""
|
||||
cards = [
|
||||
{
|
||||
"type": "opportunity",
|
||||
"title_ar": "فرصة نمو — شركة تدريب في الرياض",
|
||||
"summary_ar": "نشروا 3 وظائف مبيعات جديدة → توسع واضح في فريق المبيعات.",
|
||||
"why_it_matters_ar": "التوسع = ميزانية = نافذة شراء ≤30 يوم.",
|
||||
"recommended_action_ar": "رسالة قصيرة تعرض تجربة 7 أيام.",
|
||||
"expected_impact_sar": 18_000,
|
||||
"risk_level": "low",
|
||||
"buttons_ar": ("قبول", "تخطّي", "اكتب رسالة"),
|
||||
},
|
||||
{
|
||||
"type": "revenue_leak",
|
||||
"title_ar": "تسريب إيراد — 7 leads بلا متابعة",
|
||||
"summary_ar": "آخر تواصل قبل 72+ ساعة. الردود تتراجع 14%/ساعة.",
|
||||
"why_it_matters_ar": "الإهمال خسارة pipeline متراكمة.",
|
||||
"recommended_action_ar": "اعتمد 7 follow-ups جاهزة.",
|
||||
"expected_impact_sar": 42_000,
|
||||
"risk_level": "medium",
|
||||
"buttons_ar": ("اعتمد", "عدّل", "تخطّي"),
|
||||
},
|
||||
{
|
||||
"type": "partner_suggestion",
|
||||
"title_ar": "فرصة شراكة — وكالة B2B في جدة",
|
||||
"summary_ar": "عملاؤها يحتاجون lead-gen → Dealix يكمل خدماتها.",
|
||||
"why_it_matters_ar": "الشراكة الواحدة تفتح 3-5 leads warmer.",
|
||||
"recommended_action_ar": "رسالة partnership warm + اقتراح pilot.",
|
||||
"expected_impact_sar": 60_000,
|
||||
"risk_level": "low",
|
||||
"buttons_ar": ("اكتب رسالة", "احجز اجتماع", "تخطّي"),
|
||||
},
|
||||
{
|
||||
"type": "meeting_prep",
|
||||
"title_ar": "اجتماع غداً مع شركة العقار الذهبي",
|
||||
"summary_ar": "جاهز: ملف الشركة + 5 أسئلة + 3 اعتراضات + عرض مناسب.",
|
||||
"why_it_matters_ar": "الاجتماع المُحضَّر يرفع الإغلاق 40%+.",
|
||||
"recommended_action_ar": "افتح التحضير + راجع الأجندة.",
|
||||
"expected_impact_sar": 250_000,
|
||||
"risk_level": "low",
|
||||
"buttons_ar": ("افتح التحضير", "اكتب أجندة", "أرسل تأكيد"),
|
||||
},
|
||||
{
|
||||
"type": "review_response",
|
||||
"title_ar": "تقييم Google جديد — 2 نجوم",
|
||||
"summary_ar": "العميل اشتكى من التأخر في الرد.",
|
||||
"why_it_matters_ar": "تقييم سلبي بدون رد ≤24 ساعة يضرّ السمعة المحلية.",
|
||||
"recommended_action_ar": "اعتذار قصير + طلب تواصل + حل.",
|
||||
"expected_impact_sar": 1_000,
|
||||
"risk_level": "high",
|
||||
"buttons_ar": ("اعتمد الرد", "صعّد للمدير", "تخطّي"),
|
||||
},
|
||||
{
|
||||
"type": "competitive_move",
|
||||
"title_ar": "منافس أطلق pricing جديد",
|
||||
"summary_ar": "خفّضوا 15% على باقة Growth — يستهدفون نفس عملاءك.",
|
||||
"why_it_matters_ar": "الردود السريعة تحفظ الـ pipeline.",
|
||||
"recommended_action_ar": "حملة مضادة + ROI breakdown مقارن.",
|
||||
"expected_impact_sar": 80_000,
|
||||
"risk_level": "medium",
|
||||
"buttons_ar": ("جهّز رد", "نبّه المبيعات", "تخطّي"),
|
||||
},
|
||||
]
|
||||
# Validate constraints
|
||||
for c in cards:
|
||||
assert c["type"] in INTEL_CARD_TYPES
|
||||
assert len(c["buttons_ar"]) <= 3
|
||||
return {
|
||||
"feed_size": len(cards),
|
||||
"cards": cards,
|
||||
"policy_note_ar": "كل كرت عربي + ≤3 buttons + approval-aware.",
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
"""Competitive Move Detector — analyze competitor activity → suggest action."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
MOVE_TYPES: tuple[str, ...] = (
|
||||
"price_change",
|
||||
"new_offer",
|
||||
"hiring",
|
||||
"event",
|
||||
"content_campaign",
|
||||
"rebrand",
|
||||
"funding",
|
||||
"expansion",
|
||||
)
|
||||
|
||||
|
||||
def analyze_competitive_move(
|
||||
*,
|
||||
competitor_name: str,
|
||||
move_type: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Take one observed competitor signal → return Arabic recommended action.
|
||||
|
||||
Pure deterministic; no live competitor scraping.
|
||||
"""
|
||||
p = payload or {}
|
||||
if move_type not in MOVE_TYPES:
|
||||
return {
|
||||
"error": f"unknown move_type: {move_type}",
|
||||
"valid_types": list(MOVE_TYPES),
|
||||
}
|
||||
|
||||
if move_type == "price_change":
|
||||
delta_pct = float(p.get("price_delta_pct", -10))
|
||||
action_ar = (
|
||||
"حملة مضادة + ROI breakdown مقارن — لا تخفّض السعر."
|
||||
if delta_pct < 0 else
|
||||
"ميزة تنافسية: عرضنا أرخص — اطلق ROI proof."
|
||||
)
|
||||
urgency = "high" if abs(delta_pct) >= 15 else "medium"
|
||||
elif move_type == "new_offer":
|
||||
action_ar = (
|
||||
"حلّل العرض الجديد + اقتباس مزاياك المختلفة + offer comparison."
|
||||
)
|
||||
urgency = "medium"
|
||||
elif move_type == "hiring":
|
||||
action_ar = (
|
||||
"إشارة توسع — استهدف نفس عملائهم بعرضك المختلف."
|
||||
)
|
||||
urgency = "low"
|
||||
elif move_type == "event":
|
||||
action_ar = (
|
||||
"حضّر أنت محتوى/ندوة في نفس الفترة — استفد من اهتمام السوق."
|
||||
)
|
||||
urgency = "medium"
|
||||
elif move_type == "content_campaign":
|
||||
action_ar = (
|
||||
"اقرأ زاويتهم + اطلق رد منشور / dialog بحجة مدعومة بأرقام."
|
||||
)
|
||||
urgency = "low"
|
||||
elif move_type == "rebrand":
|
||||
action_ar = "احتفظ بهويتك — أعلن استمرار وعدك للعملاء."
|
||||
urgency = "low"
|
||||
elif move_type == "funding":
|
||||
action_ar = (
|
||||
"إشارة سرعة في السوق — ركّز على retention + speed-to-value."
|
||||
)
|
||||
urgency = "medium"
|
||||
else: # expansion
|
||||
action_ar = "نبّه فريق المبيعات + رسالة احتفاظ للعملاء الكبار."
|
||||
urgency = "medium"
|
||||
|
||||
return {
|
||||
"competitor_name": competitor_name,
|
||||
"move_type": move_type,
|
||||
"urgency": urgency,
|
||||
"recommended_action_ar": action_ar,
|
||||
"next_step_ar": "جهّز draft رد + موافقة المشغّل قبل الإطلاق.",
|
||||
"approval_required": True,
|
||||
"payload_received": p,
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
"""Decision Memory — learn the operator's preferences from Accept/Skip/Edit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
VALID_DECISIONS: tuple[str, ...] = ("accept", "skip", "edit", "block")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecisionMemory:
|
||||
"""Per-customer Accept/Skip/Edit history and aggregates."""
|
||||
|
||||
customer_id: str
|
||||
raw_decisions: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
def append(
|
||||
self,
|
||||
*,
|
||||
decision: str,
|
||||
action_type: str,
|
||||
channel: str,
|
||||
sector: str | None = None,
|
||||
tone: str | None = None,
|
||||
objection_id: str | None = None,
|
||||
) -> None:
|
||||
if decision not in VALID_DECISIONS:
|
||||
raise ValueError(f"unknown decision: {decision}")
|
||||
self.raw_decisions.append({
|
||||
"decision": decision,
|
||||
"action_type": action_type,
|
||||
"channel": channel,
|
||||
"sector": sector,
|
||||
"tone": tone,
|
||||
"objection_id": objection_id,
|
||||
})
|
||||
|
||||
def preferences(self) -> dict[str, Any]:
|
||||
if not self.raw_decisions:
|
||||
return {
|
||||
"samples": 0,
|
||||
"preferred_channels": [],
|
||||
"preferred_tones": [],
|
||||
"preferred_sectors": [],
|
||||
"rejected_action_types": [],
|
||||
"accept_rate": 0.0,
|
||||
}
|
||||
ch_counter: Counter[str] = Counter()
|
||||
tone_counter: Counter[str] = Counter()
|
||||
sector_counter: Counter[str] = Counter()
|
||||
rejected: Counter[str] = Counter()
|
||||
accepts = 0
|
||||
for d in self.raw_decisions:
|
||||
if d["decision"] == "accept":
|
||||
accepts += 1
|
||||
ch_counter[d.get("channel", "")] += 1
|
||||
if d.get("tone"):
|
||||
tone_counter[d["tone"]] += 1
|
||||
if d.get("sector"):
|
||||
sector_counter[d["sector"]] += 1
|
||||
elif d["decision"] in ("skip", "block"):
|
||||
rejected[d.get("action_type", "")] += 1
|
||||
return {
|
||||
"samples": len(self.raw_decisions),
|
||||
"preferred_channels": [c for c, _ in ch_counter.most_common(3)],
|
||||
"preferred_tones": [t for t, _ in tone_counter.most_common(2)],
|
||||
"preferred_sectors": [s for s, _ in sector_counter.most_common(3)],
|
||||
"rejected_action_types": [a for a, _ in rejected.most_common(3) if a],
|
||||
"accept_rate": round(accepts / len(self.raw_decisions), 4),
|
||||
}
|
||||
|
||||
|
||||
def learn_from_decision(
|
||||
*,
|
||||
memory: DecisionMemory,
|
||||
decision: str,
|
||||
action_type: str,
|
||||
channel: str,
|
||||
sector: str | None = None,
|
||||
tone: str | None = None,
|
||||
objection_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Record a decision + return updated preferences."""
|
||||
memory.append(
|
||||
decision=decision, action_type=action_type, channel=channel,
|
||||
sector=sector, tone=tone, objection_id=objection_id,
|
||||
)
|
||||
return {
|
||||
"customer_id": memory.customer_id,
|
||||
"added": True,
|
||||
"preferences": memory.preferences(),
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
"""Growth Brain — per-customer context + preferences + priorities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrowthBrain:
|
||||
"""The customer's growth context as a single object."""
|
||||
|
||||
customer_id: str
|
||||
company_context: dict[str, Any]
|
||||
channels_connected: tuple[str, ...]
|
||||
target_segments: tuple[str, ...]
|
||||
approved_actions: tuple[str, ...]
|
||||
blocked_actions: tuple[str, ...]
|
||||
growth_priorities: tuple[str, ...]
|
||||
risk_tolerance: str = "medium" # low / medium / high
|
||||
preferred_tone: str = "warm" # formal / warm / direct
|
||||
accept_rate_30d: float = 0.0
|
||||
avg_response_minutes: int = 0
|
||||
learning_signal_count: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"customer_id": self.customer_id,
|
||||
"company_context": self.company_context,
|
||||
"channels_connected": list(self.channels_connected),
|
||||
"target_segments": list(self.target_segments),
|
||||
"approved_actions": list(self.approved_actions),
|
||||
"blocked_actions": list(self.blocked_actions),
|
||||
"growth_priorities": list(self.growth_priorities),
|
||||
"risk_tolerance": self.risk_tolerance,
|
||||
"preferred_tone": self.preferred_tone,
|
||||
"accept_rate_30d": self.accept_rate_30d,
|
||||
"avg_response_minutes": self.avg_response_minutes,
|
||||
"learning_signal_count": self.learning_signal_count,
|
||||
}
|
||||
|
||||
def is_ready_for_autopilot(self) -> bool:
|
||||
"""≥30 learned signals + ≥40% accept rate + non-empty channels."""
|
||||
return (
|
||||
self.learning_signal_count >= 30
|
||||
and self.accept_rate_30d >= 0.40
|
||||
and len(self.channels_connected) > 0
|
||||
)
|
||||
|
||||
|
||||
def build_growth_brain(payload: dict[str, Any] | None = None) -> GrowthBrain:
|
||||
"""Build a brain from a customer payload — sane Saudi-B2B defaults."""
|
||||
p = payload or {}
|
||||
return GrowthBrain(
|
||||
customer_id=str(p.get("customer_id") or "demo"),
|
||||
company_context={
|
||||
"company_name": p.get("company_name", "Demo Saudi B2B Co."),
|
||||
"sector": p.get("sector", "real_estate"),
|
||||
"city": p.get("city", "الرياض"),
|
||||
"offer_one_liner": p.get("offer_one_liner", "تشغيل نمو B2B سعودي"),
|
||||
"ideal_customer": p.get("ideal_customer", "شركات SMB سعودية"),
|
||||
"average_deal_size_sar": float(p.get("average_deal_size_sar", 25_000)),
|
||||
},
|
||||
channels_connected=tuple(p.get("channels_connected", ("whatsapp",))),
|
||||
target_segments=tuple(p.get("target_segments", ("inbound_lead", "existing_customer"))),
|
||||
approved_actions=tuple(p.get("approved_actions", (
|
||||
"create_draft", "send_with_approval", "ingest_lead",
|
||||
))),
|
||||
blocked_actions=tuple(p.get("blocked_actions", (
|
||||
"cold_send_without_consent", "charge_card_without_user_action",
|
||||
))),
|
||||
growth_priorities=tuple(p.get("growth_priorities", (
|
||||
"fill_pipeline", "improve_response_time", "build_partner_channel",
|
||||
))),
|
||||
risk_tolerance=p.get("risk_tolerance", "medium"),
|
||||
preferred_tone=p.get("preferred_tone", "warm"),
|
||||
accept_rate_30d=float(p.get("accept_rate_30d", 0.0)),
|
||||
avg_response_minutes=int(p.get("avg_response_minutes", 0)),
|
||||
learning_signal_count=int(p.get("learning_signal_count", 0)),
|
||||
)
|
||||
@ -0,0 +1,114 @@
|
||||
"""Intelligence Mission Engine — 7 outcome-shaped growth missions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.intelligence_layer.growth_brain import GrowthBrain
|
||||
|
||||
|
||||
INTEL_MISSIONS: tuple[dict[str, Any], ...] = (
|
||||
{
|
||||
"id": "first_10_opportunities",
|
||||
"title_ar": "10 فرص في 10 دقائق",
|
||||
"goal_ar": "اكتشاف 10 شركات سعودية + رسائل عربية + موافقة + متابعة أسبوع.",
|
||||
"kill_metric": "ten_drafts_approved",
|
||||
"required_integrations": ("whatsapp",),
|
||||
"safety_rules_ar": ("لا cold WhatsApp بدون lawful basis",),
|
||||
"success_metrics": ("approve_rate ≥ 50%", "first_reply ≤ 24h"),
|
||||
},
|
||||
{
|
||||
"id": "revenue_leak_rescue",
|
||||
"title_ar": "أنقذ الإيراد الضائع",
|
||||
"goal_ar": "اقرأ Email/CRM/WhatsApp → استخرج leads ضائعة → drafts متابعة.",
|
||||
"kill_metric": "leads_revived",
|
||||
"required_integrations": ("gmail", "crm"),
|
||||
"safety_rules_ar": ("approval لكل follow-up",),
|
||||
"success_metrics": ("rescued_leads ≥ 5", "rescued_pipeline_sar ≥ 30000"),
|
||||
},
|
||||
{
|
||||
"id": "partnership_sprint",
|
||||
"title_ar": "ابدأ قناة شراكات",
|
||||
"goal_ar": "تحديد + التواصل مع 5 شركاء محتملين خلال 14 يوم.",
|
||||
"kill_metric": "partner_intros_replied",
|
||||
"required_integrations": ("gmail", "google_calendar"),
|
||||
"safety_rules_ar": ("لا outreach شخصي بدون warm context",),
|
||||
"success_metrics": ("intros_replied ≥ 2", "first_partner_meeting ≤ 14d"),
|
||||
},
|
||||
{
|
||||
"id": "customer_reactivation",
|
||||
"title_ar": "استرجع العملاء الخاملين",
|
||||
"goal_ar": "ارفع قائمة قدامى → صنّفهم → رسائل عودة بـ payment link.",
|
||||
"kill_metric": "reactivated_customers",
|
||||
"required_integrations": ("whatsapp", "moyasar"),
|
||||
"safety_rules_ar": ("Opt-in موثق فقط",),
|
||||
"success_metrics": ("reactivated ≥ 10", "revenue_sar ≥ 25000"),
|
||||
},
|
||||
{
|
||||
"id": "meeting_booking_sprint",
|
||||
"title_ar": "احجز 3 اجتماعات",
|
||||
"goal_ar": "Top-10 leads → agenda → موافقة → calendar drafts.",
|
||||
"kill_metric": "meetings_confirmed",
|
||||
"required_integrations": ("google_calendar", "whatsapp"),
|
||||
"safety_rules_ar": ("لا insert بدون OAuth + ضغطة المستخدم",),
|
||||
"success_metrics": ("meetings_confirmed ≥ 3 / 5d",),
|
||||
},
|
||||
{
|
||||
"id": "ai_visibility_sprint",
|
||||
"title_ar": "AEO Sprint — اظهر في إجابات AI",
|
||||
"goal_ar": "تحليل ظهور الشركة + خطة محتوى 30 يوم لـ ChatGPT/Gemini/Perplexity.",
|
||||
"kill_metric": "questions_visible",
|
||||
"required_integrations": ("google_business_profile",),
|
||||
"safety_rules_ar": ("لا scraping خارج المسموح",),
|
||||
"success_metrics": ("question_visibility_lift ≥ 30%",),
|
||||
},
|
||||
{
|
||||
"id": "competitive_response",
|
||||
"title_ar": "الرد على حركة منافس",
|
||||
"goal_ar": "رصد price change/offer/hiring → ردود + حملات + ROI breakdown.",
|
||||
"kill_metric": "competitor_signals_resolved",
|
||||
"required_integrations": (),
|
||||
"safety_rules_ar": ("لا تشهير", "لا اتهام عام",),
|
||||
"success_metrics": ("retention_lift", "win_rate_lift"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def list_intel_missions() -> dict[str, Any]:
|
||||
return {
|
||||
"count": len(INTEL_MISSIONS),
|
||||
"missions": list(INTEL_MISSIONS),
|
||||
"kill_feature_id": "first_10_opportunities",
|
||||
}
|
||||
|
||||
|
||||
def recommend_missions(brain: GrowthBrain | None = None, *, limit: int = 3) -> dict[str, Any]:
|
||||
"""Pick top-N missions for this customer based on brain state."""
|
||||
if brain is None:
|
||||
recommended = list(INTEL_MISSIONS)[:limit]
|
||||
else:
|
||||
# Simple heuristic: kill feature first, then prioritize by integrations
|
||||
ranked: list[tuple[dict, float]] = []
|
||||
for m in INTEL_MISSIONS:
|
||||
score = 50.0
|
||||
if m["id"] == "first_10_opportunities":
|
||||
score += 50 # always priority for new customers
|
||||
req = set(m["required_integrations"])
|
||||
connected = set(brain.channels_connected)
|
||||
if req.issubset(connected):
|
||||
score += 20
|
||||
else:
|
||||
score -= 10 * (len(req - connected))
|
||||
if "fill_pipeline" in brain.growth_priorities and m["id"] in (
|
||||
"first_10_opportunities", "revenue_leak_rescue"
|
||||
):
|
||||
score += 15
|
||||
if "build_partner_channel" in brain.growth_priorities and m["id"] == "partnership_sprint":
|
||||
score += 15
|
||||
ranked.append((m, score))
|
||||
ranked.sort(key=lambda x: x[1], reverse=True)
|
||||
recommended = [m for m, _ in ranked[:limit]]
|
||||
return {
|
||||
"recommended": recommended,
|
||||
"rationale_ar": "تم الترتيب حسب priorities العميل + القنوات المربوطة.",
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
"""Opportunity Simulator — forward simulation before sending."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
# Sector benchmarks (anchored to Saudi B2B Pulse figures)
|
||||
SECTOR_RATES: dict[str, dict[str, float]] = {
|
||||
"real_estate": {"reply": 0.074, "meeting": 0.32, "win": 0.18},
|
||||
"clinics": {"reply": 0.138, "meeting": 0.40, "win": 0.28},
|
||||
"logistics": {"reply": 0.068, "meeting": 0.30, "win": 0.22},
|
||||
"hospitality": {"reply": 0.124, "meeting": 0.38, "win": 0.24},
|
||||
"restaurants": {"reply": 0.115, "meeting": 0.42, "win": 0.30},
|
||||
"training": {"reply": 0.112, "meeting": 0.36, "win": 0.25},
|
||||
"agencies": {"reply": 0.059, "meeting": 0.28, "win": 0.20},
|
||||
"construction": {"reply": 0.032, "meeting": 0.25, "win": 0.15},
|
||||
"saas": {"reply": 0.047, "meeting": 0.30, "win": 0.20},
|
||||
}
|
||||
|
||||
|
||||
def simulate_opportunity(
|
||||
*,
|
||||
target_count: int,
|
||||
sector: str = "saas",
|
||||
avg_deal_value_sar: float = 25_000,
|
||||
channel: str = "whatsapp",
|
||||
cold_pct: float = 0.0,
|
||||
quality_lift: float = 1.0, # multiplier (Dealix lift on baseline)
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Forward-simulate a campaign before launching.
|
||||
|
||||
Returns expected replies / meetings / pipeline + risk flags.
|
||||
"""
|
||||
rates = SECTOR_RATES.get(sector.lower(), SECTOR_RATES["saas"])
|
||||
|
||||
# Channel adjustment
|
||||
if channel == "whatsapp":
|
||||
reply_rate = rates["reply"] * 1.6 * quality_lift
|
||||
elif channel == "email":
|
||||
reply_rate = rates["reply"] * 0.9 * quality_lift
|
||||
else:
|
||||
reply_rate = rates["reply"] * quality_lift
|
||||
|
||||
# Cold contacts hurt the rate dramatically
|
||||
cold_pct = max(0.0, min(1.0, cold_pct))
|
||||
if cold_pct > 0:
|
||||
reply_rate *= max(0.10, 1.0 - cold_pct * 0.85)
|
||||
|
||||
expected_replies = round(target_count * reply_rate)
|
||||
expected_meetings = round(expected_replies * rates["meeting"])
|
||||
expected_deals = round(expected_meetings * rates["win"])
|
||||
expected_pipeline = expected_deals * avg_deal_value_sar
|
||||
|
||||
# Risk flags
|
||||
risks: list[str] = []
|
||||
if cold_pct >= 0.5:
|
||||
risks.append("نسبة cold عالية — احتمال opt-out مرتفع.")
|
||||
if channel == "whatsapp" and cold_pct > 0:
|
||||
risks.append("WhatsApp + cold = خطر PDPL — راجع الـ contactability.")
|
||||
if target_count > 500 and channel == "whatsapp":
|
||||
risks.append("حملة WhatsApp كبيرة — اعتمد على templates معتمدة.")
|
||||
|
||||
risk_score = min(100, int(50 + cold_pct * 50 + (10 if target_count > 500 else 0)))
|
||||
|
||||
return {
|
||||
"inputs": {
|
||||
"target_count": target_count,
|
||||
"sector": sector,
|
||||
"avg_deal_value_sar": avg_deal_value_sar,
|
||||
"channel": channel,
|
||||
"cold_pct": cold_pct,
|
||||
"quality_lift": quality_lift,
|
||||
},
|
||||
"rates_used": rates,
|
||||
"expected_replies": expected_replies,
|
||||
"expected_meetings": expected_meetings,
|
||||
"expected_deals": expected_deals,
|
||||
"expected_pipeline_sar": expected_pipeline,
|
||||
"risk_score": risk_score,
|
||||
"risks_ar": risks,
|
||||
"recommendation_ar": (
|
||||
"ابدأ بالـ safe-only segment + معدّل أسبوعي محدود."
|
||||
if risk_score >= 50
|
||||
else "آمن للإطلاق بعد approval."
|
||||
),
|
||||
"approval_required": True,
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
"""Revenue DNA — extract the company's growth fingerprint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
|
||||
def extract_revenue_dna(
|
||||
*,
|
||||
customer_id: str,
|
||||
won_deals: list[dict[str, Any]] | None = None,
|
||||
replies: list[dict[str, Any]] | None = None,
|
||||
objections: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Compute the customer's growth DNA.
|
||||
|
||||
Inputs are optional; missing inputs return sensible defaults
|
||||
so the dashboard always has something to render.
|
||||
"""
|
||||
won_deals = won_deals or []
|
||||
replies = replies or []
|
||||
objections = objections or []
|
||||
|
||||
# Best channel = channel that produced the most won_deals
|
||||
chan_counter: Counter[str] = Counter()
|
||||
seg_counter: Counter[str] = Counter()
|
||||
angle_counter: Counter[str] = Counter()
|
||||
cycle_days: list[float] = []
|
||||
for d in won_deals:
|
||||
chan_counter[d.get("channel", "?")] += 1
|
||||
seg_counter[d.get("segment", "?")] += 1
|
||||
angle_counter[d.get("message_angle", "?")] += 1
|
||||
if "cycle_days" in d:
|
||||
cycle_days.append(float(d["cycle_days"]))
|
||||
|
||||
# Common objection
|
||||
obj_counter: Counter[str] = Counter()
|
||||
for o in objections:
|
||||
obj_counter[o.get("objection_id", "?")] += 1
|
||||
|
||||
next_experiment_ar = (
|
||||
"اختبر رسالة قصيرة (≤4 سطور) + CTA واحد على القناة الأنجح."
|
||||
if len(won_deals) >= 5 else
|
||||
"ركّز على بناء أول 10 deals عبر «10 فرص في 10 دقائق»."
|
||||
)
|
||||
|
||||
return {
|
||||
"customer_id": customer_id,
|
||||
"best_channel": (chan_counter.most_common(1)[0][0] if chan_counter else "whatsapp"),
|
||||
"best_segment": (seg_counter.most_common(1)[0][0] if seg_counter else "inbound_lead"),
|
||||
"best_message_angle": (
|
||||
angle_counter.most_common(1)[0][0] if angle_counter
|
||||
else "value_first_short_arabic"
|
||||
),
|
||||
"common_objection": (obj_counter.most_common(1)[0][0] if obj_counter else "send_offer_whatsapp"),
|
||||
"fastest_conversion_days": round(
|
||||
min(cycle_days) if cycle_days else 0, 1
|
||||
),
|
||||
"median_conversion_days": round(
|
||||
sorted(cycle_days)[len(cycle_days) // 2] if cycle_days else 0, 1
|
||||
),
|
||||
"deals_observed": len(won_deals),
|
||||
"next_experiment_ar": next_experiment_ar,
|
||||
}
|
||||
|
||||
|
||||
def build_revenue_dna_demo() -> dict[str, Any]:
|
||||
"""Demo Revenue DNA with realistic Saudi B2B values."""
|
||||
return extract_revenue_dna(
|
||||
customer_id="demo",
|
||||
won_deals=[
|
||||
{"channel": "whatsapp", "segment": "inbound_lead",
|
||||
"message_angle": "value_first_short_arabic", "cycle_days": 18},
|
||||
{"channel": "whatsapp", "segment": "existing_customer",
|
||||
"message_angle": "expansion_offer", "cycle_days": 12},
|
||||
{"channel": "email", "segment": "referral",
|
||||
"message_angle": "warm_intro", "cycle_days": 25},
|
||||
{"channel": "whatsapp", "segment": "event_lead",
|
||||
"message_angle": "value_first_short_arabic", "cycle_days": 30},
|
||||
{"channel": "whatsapp", "segment": "inbound_lead",
|
||||
"message_angle": "value_first_short_arabic", "cycle_days": 15},
|
||||
],
|
||||
objections=[
|
||||
{"objection_id": "send_offer_whatsapp"},
|
||||
{"objection_id": "send_offer_whatsapp"},
|
||||
{"objection_id": "price_high"},
|
||||
],
|
||||
)
|
||||
102
dealix/auto_client_acquisition/intelligence_layer/trust_score.py
Normal file
102
dealix/auto_client_acquisition/intelligence_layer/trust_score.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Trust Score — composite per-action verdict before execution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrustVerdict:
|
||||
"""Output of compute_trust_score."""
|
||||
|
||||
verdict: str # safe / needs_review / blocked
|
||||
score: int # 0-100 (higher = safer)
|
||||
reasons_ar: list[str]
|
||||
fixes_ar: list[str]
|
||||
|
||||
|
||||
def compute_trust_score(
|
||||
*,
|
||||
source_quality: str = "unknown", # public / partner / customer / cold / unknown
|
||||
opt_in: bool = False,
|
||||
channel: str = "whatsapp",
|
||||
message_text: str = "",
|
||||
frequency_count_this_week: int = 0,
|
||||
weekly_cap: int = 2,
|
||||
approval_status: str = "pending",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Composite trust verdict on a proposed action.
|
||||
|
||||
Pure deterministic — same inputs → same verdict.
|
||||
"""
|
||||
score = 100
|
||||
reasons: list[str] = []
|
||||
fixes: list[str] = []
|
||||
|
||||
# 1. Source quality
|
||||
src_penalty = {
|
||||
"customer": 0,
|
||||
"partner": -5,
|
||||
"public": -10,
|
||||
"unknown": -25,
|
||||
"cold": -40,
|
||||
}.get(source_quality, -20)
|
||||
score += src_penalty
|
||||
if src_penalty <= -25:
|
||||
reasons.append(f"جودة المصدر منخفضة ({source_quality}).")
|
||||
fixes.append("وثّق lawful basis قبل أي تواصل.")
|
||||
|
||||
# 2. Opt-in
|
||||
if not opt_in and channel == "whatsapp":
|
||||
score -= 30
|
||||
reasons.append("لا opt-in على قناة WhatsApp.")
|
||||
fixes.append("احصل على opt-in صريح أو حوّل القناة للإيميل.")
|
||||
|
||||
# 3. Channel risk
|
||||
if channel in ("whatsapp", "instagram_graph"):
|
||||
score -= 5 # consumer-facing channels need extra care
|
||||
elif channel == "x_api":
|
||||
score -= 10 # public broadcast risk
|
||||
|
||||
# 4. Message risk — banned phrases
|
||||
risky_phrases = ("ضمان 100", "نتائج مضمونة", "آخر فرصة", "اضغط هنا فوراً")
|
||||
found = [p for p in risky_phrases if p in (message_text or "")]
|
||||
if found:
|
||||
score -= 15 * len(found)
|
||||
reasons.append(f"عبارات محظورة: {found}")
|
||||
fixes.append("احذف العبارات المبالغة قبل الإرسال.")
|
||||
|
||||
# 5. Frequency cap
|
||||
if frequency_count_this_week >= weekly_cap:
|
||||
score -= 20
|
||||
reasons.append(f"تجاوز السقف الأسبوعي ({frequency_count_this_week}/{weekly_cap}).")
|
||||
fixes.append("انتظر بداية الأسبوع التالي.")
|
||||
|
||||
# 6. Approval gate
|
||||
if approval_status == "pending":
|
||||
score -= 10
|
||||
reasons.append("لم يصل approval المشغّل بعد.")
|
||||
fixes.append("اطلب موافقة المشغّل.")
|
||||
|
||||
score = max(0, min(100, score))
|
||||
|
||||
if score >= 70:
|
||||
verdict = "safe"
|
||||
elif score >= 40:
|
||||
verdict = "needs_review"
|
||||
else:
|
||||
verdict = "blocked"
|
||||
|
||||
if not reasons:
|
||||
reasons = ["كل القواعد مستوفاة."]
|
||||
if not fixes and verdict == "safe":
|
||||
fixes = ["جاهز للتنفيذ بعد approval إذا لزم."]
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"score": score,
|
||||
"reasons_ar": reasons,
|
||||
"fixes_ar": fixes,
|
||||
}
|
||||
74
dealix/auto_client_acquisition/platform_services/__init__.py
Normal file
74
dealix/auto_client_acquisition/platform_services/__init__.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""
|
||||
Platform Services Layer — Dealix's Growth Control Tower spine.
|
||||
|
||||
Turns the platform from "WhatsApp Growth Operator" into a multi-channel
|
||||
growth platform that ingests events from every channel a Saudi B2B uses,
|
||||
converts them into Arabic action cards, evaluates each action against
|
||||
policy, and produces unified proof.
|
||||
|
||||
Modules:
|
||||
- event_bus : typed events from all channels
|
||||
- identity_resolution : reconcile phone+email+social→one person
|
||||
- channel_registry : 11 supported channels with capabilities
|
||||
- action_policy : decide approval / block / allow
|
||||
- tool_gateway : draft-only proxy (no live actions here)
|
||||
- unified_inbox : 8 card types from events
|
||||
- action_ledger : auditable record of every action lifecycle
|
||||
- proof_ledger : value rolled up across the platform
|
||||
- service_catalog : 12 sellable services
|
||||
"""
|
||||
|
||||
from auto_client_acquisition.platform_services.action_ledger import (
|
||||
ActionLedger,
|
||||
LedgerEntry,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.action_policy import (
|
||||
POLICY_RULES,
|
||||
PolicyDecision,
|
||||
evaluate_action,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.channel_registry import (
|
||||
ALL_CHANNELS,
|
||||
Channel,
|
||||
get_channel,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.event_bus import (
|
||||
EVENT_TYPES,
|
||||
PlatformEvent,
|
||||
make_event,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.identity_resolution import (
|
||||
Identity,
|
||||
resolve_identity,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.proof_ledger import (
|
||||
PlatformProofLedger,
|
||||
build_demo_platform_proof,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.service_catalog import (
|
||||
SELLABLE_SERVICES,
|
||||
ServiceOffering,
|
||||
list_services,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.tool_gateway import (
|
||||
GatewayResult,
|
||||
invoke_tool,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.unified_inbox import (
|
||||
CARD_TYPES,
|
||||
InboxCard,
|
||||
build_card_from_event,
|
||||
build_demo_feed,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"EVENT_TYPES", "PlatformEvent", "make_event",
|
||||
"Identity", "resolve_identity",
|
||||
"ALL_CHANNELS", "Channel", "get_channel",
|
||||
"POLICY_RULES", "PolicyDecision", "evaluate_action",
|
||||
"GatewayResult", "invoke_tool",
|
||||
"CARD_TYPES", "InboxCard", "build_card_from_event", "build_demo_feed",
|
||||
"ActionLedger", "LedgerEntry",
|
||||
"PlatformProofLedger", "build_demo_platform_proof",
|
||||
"SELLABLE_SERVICES", "ServiceOffering", "list_services",
|
||||
]
|
||||
@ -0,0 +1,107 @@
|
||||
"""
|
||||
Action Ledger — auditable record of every action lifecycle.
|
||||
|
||||
Stage transitions per action: requested → (approved | rejected | blocked)
|
||||
→ executed → outcome.
|
||||
|
||||
Used for SDAIA / DPO inspections + customer's own audit trail.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
VALID_STAGES: tuple[str, ...] = (
|
||||
"requested", "approved", "rejected", "blocked",
|
||||
"executed", "outcome_recorded",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
"""One entry in the action ledger."""
|
||||
|
||||
entry_id: str
|
||||
customer_id: str
|
||||
action_type: str
|
||||
channel: str
|
||||
stage: str
|
||||
actor: str = "system"
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
reason_ar: str = ""
|
||||
created_at: datetime = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
)
|
||||
correlation_id: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"entry_id": self.entry_id,
|
||||
"customer_id": self.customer_id,
|
||||
"action_type": self.action_type,
|
||||
"channel": self.channel,
|
||||
"stage": self.stage,
|
||||
"actor": self.actor,
|
||||
"payload": self.payload,
|
||||
"reason_ar": self.reason_ar,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"correlation_id": self.correlation_id,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionLedger:
|
||||
"""Append-only ledger keyed by customer_id."""
|
||||
|
||||
entries: list[LedgerEntry] = field(default_factory=list)
|
||||
|
||||
def append(
|
||||
self,
|
||||
*,
|
||||
customer_id: str,
|
||||
action_type: str,
|
||||
channel: str,
|
||||
stage: str,
|
||||
actor: str = "system",
|
||||
payload: dict[str, Any] | None = None,
|
||||
reason_ar: str = "",
|
||||
correlation_id: str | None = None,
|
||||
) -> LedgerEntry:
|
||||
if stage not in VALID_STAGES:
|
||||
raise ValueError(f"unknown stage: {stage}")
|
||||
entry = LedgerEntry(
|
||||
entry_id=f"led_{uuid.uuid4().hex[:20]}",
|
||||
customer_id=customer_id,
|
||||
action_type=action_type,
|
||||
channel=channel,
|
||||
stage=stage,
|
||||
actor=actor,
|
||||
payload=payload or {},
|
||||
reason_ar=reason_ar,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
self.entries.append(entry)
|
||||
return entry
|
||||
|
||||
def for_customer(self, customer_id: str) -> list[LedgerEntry]:
|
||||
return [e for e in self.entries if e.customer_id == customer_id]
|
||||
|
||||
def summary(self, customer_id: str | None = None) -> dict[str, Any]:
|
||||
pool = self.entries if customer_id is None else self.for_customer(customer_id)
|
||||
by_stage: dict[str, int] = {}
|
||||
by_channel: dict[str, int] = {}
|
||||
by_action: dict[str, int] = {}
|
||||
for e in pool:
|
||||
by_stage[e.stage] = by_stage.get(e.stage, 0) + 1
|
||||
by_channel[e.channel] = by_channel.get(e.channel, 0) + 1
|
||||
by_action[e.action_type] = by_action.get(e.action_type, 0) + 1
|
||||
return {
|
||||
"total": len(pool),
|
||||
"by_stage": by_stage,
|
||||
"by_channel": by_channel,
|
||||
"by_action_type": by_action,
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
"""
|
||||
Action Policy Engine — decides whether an action can run, needs approval,
|
||||
or is blocked. The single chokepoint that protects the customer's
|
||||
reputation + enforces PDPL.
|
||||
|
||||
Design: pure deterministic rules. Easily testable, easily auditable,
|
||||
easy for the customer to explain to compliance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ── Policy rules — each rule is (action_type, condition, decision, reason_ar)
|
||||
POLICY_RULES: list[dict[str, Any]] = [
|
||||
# Hard blocks — never executed
|
||||
{
|
||||
"rule_id": "block_cold_whatsapp",
|
||||
"action": "send_whatsapp",
|
||||
"when": {"source": "cold_list", "consent": False},
|
||||
"decision": "blocked",
|
||||
"reason_ar": "WhatsApp البارد محظور بدون lawful basis (PDPL م.5).",
|
||||
},
|
||||
{
|
||||
"rule_id": "block_payment_no_confirm",
|
||||
"action": "charge_payment",
|
||||
"when": {"user_confirmed": False},
|
||||
"decision": "blocked",
|
||||
"reason_ar": "الخصم يحتاج تأكيد المستخدم على Moyasar — لا charge مباشر.",
|
||||
},
|
||||
{
|
||||
"rule_id": "block_secrets_in_payload",
|
||||
"action": "*",
|
||||
"when": {"payload_contains_secret": True},
|
||||
"decision": "blocked",
|
||||
"reason_ar": "تم اكتشاف secret في الـ payload — حماية تلقائية.",
|
||||
},
|
||||
# Approval gates — must pass through human
|
||||
{
|
||||
"rule_id": "external_send_needs_approval",
|
||||
"action": "send_whatsapp,send_email,send_inmail,post_social",
|
||||
"when": {"approval_status": "pending"},
|
||||
"decision": "approval_required",
|
||||
"reason_ar": "كل إرسال خارجي يحتاج موافقة العميل قبل التنفيذ.",
|
||||
},
|
||||
{
|
||||
"rule_id": "calendar_insert_needs_approval",
|
||||
"action": "calendar_insert_event",
|
||||
"when": {"approval_status": "pending"},
|
||||
"decision": "approval_required",
|
||||
"reason_ar": "إنشاء اجتماع في تقويم العميل يحتاج موافقة قبل insert.",
|
||||
},
|
||||
{
|
||||
"rule_id": "social_dm_needs_explicit",
|
||||
"action": "send_social_dm",
|
||||
"when": {"explicit_permission": False},
|
||||
"decision": "approval_required",
|
||||
"reason_ar": "DM السوشيال يحتاج إذن صريح لكل حساب.",
|
||||
},
|
||||
# Needs review
|
||||
{
|
||||
"rule_id": "unknown_source_review",
|
||||
"action": "*",
|
||||
"when": {"source": "unknown"},
|
||||
"decision": "approval_required",
|
||||
"reason_ar": "مصدر البيانات غير محدد — يحتاج توثيق lawful basis.",
|
||||
},
|
||||
{
|
||||
"rule_id": "high_value_deal_review",
|
||||
"action": "*",
|
||||
"when": {"deal_value_sar_gte": 100_000},
|
||||
"decision": "approval_required",
|
||||
"reason_ar": "صفقة قيمتها ≥100K ريال — راجعها قبل التنفيذ.",
|
||||
},
|
||||
# Allowed (default for safe paths)
|
||||
{
|
||||
"rule_id": "draft_only_safe",
|
||||
"action": "create_draft,read_data,classify_reply",
|
||||
"when": {},
|
||||
"decision": "allow",
|
||||
"reason_ar": "إجراء داخلي آمن — لا يخرج للعميل النهائي.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyDecision:
|
||||
"""Output of evaluate_action."""
|
||||
|
||||
decision: str # allow / approval_required / blocked
|
||||
matched_rule_id: str | None
|
||||
reasons_ar: list[str] = field(default_factory=list)
|
||||
suggested_next_action_ar: str = ""
|
||||
|
||||
|
||||
def evaluate_action(
|
||||
*,
|
||||
action: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> PolicyDecision:
|
||||
"""
|
||||
Evaluate a proposed action against the policy rules.
|
||||
|
||||
First matching rule wins. Default: needs_review (defensive).
|
||||
"""
|
||||
ctx = context or {}
|
||||
matched_reasons: list[str] = []
|
||||
final_decision = "allow"
|
||||
matched_rule_id: str | None = None
|
||||
next_action = "ready_for_execution"
|
||||
|
||||
for rule in POLICY_RULES:
|
||||
# Action match (comma-separated list, "*" = match-any)
|
||||
applicable_actions = rule["action"].split(",") if rule["action"] != "*" else [action]
|
||||
if action not in applicable_actions and rule["action"] != "*":
|
||||
continue
|
||||
|
||||
# Condition match — every key in `when` must match the context
|
||||
when = rule["when"]
|
||||
cond_match = True
|
||||
for k, expected in when.items():
|
||||
if k.endswith("_gte"):
|
||||
attr = k[:-4]
|
||||
if not (float(ctx.get(attr, 0)) >= float(expected)):
|
||||
cond_match = False
|
||||
break
|
||||
elif k == "payload_contains_secret":
|
||||
if expected and not _has_secret_marker(ctx.get("payload", {})):
|
||||
cond_match = False
|
||||
break
|
||||
elif ctx.get(k) != expected:
|
||||
cond_match = False
|
||||
break
|
||||
|
||||
if not cond_match:
|
||||
continue
|
||||
|
||||
decision = rule["decision"]
|
||||
matched_reasons.append(rule["reason_ar"])
|
||||
matched_rule_id = rule["rule_id"]
|
||||
|
||||
if decision == "blocked":
|
||||
return PolicyDecision(
|
||||
decision="blocked",
|
||||
matched_rule_id=matched_rule_id,
|
||||
reasons_ar=matched_reasons,
|
||||
suggested_next_action_ar="معالجة سبب الحظر قبل المحاولة مرة أخرى.",
|
||||
)
|
||||
if decision == "approval_required":
|
||||
final_decision = "approval_required"
|
||||
next_action = "operator_approves_then_execute"
|
||||
# 'allow' rules just confirm — keep looking for stricter rule
|
||||
|
||||
return PolicyDecision(
|
||||
decision=final_decision,
|
||||
matched_rule_id=matched_rule_id,
|
||||
reasons_ar=matched_reasons or ["لا قاعدة مطابقة — الإجراء آمن افتراضياً."],
|
||||
suggested_next_action_ar=next_action,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────
|
||||
_SECRET_MARKERS = ("api_key", "secret_key", "private_key", "password", "ghp_", "sk-ant-", "moyasar_secret")
|
||||
|
||||
|
||||
def _has_secret_marker(payload: dict[str, Any]) -> bool:
|
||||
"""Cheap heuristic check — production pairs this with a stronger scanner."""
|
||||
if not isinstance(payload, dict):
|
||||
return False
|
||||
flat = str(payload).lower()
|
||||
return any(marker in flat for marker in _SECRET_MARKERS)
|
||||
@ -0,0 +1,213 @@
|
||||
"""
|
||||
Channel Registry — 11 supported channels with capabilities + risk profile.
|
||||
|
||||
Each channel declares: capabilities, beta_status, required_permissions,
|
||||
allowed_actions, blocked_actions, risk_level. Used by the action policy
|
||||
engine and the unified inbox.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Channel:
|
||||
"""A connected channel + what it can / cannot do."""
|
||||
|
||||
key: str
|
||||
label_ar: str
|
||||
label_en: str
|
||||
capabilities: tuple[str, ...]
|
||||
beta_status: str # ga / beta / experimental / planned
|
||||
required_permissions: tuple[str, ...]
|
||||
allowed_actions: tuple[str, ...]
|
||||
blocked_actions: tuple[str, ...]
|
||||
risk_level: str # low / medium / high
|
||||
notes_ar: str = ""
|
||||
|
||||
|
||||
# ── The 11 channels we model ────────────────────────────────────
|
||||
ALL_CHANNELS: tuple[Channel, ...] = (
|
||||
Channel(
|
||||
key="whatsapp",
|
||||
label_ar="واتساب",
|
||||
label_en="WhatsApp Business / Cloud",
|
||||
capabilities=(
|
||||
"inbound_messages", "outbound_template_messages",
|
||||
"interactive_buttons_max_3", "media_send", "opt_out_handling",
|
||||
),
|
||||
beta_status="ga",
|
||||
required_permissions=(
|
||||
"waba_account_id", "phone_number_id", "verified_business",
|
||||
),
|
||||
allowed_actions=("draft_message", "send_with_approval", "track_reply"),
|
||||
blocked_actions=("cold_send_without_consent", "bulk_unsolicited_send"),
|
||||
risk_level="medium",
|
||||
notes_ar="حد 3 buttons تفاعلية. الإرسال البارد محظور بدون lawful basis.",
|
||||
),
|
||||
Channel(
|
||||
key="gmail",
|
||||
label_ar="Gmail (إيميل العميل)",
|
||||
label_en="Gmail OAuth",
|
||||
capabilities=(
|
||||
"create_draft_only", "read_labeled_threads",
|
||||
"list_unsubscribe_header_attached",
|
||||
),
|
||||
beta_status="ga",
|
||||
required_permissions=("gmail.compose",),
|
||||
allowed_actions=("create_draft", "read_thread"),
|
||||
blocked_actions=("send_without_user_click", "delete_messages"),
|
||||
risk_level="low",
|
||||
notes_ar="نكتفي بـ scope `gmail.compose`. المستخدم يضغط Send بنفسه.",
|
||||
),
|
||||
Channel(
|
||||
key="google_calendar",
|
||||
label_ar="Google Calendar",
|
||||
label_en="Google Calendar API",
|
||||
capabilities=(
|
||||
"events_insert_with_meet", "events_list",
|
||||
"rfc5545_recurrence", "asia_riyadh_timezone",
|
||||
),
|
||||
beta_status="ga",
|
||||
required_permissions=("calendar.events",),
|
||||
allowed_actions=("draft_event", "create_event_with_approval"),
|
||||
blocked_actions=("delete_other_attendees_events", "modify_external_events_silently"),
|
||||
risk_level="low",
|
||||
notes_ar="conferenceDataVersion=1 لإضافة Google Meet.",
|
||||
),
|
||||
Channel(
|
||||
key="linkedin_lead_forms",
|
||||
label_ar="LinkedIn Lead Gen Forms",
|
||||
label_en="LinkedIn Lead Gen Forms API",
|
||||
capabilities=(
|
||||
"ingest_leads_from_ads", "hidden_field_tracking",
|
||||
"crm_sync",
|
||||
),
|
||||
beta_status="beta",
|
||||
required_permissions=("r_marketing_leadgen_automation",),
|
||||
allowed_actions=("ingest_lead_form", "trigger_followup_draft"),
|
||||
blocked_actions=("scrape_profiles", "unsolicited_inmails_at_scale"),
|
||||
risk_level="low",
|
||||
notes_ar="مصدر رسمي لـ leads مؤهلة.",
|
||||
),
|
||||
Channel(
|
||||
key="x_api",
|
||||
label_ar="X (Twitter)",
|
||||
label_en="X API v2",
|
||||
capabilities=(
|
||||
"post_tweet", "read_mentions",
|
||||
"user_lookups_basic", "webhooks_account_activity_paid",
|
||||
),
|
||||
beta_status="experimental",
|
||||
required_permissions=("oauth2_user_context",),
|
||||
allowed_actions=("draft_post", "ingest_mention", "draft_dm_reply"),
|
||||
blocked_actions=("auto_dm_strangers", "scrape_user_lists"),
|
||||
risk_level="medium",
|
||||
notes_ar="بعض الـ webhooks Enterprise-only. نقتصر على ما تتيحه الخطة الحالية.",
|
||||
),
|
||||
Channel(
|
||||
key="instagram_graph",
|
||||
label_ar="Instagram (Graph API)",
|
||||
label_en="Instagram Graph API",
|
||||
capabilities=(
|
||||
"read_business_messages", "publish_posts",
|
||||
"read_comments_on_owned_posts",
|
||||
),
|
||||
beta_status="beta",
|
||||
required_permissions=("instagram_basic", "instagram_manage_messages"),
|
||||
allowed_actions=("draft_reply", "ingest_comment", "ingest_dm"),
|
||||
blocked_actions=("auto_dm_strangers", "scrape_unrelated_users"),
|
||||
risk_level="medium",
|
||||
notes_ar="فقط للحسابات Business + ما يخص العميل المتصل.",
|
||||
),
|
||||
Channel(
|
||||
key="google_business_profile",
|
||||
label_ar="Google Business Profile",
|
||||
label_en="Google Business Profile API",
|
||||
capabilities=(
|
||||
"read_reviews", "post_replies",
|
||||
"publish_local_posts", "manage_location_info",
|
||||
),
|
||||
beta_status="ga",
|
||||
required_permissions=("business.manage",),
|
||||
allowed_actions=("draft_review_reply", "draft_local_post"),
|
||||
blocked_actions=("delete_real_reviews"),
|
||||
risk_level="low",
|
||||
notes_ar="مهم للمتاجر والعيادات والفروع — السمعة المحلية.",
|
||||
),
|
||||
Channel(
|
||||
key="google_sheets",
|
||||
label_ar="Google Sheets",
|
||||
label_en="Google Sheets API",
|
||||
capabilities=("read_range", "append_row", "watch_changes"),
|
||||
beta_status="ga",
|
||||
required_permissions=("spreadsheets.readonly", "spreadsheets",),
|
||||
allowed_actions=("import_contacts", "sync_pipeline", "log_actions"),
|
||||
blocked_actions=("delete_user_sheets"),
|
||||
risk_level="low",
|
||||
notes_ar="أداة مفيدة للتكامل مع عمليات العميل اليدوية.",
|
||||
),
|
||||
Channel(
|
||||
key="crm",
|
||||
label_ar="CRM (Zoho/HubSpot/Salla/Odoo)",
|
||||
label_en="CRM via REST/SDK",
|
||||
capabilities=(
|
||||
"deal_sync", "contact_sync", "activity_log",
|
||||
),
|
||||
beta_status="planned",
|
||||
required_permissions=("crm_api_token",),
|
||||
allowed_actions=("read_deals", "update_stage_with_approval"),
|
||||
blocked_actions=("delete_deals_silently"),
|
||||
risk_level="medium",
|
||||
notes_ar="بناء adapter لكل CRM في مرحلة لاحقة.",
|
||||
),
|
||||
Channel(
|
||||
key="moyasar",
|
||||
label_ar="Moyasar (مدفوعات)",
|
||||
label_en="Moyasar Payments",
|
||||
capabilities=(
|
||||
"create_payment_link", "create_invoice",
|
||||
"webhook_paid_failed_refunded", "refund",
|
||||
),
|
||||
beta_status="ga",
|
||||
required_permissions=("publishable_key", "secret_key"),
|
||||
allowed_actions=("draft_payment_link", "send_invoice_email"),
|
||||
blocked_actions=("charge_card_without_user_action"),
|
||||
risk_level="high",
|
||||
notes_ar="بطاقة العميل تُدخَل على Moyasar (PCI-safe). لا تخزين خانات.",
|
||||
),
|
||||
Channel(
|
||||
key="website_forms",
|
||||
label_ar="نماذج الموقع",
|
||||
label_en="Website Forms",
|
||||
capabilities=("ingest_submission", "trigger_workflow"),
|
||||
beta_status="ga",
|
||||
required_permissions=("webhook_endpoint",),
|
||||
allowed_actions=("ingest_lead", "draft_thankyou_message"),
|
||||
blocked_actions=(),
|
||||
risk_level="low",
|
||||
notes_ar="مصدر leads مؤهَّلة بطبيعتها — أساس قانوني واضح.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_channel(key: str) -> Channel | None:
|
||||
for c in ALL_CHANNELS:
|
||||
if c.key == key:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def channels_summary() -> dict[str, Any]:
|
||||
by_status: dict[str, int] = {}
|
||||
by_risk: dict[str, int] = {}
|
||||
for c in ALL_CHANNELS:
|
||||
by_status[c.beta_status] = by_status.get(c.beta_status, 0) + 1
|
||||
by_risk[c.risk_level] = by_risk.get(c.risk_level, 0) + 1
|
||||
return {
|
||||
"total": len(ALL_CHANNELS),
|
||||
"by_beta_status": by_status,
|
||||
"by_risk_level": by_risk,
|
||||
}
|
||||
110
dealix/auto_client_acquisition/platform_services/event_bus.py
Normal file
110
dealix/auto_client_acquisition/platform_services/event_bus.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""
|
||||
Omni-Channel Event Bus — every channel emits typed events here.
|
||||
|
||||
Pure structures + helpers; the actual transport (Redis/Kafka) lives in a
|
||||
production adapter. This module is testable in isolation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ── Event taxonomy ────────────────────────────────────────────────
|
||||
EVENT_TYPES: tuple[str, ...] = (
|
||||
# WhatsApp
|
||||
"whatsapp.message_received",
|
||||
"whatsapp.message_sent",
|
||||
"whatsapp.opt_out",
|
||||
# Email (Gmail or company SMTP)
|
||||
"email.received",
|
||||
"email.draft_created",
|
||||
"email.sent",
|
||||
# Calendar
|
||||
"calendar.meeting_scheduled",
|
||||
"calendar.meeting_held",
|
||||
"calendar.no_show",
|
||||
# Social (X / LinkedIn / Instagram / Facebook)
|
||||
"social.comment_received",
|
||||
"social.dm_received",
|
||||
"social.mention_received",
|
||||
"social.lead_form_submitted",
|
||||
# Website + CRM
|
||||
"lead.form_submitted",
|
||||
"lead.crm_imported",
|
||||
# Payments (Moyasar)
|
||||
"payment.initiated",
|
||||
"payment.paid",
|
||||
"payment.failed",
|
||||
"payment.refunded",
|
||||
# Reviews / reputation (Google Business Profile)
|
||||
"review.created",
|
||||
"review.replied",
|
||||
# Partners
|
||||
"partner.suggested",
|
||||
"partner.intro_made",
|
||||
# Internal lifecycle
|
||||
"action.requested",
|
||||
"action.approved",
|
||||
"action.rejected",
|
||||
"action.executed",
|
||||
"action.blocked",
|
||||
# Sheets / CRM sync
|
||||
"sheet.row_added",
|
||||
"crm.deal_updated",
|
||||
)
|
||||
|
||||
|
||||
# ── Event envelope ────────────────────────────────────────────────
|
||||
@dataclass(frozen=True)
|
||||
class PlatformEvent:
|
||||
"""Immutable platform event."""
|
||||
|
||||
event_id: str
|
||||
event_type: str
|
||||
channel: str # whatsapp / gmail / google_calendar / x / ...
|
||||
customer_id: str
|
||||
occurred_at: datetime
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
correlation_id: str | None = None
|
||||
actor: str = "system"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"event_id": self.event_id,
|
||||
"event_type": self.event_type,
|
||||
"channel": self.channel,
|
||||
"customer_id": self.customer_id,
|
||||
"occurred_at": self.occurred_at.isoformat(),
|
||||
"payload": self.payload,
|
||||
"correlation_id": self.correlation_id,
|
||||
"actor": self.actor,
|
||||
}
|
||||
|
||||
|
||||
def make_event(
|
||||
*,
|
||||
event_type: str,
|
||||
channel: str,
|
||||
customer_id: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
correlation_id: str | None = None,
|
||||
actor: str = "system",
|
||||
occurred_at: datetime | None = None,
|
||||
) -> PlatformEvent:
|
||||
"""Construct a validated event."""
|
||||
if event_type not in EVENT_TYPES:
|
||||
raise ValueError(f"unknown event_type: {event_type}")
|
||||
return PlatformEvent(
|
||||
event_id=f"pevt_{uuid.uuid4().hex[:24]}",
|
||||
event_type=event_type,
|
||||
channel=channel,
|
||||
customer_id=customer_id,
|
||||
occurred_at=occurred_at or datetime.now(timezone.utc).replace(tzinfo=None),
|
||||
payload=payload or {},
|
||||
correlation_id=correlation_id,
|
||||
actor=actor,
|
||||
)
|
||||
@ -0,0 +1,91 @@
|
||||
"""
|
||||
Identity Resolution — reconcile signals from many channels into one Identity.
|
||||
|
||||
Inputs: phone, email, company, social handles, CRM ids.
|
||||
Output: a single Identity record with confidence per matched signal.
|
||||
|
||||
Pure deterministic — production version would hit a graph DB.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Identity:
|
||||
"""A reconciled identity across channels."""
|
||||
|
||||
identity_id: str
|
||||
primary_phone: str | None = None
|
||||
primary_email: str | None = None
|
||||
company: str | None = None
|
||||
crm_id: str | None = None
|
||||
social_handles: dict[str, str] = field(default_factory=dict)
|
||||
confidence: float = 0.0 # 0..1
|
||||
sources: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _hash_id(*parts: str) -> str:
|
||||
"""Deterministic ID from any combination of stable identifiers."""
|
||||
seed = "|".join(p.lower().strip() for p in parts if p)
|
||||
if not seed:
|
||||
return ""
|
||||
h = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
|
||||
return f"id_{h}"
|
||||
|
||||
|
||||
def resolve_identity(*, signals: list[dict[str, Any]]) -> Identity:
|
||||
"""
|
||||
Merge a list of signals (from different channels) into one Identity.
|
||||
|
||||
Each signal can be: {phone, email, company, crm_id, social_handles, source}.
|
||||
"""
|
||||
phones: dict[str, int] = {}
|
||||
emails: dict[str, int] = {}
|
||||
companies: dict[str, int] = {}
|
||||
crm_ids: list[str] = []
|
||||
socials: dict[str, str] = {}
|
||||
sources: list[str] = []
|
||||
|
||||
for s in signals:
|
||||
ph = (s.get("phone") or "").strip()
|
||||
em = (s.get("email") or "").strip().lower()
|
||||
co = (s.get("company") or "").strip()
|
||||
crm = (s.get("crm_id") or "").strip()
|
||||
if ph:
|
||||
phones[ph] = phones.get(ph, 0) + 1
|
||||
if em:
|
||||
emails[em] = emails.get(em, 0) + 1
|
||||
if co:
|
||||
companies[co] = companies.get(co, 0) + 1
|
||||
if crm:
|
||||
crm_ids.append(crm)
|
||||
for k, v in (s.get("social_handles") or {}).items():
|
||||
if k not in socials and v:
|
||||
socials[k] = v
|
||||
if s.get("source"):
|
||||
sources.append(str(s["source"]))
|
||||
|
||||
# Pick most-frequent canonical values
|
||||
primary_phone = max(phones, key=phones.get) if phones else None
|
||||
primary_email = max(emails, key=emails.get) if emails else None
|
||||
company = max(companies, key=companies.get) if companies else None
|
||||
crm_id = crm_ids[0] if crm_ids else None
|
||||
|
||||
# Confidence: proportional to number of independent strong signals
|
||||
strong_signals = sum(1 for x in (primary_phone, primary_email, crm_id) if x)
|
||||
confidence = min(1.0, 0.30 * strong_signals + 0.10 * (1 if socials else 0))
|
||||
|
||||
return Identity(
|
||||
identity_id=_hash_id(primary_phone or "", primary_email or "", crm_id or ""),
|
||||
primary_phone=primary_phone,
|
||||
primary_email=primary_email,
|
||||
company=company,
|
||||
crm_id=crm_id,
|
||||
social_handles=dict(socials),
|
||||
confidence=round(confidence, 3),
|
||||
sources=list(dict.fromkeys(sources)), # dedupe preserve order
|
||||
)
|
||||
@ -0,0 +1,80 @@
|
||||
"""
|
||||
Platform Proof Ledger — value rolled up across the entire platform.
|
||||
|
||||
Tracks: leads, meetings, drafts, sends, payments, revenue influenced,
|
||||
risks blocked, time saved, partner ops. Pure functions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformProofLedger:
|
||||
"""Aggregated platform value over a period."""
|
||||
|
||||
customer_id: str
|
||||
period_label: str
|
||||
leads_created: int = 0
|
||||
meetings_booked: int = 0
|
||||
drafts_approved: int = 0
|
||||
messages_sent: int = 0
|
||||
payments_initiated: int = 0
|
||||
payments_paid: int = 0
|
||||
revenue_influenced_sar: float = 0.0
|
||||
risks_blocked: int = 0
|
||||
time_saved_hours: float = 0.0
|
||||
partner_opportunities: int = 0
|
||||
by_channel: dict[str, dict[str, float]] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"customer_id": self.customer_id,
|
||||
"period_label": self.period_label,
|
||||
"totals": {
|
||||
"leads_created": self.leads_created,
|
||||
"meetings_booked": self.meetings_booked,
|
||||
"drafts_approved": self.drafts_approved,
|
||||
"messages_sent": self.messages_sent,
|
||||
"payments_initiated": self.payments_initiated,
|
||||
"payments_paid": self.payments_paid,
|
||||
"revenue_influenced_sar": self.revenue_influenced_sar,
|
||||
"risks_blocked": self.risks_blocked,
|
||||
"time_saved_hours": self.time_saved_hours,
|
||||
"partner_opportunities": self.partner_opportunities,
|
||||
},
|
||||
"by_channel": self.by_channel,
|
||||
}
|
||||
|
||||
|
||||
def build_demo_platform_proof(
|
||||
*,
|
||||
customer_id: str = "demo",
|
||||
period_label: str = "May 2026",
|
||||
) -> PlatformProofLedger:
|
||||
"""Deterministic demo for the dashboard."""
|
||||
return PlatformProofLedger(
|
||||
customer_id=customer_id,
|
||||
period_label=period_label,
|
||||
leads_created=72,
|
||||
meetings_booked=14,
|
||||
drafts_approved=58,
|
||||
messages_sent=58,
|
||||
payments_initiated=4,
|
||||
payments_paid=3,
|
||||
revenue_influenced_sar=185_000,
|
||||
risks_blocked=21, # cold whatsapp + secrets in payload + opt-out + ...
|
||||
time_saved_hours=42,
|
||||
partner_opportunities=6,
|
||||
by_channel={
|
||||
"whatsapp": {"messages_sent": 33, "replies": 12, "meetings": 5},
|
||||
"gmail": {"drafts": 18, "sent": 18, "replies": 6},
|
||||
"google_calendar": {"events_drafted": 14, "events_inserted": 0},
|
||||
"moyasar": {"links_drafted": 4, "paid": 3},
|
||||
"google_business_profile": {"reviews_replied": 8},
|
||||
"linkedin_lead_forms": {"leads_ingested": 11},
|
||||
"website_forms": {"leads_ingested": 22},
|
||||
},
|
||||
)
|
||||
@ -0,0 +1,219 @@
|
||||
"""
|
||||
Service Catalog — 12 sellable services on top of the platform.
|
||||
|
||||
Each service has: target_customer, outcome, deliverables, pricing_model,
|
||||
required_integrations, proof_metric.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ServiceOffering:
|
||||
"""A sellable service offering."""
|
||||
|
||||
key: str
|
||||
label_ar: str
|
||||
label_en: str
|
||||
target_customer_ar: str
|
||||
outcome_ar: str
|
||||
deliverables_ar: tuple[str, ...]
|
||||
pricing_model_ar: str
|
||||
required_integrations: tuple[str, ...]
|
||||
proof_metric_ar: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"key": self.key,
|
||||
"label_ar": self.label_ar,
|
||||
"label_en": self.label_en,
|
||||
"target_customer_ar": self.target_customer_ar,
|
||||
"outcome_ar": self.outcome_ar,
|
||||
"deliverables_ar": list(self.deliverables_ar),
|
||||
"pricing_model_ar": self.pricing_model_ar,
|
||||
"required_integrations": list(self.required_integrations),
|
||||
"proof_metric_ar": self.proof_metric_ar,
|
||||
}
|
||||
|
||||
|
||||
SELLABLE_SERVICES: tuple[ServiceOffering, ...] = (
|
||||
ServiceOffering(
|
||||
key="growth_operator_subscription",
|
||||
label_ar="Growth Operator — اشتراك شهري",
|
||||
label_en="Growth Operator Subscription",
|
||||
target_customer_ar="شركات B2B سعودية تبحث عن نمو منظم",
|
||||
outcome_ar="فرص يومية + رسائل عربية + موافقات + Proof Pack شهري",
|
||||
deliverables_ar=(
|
||||
"Daily brief", "Command Feed", "Top opportunities",
|
||||
"Message drafts", "Approvals", "Weekly Proof Pack",
|
||||
),
|
||||
pricing_model_ar="شهري (299 / 2,999 / 7,999 ريال حسب الحجم)",
|
||||
required_integrations=("whatsapp",),
|
||||
proof_metric_ar="Pipeline added × monthly cost multiple",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="channel_setup_service",
|
||||
label_ar="إعداد القنوات",
|
||||
label_en="Channel Setup Service",
|
||||
target_customer_ar="عملاء جدد لم يربطوا قنواتهم بعد",
|
||||
outcome_ar="ربط آمن لكل قنوات نمو الشركة (PDPL-compliant)",
|
||||
deliverables_ar=(
|
||||
"ربط WhatsApp", "ربط Gmail", "ربط Calendar",
|
||||
"ربط Sheets / CRM", "ربط Moyasar", "ربط social accounts",
|
||||
),
|
||||
pricing_model_ar="رسوم setup (3,000-15,000 ريال) لمرة واحدة",
|
||||
required_integrations=("whatsapp", "gmail", "google_calendar", "moyasar"),
|
||||
proof_metric_ar="عدد القنوات المربوطة + uptime أسبوعي",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="lead_intelligence_service",
|
||||
label_ar="Lead Intelligence — تنظيف وتصنيف القوائم",
|
||||
label_en="Lead Intelligence Service",
|
||||
target_customer_ar="عملاء عندهم قوائم أرقام ضخمة غير منظمة",
|
||||
outcome_ar="قائمة آمنة + مصنّفة + Top-10 مرشحة للإطلاق",
|
||||
deliverables_ar=(
|
||||
"normalize_phone", "dedupe", "classify source",
|
||||
"contactability scoring", "segmentation", "Top-10 + why_now",
|
||||
),
|
||||
pricing_model_ar="رسوم لمرة + per-1000-contact pricing",
|
||||
required_integrations=("website_forms", "google_sheets"),
|
||||
proof_metric_ar="نسبة contacts safe + Top-10 conversion",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="outreach_approval_service",
|
||||
label_ar="Outreach بموافقة كاملة",
|
||||
label_en="Outreach Approval Service",
|
||||
target_customer_ar="شركات تخاف من الإرسال العشوائي",
|
||||
outcome_ar="حملات outreach آمنة عبر approval-first flow",
|
||||
deliverables_ar=(
|
||||
"Drafts عربية", "PDPL gates", "Approval queue",
|
||||
"Tracking", "Follow-up", "Proof",
|
||||
),
|
||||
pricing_model_ar="مدمج مع subscription + add-on per-campaign",
|
||||
required_integrations=("whatsapp", "gmail"),
|
||||
proof_metric_ar="معدل الرد + meeting rate + opt-out rate",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="partnership_sprint",
|
||||
label_ar="Partnership Sprint — 14 يوم",
|
||||
label_en="Partnership Sprint",
|
||||
target_customer_ar="شركات تريد قناة شراكات منظمة",
|
||||
outcome_ar="20 شريك محتمل + 10 رسائل + 5 اجتماعات + 1 partner offer",
|
||||
deliverables_ar=(
|
||||
"Target list", "Outreach drafts", "Meeting drafts",
|
||||
"Partner scorecard", "Revenue share template",
|
||||
),
|
||||
pricing_model_ar="رسوم ثابتة (10,000 ريال للـ sprint)",
|
||||
required_integrations=("gmail", "google_calendar"),
|
||||
proof_metric_ar="Partner intros replied + first deal influenced",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="email_revenue_rescue",
|
||||
label_ar="Email Revenue Rescue — استخراج فرص ضائعة",
|
||||
label_en="Email Revenue Rescue",
|
||||
target_customer_ar="شركات عندها inbox مزدحم وفرص ضائعة",
|
||||
outcome_ar="استخراج leads + فرص + drafts من إيميل الشركة",
|
||||
deliverables_ar=(
|
||||
"Inbox audit", "Lost leads list", "Drafts",
|
||||
"Meeting prep", "Pipeline update",
|
||||
),
|
||||
pricing_model_ar="رسوم لمرة + ongoing add-on",
|
||||
required_integrations=("gmail", "google_calendar"),
|
||||
proof_metric_ar="عدد الفرص المُستخرجة + pipeline rescued",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="social_growth_os",
|
||||
label_ar="Social Growth OS — تعليقات + DMs + leads",
|
||||
label_en="Social Growth OS",
|
||||
target_customer_ar="شركات نشطة على LinkedIn / X / Instagram",
|
||||
outcome_ar="تحويل التعليقات والـ mentions إلى فرص",
|
||||
deliverables_ar=(
|
||||
"Listening", "Reply drafts", "Lead extraction",
|
||||
"DM drafts (with permission)", "Reputation tasks",
|
||||
),
|
||||
pricing_model_ar="add-on شهري على Growth/Scale",
|
||||
required_integrations=("x_api", "instagram_graph", "linkedin_lead_forms"),
|
||||
proof_metric_ar="Social-sourced leads + replied mentions",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="local_business_growth",
|
||||
label_ar="Local Business Growth — للمتاجر والعيادات",
|
||||
label_en="Local Business Growth",
|
||||
target_customer_ar="عيادات + مطاعم + متاجر + فروع",
|
||||
outcome_ar="إدارة Google Business + reviews + WhatsApp inbound + booking",
|
||||
deliverables_ar=(
|
||||
"Reviews response", "GBP posts", "Branch info sync",
|
||||
"WhatsApp booking flow", "Payment links",
|
||||
),
|
||||
pricing_model_ar="شهري (999-2,999 ريال) + per-location",
|
||||
required_integrations=("google_business_profile", "whatsapp", "moyasar"),
|
||||
proof_metric_ar="Booking rate + average review rating + revenue per location",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="ai_visibility_aeo_sprint",
|
||||
label_ar="AI Visibility / AEO Sprint",
|
||||
label_en="AI Visibility / AEO Sprint",
|
||||
target_customer_ar="شركات تريد تظهر في إجابات ChatGPT / Gemini / Perplexity",
|
||||
outcome_ar="زيادة ظهور الشركة في answer engines + خطة محتوى 30 يوم",
|
||||
deliverables_ar=(
|
||||
"AEO audit", "Question-gap analysis", "Content plan",
|
||||
"FAQ pages", "Comparison pages", "Local posts",
|
||||
),
|
||||
pricing_model_ar="رسوم لمرة (15,000 ريال) أو monthly retainer",
|
||||
required_integrations=("google_business_profile",),
|
||||
proof_metric_ar="عدد الأسئلة التي تظهر فيها الشركة + competitor delta",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="revenue_proof_pack_service",
|
||||
label_ar="Revenue Proof Pack — شهري للإدارة",
|
||||
label_en="Revenue Proof Pack Service",
|
||||
target_customer_ar="مدراء يحتاجون إثبات قيمة Dealix شهرياً",
|
||||
outcome_ar="تقرير شهري بـ ROI + grading + خطة الشهر القادم",
|
||||
deliverables_ar=(
|
||||
"Activity report", "Money report", "Quality + Risk report",
|
||||
"Best-of insights", "Next-month plan",
|
||||
),
|
||||
pricing_model_ar="مدمج مع subscription Growth/Scale",
|
||||
required_integrations=(),
|
||||
proof_metric_ar="Customer NPS + renewal rate",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="customer_success_operator",
|
||||
label_ar="Customer Success Operator — منع churn",
|
||||
label_en="Customer Success Operator",
|
||||
target_customer_ar="شركات SaaS / subscription business",
|
||||
outcome_ar="health score + churn prediction + upsell signals",
|
||||
deliverables_ar=(
|
||||
"Health score 4-dim", "Churn prediction",
|
||||
"Expansion signals", "QBR auto-drafts",
|
||||
),
|
||||
pricing_model_ar="add-on على Scale tier (1,500 ريال/شهر)",
|
||||
required_integrations=("crm",),
|
||||
proof_metric_ar="Customer churn rate + NRR (Net Revenue Retention)",
|
||||
),
|
||||
ServiceOffering(
|
||||
key="payments_collections_operator",
|
||||
label_ar="Payments & Collections Operator",
|
||||
label_en="Payments & Collections Operator",
|
||||
target_customer_ar="شركات عندها فواتير متأخرة أو payments ضائعة",
|
||||
outcome_ar="quote + invoice drafts + reminders + recovery",
|
||||
deliverables_ar=(
|
||||
"Payment links (Moyasar)", "Invoice drafts",
|
||||
"Failed-payment recovery", "Renewal reminders",
|
||||
),
|
||||
pricing_model_ar="شهري + 1-3% success fee على recovered revenue",
|
||||
required_integrations=("moyasar", "whatsapp", "gmail"),
|
||||
proof_metric_ar="Recovered SAR + on-time payment rate",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def list_services() -> dict[str, Any]:
|
||||
"""Catalog the platform's sellable services."""
|
||||
return {
|
||||
"total": len(SELLABLE_SERVICES),
|
||||
"services": [s.to_dict() for s in SELLABLE_SERVICES],
|
||||
}
|
||||
193
dealix/auto_client_acquisition/platform_services/tool_gateway.py
Normal file
193
dealix/auto_client_acquisition/platform_services/tool_gateway.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
Safe Tool Gateway — single chokepoint for every external action.
|
||||
|
||||
Returns one of: draft_created / approval_required / blocked /
|
||||
ready_for_adapter / unsupported. Never executes a live action here;
|
||||
the actual API call (Gmail/Calendar/WhatsApp/Moyasar/...) happens in
|
||||
the dedicated adapter that's gated by an explicit env flag.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.platform_services.action_policy import evaluate_action
|
||||
from auto_client_acquisition.platform_services.channel_registry import get_channel
|
||||
|
||||
|
||||
SUPPORTED_TOOLS: tuple[str, ...] = (
|
||||
# Gmail / Email
|
||||
"gmail.create_draft",
|
||||
"gmail.read_thread",
|
||||
# Calendar
|
||||
"calendar.draft_event",
|
||||
"calendar.insert_event",
|
||||
# WhatsApp
|
||||
"whatsapp.send_message",
|
||||
"whatsapp.draft_message",
|
||||
# Moyasar
|
||||
"moyasar.create_payment_link",
|
||||
"moyasar.create_invoice",
|
||||
"moyasar.refund",
|
||||
# Social
|
||||
"social.post",
|
||||
"social.send_dm",
|
||||
# Sheets / CRM
|
||||
"sheets.append_row",
|
||||
"crm.update_deal_stage",
|
||||
# Reviews
|
||||
"gbp.reply_review",
|
||||
"gbp.publish_post",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GatewayResult:
|
||||
"""Outcome of a tool invocation through the gateway."""
|
||||
|
||||
status: str # draft_created / approval_required / blocked
|
||||
# / ready_for_adapter / unsupported
|
||||
tool: str
|
||||
matched_policy_rule: str | None = None
|
||||
reasons_ar: list[str] = field(default_factory=list)
|
||||
next_action_ar: str = ""
|
||||
payload_passthrough: dict[str, Any] | None = None
|
||||
|
||||
|
||||
# ── Live-execution flag — defaults to OFF ───────────────────────
|
||||
def _live_send_allowed(channel: str) -> bool:
|
||||
"""Each channel has its own env flag; OFF by default everywhere."""
|
||||
flag_map = {
|
||||
"whatsapp": "WHATSAPP_ALLOW_LIVE_SEND",
|
||||
"gmail": "GMAIL_ALLOW_LIVE_SEND",
|
||||
"google_calendar": "CALENDAR_ALLOW_LIVE_INSERT",
|
||||
"moyasar": "MOYASAR_ALLOW_LIVE_CHARGE",
|
||||
"social": "SOCIAL_ALLOW_LIVE_POST",
|
||||
"x_api": "SOCIAL_ALLOW_LIVE_POST",
|
||||
"instagram_graph": "SOCIAL_ALLOW_LIVE_POST",
|
||||
"google_business_profile": "GBP_ALLOW_LIVE_REPLY",
|
||||
}
|
||||
flag = flag_map.get(channel)
|
||||
if not flag:
|
||||
return False
|
||||
return os.environ.get(flag, "false").lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
def invoke_tool(
|
||||
*,
|
||||
tool: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> GatewayResult:
|
||||
"""
|
||||
Single entry point for every tool action.
|
||||
|
||||
Flow: validate tool name → map to policy action → evaluate policy
|
||||
→ check live-send flag → return GatewayResult (never throws on
|
||||
business-logic failures).
|
||||
"""
|
||||
if tool not in SUPPORTED_TOOLS:
|
||||
return GatewayResult(
|
||||
status="unsupported",
|
||||
tool=tool,
|
||||
reasons_ar=[f"الأداة غير مدعومة: {tool}"],
|
||||
)
|
||||
|
||||
channel_key = tool.split(".", 1)[0]
|
||||
channel = get_channel(_normalize_channel(channel_key))
|
||||
payload = payload or {}
|
||||
ctx = dict(context or {})
|
||||
if "payload" not in ctx:
|
||||
ctx["payload"] = payload
|
||||
|
||||
# Map tool → policy action (the granular labels the policy understands)
|
||||
action_map: dict[str, str] = {
|
||||
"gmail.create_draft": "create_draft",
|
||||
"gmail.read_thread": "read_data",
|
||||
"calendar.draft_event": "create_draft",
|
||||
"calendar.insert_event": "calendar_insert_event",
|
||||
"whatsapp.send_message": "send_whatsapp",
|
||||
"whatsapp.draft_message": "create_draft",
|
||||
"moyasar.create_payment_link": "create_draft",
|
||||
"moyasar.create_invoice": "create_draft",
|
||||
"moyasar.refund": "charge_payment",
|
||||
"social.post": "post_social",
|
||||
"social.send_dm": "send_social_dm",
|
||||
"sheets.append_row": "create_draft",
|
||||
"crm.update_deal_stage": "create_draft",
|
||||
"gbp.reply_review": "post_social",
|
||||
"gbp.publish_post": "post_social",
|
||||
}
|
||||
policy_action = action_map.get(tool, "create_draft")
|
||||
|
||||
decision = evaluate_action(action=policy_action, context=ctx)
|
||||
|
||||
if decision.decision == "blocked":
|
||||
return GatewayResult(
|
||||
status="blocked",
|
||||
tool=tool,
|
||||
matched_policy_rule=decision.matched_rule_id,
|
||||
reasons_ar=decision.reasons_ar,
|
||||
next_action_ar=decision.suggested_next_action_ar,
|
||||
)
|
||||
if decision.decision == "approval_required":
|
||||
return GatewayResult(
|
||||
status="approval_required",
|
||||
tool=tool,
|
||||
matched_policy_rule=decision.matched_rule_id,
|
||||
reasons_ar=decision.reasons_ar,
|
||||
next_action_ar=decision.suggested_next_action_ar,
|
||||
payload_passthrough=payload,
|
||||
)
|
||||
|
||||
# decision == "allow" → check live-send flag for the channel
|
||||
if _is_external_send(tool):
|
||||
if _live_send_allowed(_normalize_channel(channel_key)):
|
||||
return GatewayResult(
|
||||
status="ready_for_adapter",
|
||||
tool=tool,
|
||||
reasons_ar=["السياسة موافقة + LIVE flag مفعل — جاهز لـ adapter."],
|
||||
payload_passthrough=payload,
|
||||
)
|
||||
# Default: keep as draft
|
||||
return GatewayResult(
|
||||
status="draft_created",
|
||||
tool=tool,
|
||||
reasons_ar=["السياسة موافقة لكن LIVE flag غير مفعل — تم حفظه draft."],
|
||||
payload_passthrough=payload,
|
||||
)
|
||||
|
||||
return GatewayResult(
|
||||
status="draft_created",
|
||||
tool=tool,
|
||||
reasons_ar=["إجراء داخلي / draft — لا تفاعل خارجي."],
|
||||
payload_passthrough=payload,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────
|
||||
def _normalize_channel(prefix: str) -> str:
|
||||
"""Channel registry uses dotted keys; tool prefixes use snake."""
|
||||
return {
|
||||
"calendar": "google_calendar",
|
||||
"gbp": "google_business_profile",
|
||||
"social": "x_api", # used as an umbrella prefix
|
||||
"sheets": "google_sheets",
|
||||
}.get(prefix, prefix)
|
||||
|
||||
|
||||
def _is_external_send(tool: str) -> bool:
|
||||
return tool in {
|
||||
"whatsapp.send_message",
|
||||
"calendar.insert_event",
|
||||
"moyasar.create_payment_link",
|
||||
"moyasar.create_invoice",
|
||||
"moyasar.refund",
|
||||
"social.post",
|
||||
"social.send_dm",
|
||||
"gbp.reply_review",
|
||||
"gbp.publish_post",
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
"""
|
||||
Unified Growth Inbox — turn platform events into Arabic action cards.
|
||||
|
||||
8 card types: opportunity / email_lead / whatsapp_reply / social_comment /
|
||||
payment / meeting_prep / review_response / partner_suggestion.
|
||||
|
||||
Every card: title_ar, summary_ar, why_it_matters_ar, recommended_action_ar,
|
||||
risk_level, expected_impact_sar, ≤3 buttons, approval_required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.platform_services.event_bus import PlatformEvent
|
||||
|
||||
|
||||
CARD_TYPES: tuple[str, ...] = (
|
||||
"opportunity",
|
||||
"email_lead",
|
||||
"whatsapp_reply",
|
||||
"social_comment",
|
||||
"payment",
|
||||
"meeting_prep",
|
||||
"review_response",
|
||||
"partner_suggestion",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InboxCard:
|
||||
"""One card in the unified inbox."""
|
||||
|
||||
card_id: str
|
||||
type: str
|
||||
channel: str
|
||||
title_ar: str
|
||||
summary_ar: str
|
||||
why_it_matters_ar: str
|
||||
recommended_action_ar: str
|
||||
risk_level: str # low / medium / high
|
||||
expected_impact_sar: float = 0.0
|
||||
buttons_ar: tuple[str, ...] = () # ≤3 per WhatsApp limit
|
||||
approval_required: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if len(self.buttons_ar) > 3:
|
||||
raise ValueError("buttons_ar must have ≤3 items (WhatsApp limit)")
|
||||
if self.type not in CARD_TYPES:
|
||||
raise ValueError(f"unknown card type: {self.type}")
|
||||
if self.risk_level not in ("low", "medium", "high"):
|
||||
raise ValueError(f"invalid risk_level: {self.risk_level}")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"card_id": self.card_id,
|
||||
"type": self.type,
|
||||
"channel": self.channel,
|
||||
"title_ar": self.title_ar,
|
||||
"summary_ar": self.summary_ar,
|
||||
"why_it_matters_ar": self.why_it_matters_ar,
|
||||
"recommended_action_ar": self.recommended_action_ar,
|
||||
"risk_level": self.risk_level,
|
||||
"expected_impact_sar": self.expected_impact_sar,
|
||||
"buttons_ar": list(self.buttons_ar),
|
||||
"approval_required": self.approval_required,
|
||||
}
|
||||
|
||||
|
||||
# ── Per-event-type renderers ─────────────────────────────────────
|
||||
def build_card_from_event(event: PlatformEvent) -> InboxCard | None:
|
||||
"""Render an event into a card. Returns None for non-actionable events."""
|
||||
et = event.event_type
|
||||
p = event.payload
|
||||
|
||||
if et == "whatsapp.message_received":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="whatsapp_reply",
|
||||
channel="whatsapp",
|
||||
title_ar=f"رد جديد من {p.get('from_name', '—')}",
|
||||
summary_ar=str(p.get("text_preview", ""))[:160],
|
||||
why_it_matters_ar="رد سريع خلال ٣٠ دقيقة يضاعف احتمال الحجز.",
|
||||
recommended_action_ar="صنّف الرد + جهّز رد عربي مناسب",
|
||||
risk_level="low",
|
||||
expected_impact_sar=2_500,
|
||||
buttons_ar=("اعتمد", "تخطّي", "عدّل"),
|
||||
)
|
||||
|
||||
if et == "email.received":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="email_lead",
|
||||
channel="gmail",
|
||||
title_ar=f"إيميل جديد من {p.get('from', '—')}",
|
||||
summary_ar=str(p.get("subject", ""))[:200],
|
||||
why_it_matters_ar="إيميل من عميل محتمل — رد ≤4 ساعات يضاعف التحويل.",
|
||||
recommended_action_ar="جهّز رد رسمي + عرض اجتماع 15 دقيقة",
|
||||
risk_level="low",
|
||||
expected_impact_sar=8_000,
|
||||
buttons_ar=("جهّز مسودة", "احجز اجتماع", "تخطّي"),
|
||||
)
|
||||
|
||||
if et == "calendar.meeting_scheduled":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="meeting_prep",
|
||||
channel="google_calendar",
|
||||
title_ar=f"اجتماع {p.get('when', 'قريباً')} مع {p.get('contact', '—')}",
|
||||
summary_ar="جهّزت ملخص الشركة + 5 أسئلة + اعتراضات محتملة + عرض مناسب.",
|
||||
why_it_matters_ar="الاجتماع المُحضَّر يرفع احتمال الإغلاق بنسبة 40%+.",
|
||||
recommended_action_ar="افتح ملف التحضير + راجع الأجندة",
|
||||
risk_level="low",
|
||||
expected_impact_sar=p.get("expected_value_sar", 25_000),
|
||||
buttons_ar=("افتح التحضير", "اكتب أجندة", "أرسل تأكيد"),
|
||||
approval_required=False,
|
||||
)
|
||||
|
||||
if et == "payment.failed":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="payment",
|
||||
channel="moyasar",
|
||||
title_ar="فشل دفعة",
|
||||
summary_ar=f"العميل {p.get('customer_id', '—')} — مبلغ {p.get('amount_sar', 0):,.0f} ريال.",
|
||||
why_it_matters_ar="فشل الدفع غالباً سببه فني — متابعة سريعة تنقذ الصفقة.",
|
||||
recommended_action_ar="جهّز رسالة WhatsApp + رابط Moyasar جديد",
|
||||
risk_level="medium",
|
||||
expected_impact_sar=p.get("amount_sar", 2_999),
|
||||
buttons_ar=("جهّز رسالة", "رابط جديد", "اتصل"),
|
||||
)
|
||||
|
||||
if et == "review.created":
|
||||
rating = float(p.get("rating", 5))
|
||||
risk = "high" if rating <= 2 else "medium" if rating <= 3 else "low"
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="review_response",
|
||||
channel="google_business_profile",
|
||||
title_ar=f"تقييم Google جديد: {rating} نجوم",
|
||||
summary_ar=str(p.get("text", ""))[:180],
|
||||
why_it_matters_ar=(
|
||||
"التقييم السلبي بدون رد خلال 24 ساعة يضرّ بالسمعة المحلية."
|
||||
if rating <= 3 else "التقييم الإيجابي فرصة للشكر + طلب إحالة."
|
||||
),
|
||||
recommended_action_ar=(
|
||||
"اعتذار قصير + طلب تواصل + حل" if rating <= 3
|
||||
else "شكر دافئ + دعوة لطلب إحالة"
|
||||
),
|
||||
risk_level=risk,
|
||||
expected_impact_sar=1_000,
|
||||
buttons_ar=("اعتمد الرد", "صعّد للمدير", "تخطّي")
|
||||
if rating <= 3
|
||||
else ("اعتمد الرد", "اطلب إحالة", "تخطّي"),
|
||||
)
|
||||
|
||||
if et == "social.comment_received":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="social_comment",
|
||||
channel=event.channel,
|
||||
title_ar=f"تعليق جديد على {event.channel}",
|
||||
summary_ar=str(p.get("text", ""))[:150],
|
||||
why_it_matters_ar="التعليقات الإيجابية = leads warmer من cold outreach.",
|
||||
recommended_action_ar="جهّز رد عربي + اقترح DM لو فيه إشارة شراء",
|
||||
risk_level="medium",
|
||||
expected_impact_sar=1_500,
|
||||
buttons_ar=("جهّز رد", "ابدأ DM", "تخطّي"),
|
||||
)
|
||||
|
||||
if et == "lead.form_submitted":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="opportunity",
|
||||
channel=event.channel,
|
||||
title_ar=f"Lead جديد: {p.get('company', '—')}",
|
||||
summary_ar=f"{p.get('name', '')} — {p.get('email', '')} — {p.get('city', '')}",
|
||||
why_it_matters_ar="Lead تعبأ نموذج → أعلى احتمال تحويل بين كل المصادر.",
|
||||
recommended_action_ar="رد ≤30 دقيقة + احجز مكالمة 15 دقيقة",
|
||||
risk_level="low",
|
||||
expected_impact_sar=p.get("expected_value_sar", 12_000),
|
||||
buttons_ar=("جهّز رد فوري", "احجز مكالمة", "تخطّي"),
|
||||
)
|
||||
|
||||
if et == "partner.suggested":
|
||||
return InboxCard(
|
||||
card_id=f"card_{event.event_id}",
|
||||
type="partner_suggestion",
|
||||
channel="internal",
|
||||
title_ar=f"اقتراح شريك: {p.get('partner_name', '—')}",
|
||||
summary_ar=str(p.get("rationale_ar", ""))[:200],
|
||||
why_it_matters_ar="الشراكة الواحدة تفتح 3-5 leads warmer من cold.",
|
||||
recommended_action_ar="جهّز رسالة warm + احجز مكالمة 20 دقيقة",
|
||||
risk_level="low",
|
||||
expected_impact_sar=p.get("expected_revenue_sar", 50_000),
|
||||
buttons_ar=("اكتب رسالة", "احجز", "تخطّي"),
|
||||
)
|
||||
|
||||
return None # non-actionable event
|
||||
|
||||
|
||||
# ── Demo feed builder ────────────────────────────────────────────
|
||||
def build_demo_feed() -> dict[str, Any]:
|
||||
"""A deterministic demo feed for the dashboard preview."""
|
||||
from auto_client_acquisition.platform_services.event_bus import make_event
|
||||
|
||||
events = [
|
||||
make_event(
|
||||
event_type="lead.form_submitted", channel="website_forms",
|
||||
customer_id="demo",
|
||||
payload={"company": "شركة العقار الذهبي", "name": "خالد",
|
||||
"email": "khalid@example.sa", "city": "الرياض",
|
||||
"expected_value_sar": 18_000},
|
||||
),
|
||||
make_event(
|
||||
event_type="email.received", channel="gmail",
|
||||
customer_id="demo",
|
||||
payload={"from": "ali@example.sa", "subject": "استفسار عن الباقات للشركات"},
|
||||
),
|
||||
make_event(
|
||||
event_type="whatsapp.message_received", channel="whatsapp",
|
||||
customer_id="demo",
|
||||
payload={"from_name": "نورا — Saudi Logistics",
|
||||
"text_preview": "ابغى أعرف وش الفرق بين Growth و Scale؟"},
|
||||
),
|
||||
make_event(
|
||||
event_type="payment.failed", channel="moyasar",
|
||||
customer_id="demo",
|
||||
payload={"customer_id": "cust_123", "amount_sar": 2_999},
|
||||
),
|
||||
make_event(
|
||||
event_type="review.created", channel="google_business_profile",
|
||||
customer_id="demo",
|
||||
payload={"rating": 2, "text": "تأخر الرد في عيادتنا"},
|
||||
),
|
||||
make_event(
|
||||
event_type="partner.suggested", channel="internal",
|
||||
customer_id="demo",
|
||||
payload={"partner_name": "وكالة B2B في جدة",
|
||||
"rationale_ar": "عملاؤها يحتاجون lead-gen — Dealix يكمل خدماتها.",
|
||||
"expected_revenue_sar": 60_000},
|
||||
),
|
||||
]
|
||||
cards = [c.to_dict() for e in events if (c := build_card_from_event(e)) is not None]
|
||||
return {
|
||||
"feed_size": len(cards),
|
||||
"cards": cards,
|
||||
"policy_note_ar": "كل card عربي + ≤3 buttons + approval-aware.",
|
||||
}
|
||||
@ -148,6 +148,36 @@ OAuth Gmail/Calendar، حصص، سياسات.
|
||||
|
||||
وعد منتجي مركزي: من مدخلات شركة/قطاع/مدينة/عرض/هدف إلى قائمة ١٠ فرص مع Why Now ومستوى مخاطرة ومسودات عربية **بانتظار الموافقة فقط** — **`POST /api/v1/innovation/opportunities/ten-in-ten`**؛ وصف المهمة في `GET /api/v1/innovation/growth-missions`؛ الاستراتيجية في [`INNOVATION_STRATEGY.md`](INNOVATION_STRATEGY.md)؛ الإطار التشغيلي بجانب `GET /api/v1/business/gtm/first-10` عند التوسع.
|
||||
|
||||
## 32. Platform Services Layer — برج التحكم بالنمو
|
||||
|
||||
طبقة موحدة multi-channel فوق `growth_operator` تحوّل Dealix من قناة WhatsApp إلى منصة:
|
||||
|
||||
- **11 قناة** (`whatsapp, gmail, google_calendar, moyasar, linkedin_lead_forms, x_api, instagram_graph, google_business_profile, google_sheets, crm, website_forms`).
|
||||
- **Action Policy Engine**: block_cold_whatsapp / block_payment_no_confirm / block_secrets / external_send_needs_approval / high_value_deal_review.
|
||||
- **Tool Gateway** هو المخرج التنفيذي الوحيد — كل أداة تمر منه. Live env flags افتراضياً OFF.
|
||||
- **Unified Inbox**: 8 أنواع بطاقات، ≤3 أزرار، عربية.
|
||||
- **Action Ledger** + **Proof Ledger** (أثر فعلي مقاس بالقناة).
|
||||
- **12 خدمة قابلة للبيع** (`growth_operator_subscription`, `channel_setup_service`, `lead_intelligence_service`, `partnership_sprint`, `email_revenue_rescue`, `social_growth_os`, `local_business_growth`, `ai_visibility_aeo_sprint`, `revenue_proof_pack_service`, `customer_success_operator`, `payments_collections_operator`, `outreach_approval_service`).
|
||||
|
||||
**Endpoints:** `/api/v1/platform/{services/catalog, channels, policy/rules, actions/evaluate, tools/invoke, events/ingest, inbox/feed, identity/resolve, ledger/summary, proof-ledger/demo}`. **التفصيل:** [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md).
|
||||
|
||||
## 33. Intelligence Layer — الشبكة العصبية للنمو
|
||||
|
||||
طبقة فوق Platform Services تجعل Dealix يتعلم ويقترح ويحاكي:
|
||||
|
||||
- **Growth Brain** لكل عميل + `is_ready_for_autopilot()` (≥30 signals + ≥40% accept).
|
||||
- **Command Feed**: 9 أنواع بطاقات يومية (opportunity / revenue_leak / partner_suggestion / meeting_prep / review_response / competitive_move / customer_reactivation / ai_visibility_alert / action_required).
|
||||
- **Action Graph** (10 أنواع حواف): signal → action → outcome.
|
||||
- **Mission Engine**: 7 ميشنات، **Kill Feature: `first_10_opportunities`**.
|
||||
- **Decision Memory**: تعلّم من Accept/Skip/Edit/Block.
|
||||
- **Trust Score** مركب لكل رسالة (safe ≥70 / needs_review 40-69 / blocked <40).
|
||||
- **Revenue DNA**: best_channel / best_segment / best_angle / common_objection / avg_cycle_days.
|
||||
- **Opportunity Simulator** (9 قطاعات سعودية): توقع replies/meetings/deals/pipeline_sar + risk_score.
|
||||
- **Competitive Move Detector**: 8 أنواع حركات + recommended_action_ar.
|
||||
- **Founder Shadow Board**: موجز أسبوعي (3 قرارات + 3 فرص + 3 مخاطر + علاقة + تجربة + مؤشر).
|
||||
|
||||
**Endpoints:** `/api/v1/intelligence/{growth-brain/build, command-feed/demo, missions, missions/recommend, trust-score, revenue-dna/demo, revenue-dna, simulate-opportunity, competitive-move/analyze, board-brief/demo, decisions/record, decisions/preferences}`. **التفصيل:** [`INTELLIGENCE_LAYER_STRATEGY.md`](INTELLIGENCE_LAYER_STRATEGY.md).
|
||||
|
||||
---
|
||||
|
||||
**الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً.
|
||||
|
||||
269
dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md
Normal file
269
dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md
Normal file
@ -0,0 +1,269 @@
|
||||
# Intelligence Layer Strategy — الشبكة العصبية للنمو
|
||||
## (Dealix Growth Neural Network)
|
||||
|
||||
> **الهدف:** تحويل Dealix من "منصة multi-channel" إلى **شبكة عصبية للنمو** تتعلم من قرارات صاحب النشاط، تستخرج DNA الإيرادات، وتعمل ميشنات نمو ذاتية بدلاً من الانتظار للمستخدم.
|
||||
|
||||
---
|
||||
|
||||
## 1. لماذا Intelligence Layer؟
|
||||
|
||||
Platform Services أعطتنا **القنوات + الأمان + الـledgers**. لكن:
|
||||
- لا تتذكر ما يفضله المستخدم.
|
||||
- لا تستخرج رؤى من الفائزين/الخاسرين.
|
||||
- لا تقترح بطاقات قرار جاهزة كل صباح.
|
||||
- لا تحاكي قبل ما ترسل.
|
||||
|
||||
Intelligence Layer هي الطبقة التي تجعل المنصة "تشتغل لوحدها أثناء نوم المستخدم".
|
||||
|
||||
---
|
||||
|
||||
## 2. الوحدات (10 modules)
|
||||
|
||||
| # | الوحدة | الدور |
|
||||
|---|--------|------|
|
||||
| 1 | `growth_brain` | Brain لكل عميل: قطاع، قنوات، أهداف، تفضيلات، مؤشرات. `is_ready_for_autopilot()`. |
|
||||
| 2 | `command_feed` | بطاقات قرار يومية بالعربي (opportunity / revenue_leak / partner_suggestion / meeting_prep / review_response / competitive_move). |
|
||||
| 3 | `action_graph` | رسم بياني للنوع: signal → action → outcome (10 أنواع حواف). |
|
||||
| 4 | `mission_engine` | 7 ميشنات نمو، أهمها **Kill Feature: "10 فرص في 10 دقائق"**. |
|
||||
| 5 | `decision_memory` | يتعلم من Accept / Skip / Edit / Block ويخرج preferences. |
|
||||
| 6 | `trust_score` | مقياس مركّب لكل رسالة (source + opt_in + channel + content + freq + approval). |
|
||||
| 7 | `revenue_dna` | يستخرج: أفضل قناة، أفضل segment، أفضل angle، أكثر اعتراض، متوسط دورة البيع. |
|
||||
| 8 | `opportunity_simulator` | محاكي إلى الأمام: target_count → expected_replies/meetings/deals/pipeline_sar. |
|
||||
| 9 | `competitive_moves` | رصد + رد على حركات المنافسين (price_change / new_offer / hire / funding / launch...). |
|
||||
| 10 | `board_brief` | Founder Shadow Board — موجز أسبوعي: قرارات، فرص، مخاطر، علاقة، تجربة، مؤشر. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Growth Brain
|
||||
|
||||
`build_growth_brain(payload)` يبني سجل لكل عميل:
|
||||
```
|
||||
customer_id, sector, regions, channels_connected,
|
||||
preferred_tone, growth_priorities,
|
||||
learning_signal_count, accept_rate_30d
|
||||
```
|
||||
|
||||
**الجاهزية للأوتوبايلوت:**
|
||||
```
|
||||
ready = (learning_signal_count ≥ 30)
|
||||
AND (accept_rate_30d ≥ 0.40)
|
||||
AND (≥ 1 قناة موصولة)
|
||||
```
|
||||
|
||||
قبل الجاهزية → **draft + approval فقط**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Command Feed (يومي)
|
||||
|
||||
بطاقات بالعربي مع ≤3 أزرار، 9 أنواع:
|
||||
```
|
||||
opportunity, revenue_leak, partner_suggestion,
|
||||
meeting_prep, review_response, ai_visibility_alert,
|
||||
competitive_move, customer_reactivation, action_required
|
||||
```
|
||||
|
||||
`build_command_feed_demo()` يرجع 6 بطاقات تجريبية واقعية.
|
||||
|
||||
---
|
||||
|
||||
## 5. Action Graph
|
||||
|
||||
أنواع الحواف الـ10:
|
||||
```
|
||||
signal_created_opportunity, message_triggered_reply,
|
||||
reply_led_to_meeting, meeting_led_to_proposal,
|
||||
proposal_led_to_payment, partner_suggestion_taken,
|
||||
review_response_recovered_customer, approval_allowed_send,
|
||||
blocked_action_prevented_risk, content_generated_lead
|
||||
```
|
||||
|
||||
`what_works_summary(customer_id)` يُرجع: مجموع الحواف + توزيعها بالنوع → "ما الذي يعمل فعلاً".
|
||||
|
||||
---
|
||||
|
||||
## 6. Mission Engine — 7 ميشنات
|
||||
|
||||
| ID | الاسم | ملاحظات |
|
||||
|----|-------|---------|
|
||||
| **first_10_opportunities** ⭐ | 10 فرص في 10 دقائق | **Kill Feature** — يبدأ من 0 ويُسلم 10 leads بالعربي قبل أن يعتاد المستخدم على المنصة. |
|
||||
| revenue_leak_rescue | استعادة الإيرادات المتسربة | عملاء توقفوا، فواتير معلقة. |
|
||||
| partnership_sprint | سبرنت شراكات | Partner Graph — اقتراحات تكامل. |
|
||||
| customer_reactivation | إعادة تنشيط عملاء | فترة سكون → رسالة دافئة. |
|
||||
| meeting_booking_sprint | حجز اجتماعات | drafts للجدولة + اعتماد. |
|
||||
| ai_visibility_sprint | Answer Engine Optimization | ظهور النشاط في Perplexity / ChatGPT / Gemini. |
|
||||
| competitive_response | الرد على حركات المنافسين | يُفعّل عند رصد price_change / launch / funding. |
|
||||
|
||||
`recommend_missions(brain, limit=3)` يرتّب بحسب توافق القطاع + القنوات + الأولويات.
|
||||
|
||||
---
|
||||
|
||||
## 7. Decision Memory
|
||||
|
||||
يتعلم من 4 قرارات: `accept / skip / edit / block`.
|
||||
|
||||
`preferences()` يُرجع:
|
||||
```
|
||||
accept_rate, samples,
|
||||
preferred_channels, preferred_tones, preferred_sectors,
|
||||
rejected_action_types
|
||||
```
|
||||
|
||||
يستخدمها `mission_engine` لرفع/خفض ترتيب البطاقات → الـ "warm-up" loop.
|
||||
|
||||
---
|
||||
|
||||
## 8. Trust Score
|
||||
|
||||
نتيجة 0..100 + verdict (`safe ≥70` / `needs_review 40-69` / `blocked <40`).
|
||||
|
||||
العوامل:
|
||||
- `source_quality` (customer / opt_in_lead / referral / cold / unknown).
|
||||
- `opt_in` (boolean).
|
||||
- `channel` risk (whatsapp risk أعلى من email).
|
||||
- محتوى الرسالة (عبارات محظورة: "ضمان 100%", "آخر فرصة"...).
|
||||
- `frequency_count_this_week` vs `weekly_cap`.
|
||||
- `approval_status`.
|
||||
|
||||
تطبيق فوري: قبل أي `tool_gateway.invoke_tool` → بطاقة في الـCommand Feed بدلاً من الإرسال.
|
||||
|
||||
---
|
||||
|
||||
## 9. Revenue DNA
|
||||
|
||||
`extract_revenue_dna(customer_id, won_deals, replies, objections)` يُرجع:
|
||||
```
|
||||
best_channel, best_segment, best_message_angle,
|
||||
common_objection, avg_cycle_days,
|
||||
deals_observed, next_experiment_ar
|
||||
```
|
||||
|
||||
استعمال: ميشن `revenue_dna_demo` يُري المالك "هذا ما يفوز فعلاً عندك".
|
||||
|
||||
---
|
||||
|
||||
## 10. Opportunity Simulator
|
||||
|
||||
`simulate_opportunity(target_count, sector, avg_deal_value_sar, channel, cold_pct, quality_lift)`:
|
||||
|
||||
يُرجع:
|
||||
```
|
||||
expected_replies, expected_meetings, expected_deals,
|
||||
expected_pipeline_sar, risk_score (0..100),
|
||||
risks_ar, rates_used, approval_required=True
|
||||
```
|
||||
|
||||
9 قطاعات سعودية مهيّأة (real_estate, saas, retail, food, education, healthcare, logistics, fintech, contracting).
|
||||
|
||||
**استعمال حرج:** تحاكِ قبل ما تنفّذ → "مع 100 جهة، النتيجة المتوقعة 6 صفقات بقيمة 300K، مخاطرة PDPL متوسطة لو 60% بارد".
|
||||
|
||||
---
|
||||
|
||||
## 11. Competitive Moves
|
||||
|
||||
8 أنواع حركات: `price_change, new_offer, new_hire, funding, launch, partnership, layoffs, expansion`.
|
||||
|
||||
`analyze_competitive_move(competitor_name, move_type, payload)` → urgency + Arabic recommended_action + approval_required.
|
||||
|
||||
مثال: price_change بـ-25% → urgency `high` + اقتراح بطاقة "أرسل عرض مضاد للعملاء المترددين".
|
||||
|
||||
---
|
||||
|
||||
## 12. Board Brief — Founder Shadow Board
|
||||
|
||||
`build_board_brief()` يُرجع موجز أسبوعي:
|
||||
```
|
||||
decisions_required_ar (3),
|
||||
top_opportunities_ar (3),
|
||||
top_risks_ar (3),
|
||||
key_relationship_ar,
|
||||
experiment_to_run_ar,
|
||||
metric_to_watch_ar,
|
||||
money_summary
|
||||
```
|
||||
|
||||
استعمال: ميل أسبوعي يومي الأحد 7:00 ص → "هذا ما يحتاج قراركم هذا الأسبوع، وهذا ما يكشفه الذكاء الاصطناعي".
|
||||
|
||||
---
|
||||
|
||||
## 13. Endpoints (`/api/v1/intelligence/...`)
|
||||
|
||||
```
|
||||
POST /growth-brain/build
|
||||
GET /command-feed/demo
|
||||
GET /missions
|
||||
POST /missions/recommend
|
||||
POST /trust-score
|
||||
GET /revenue-dna/demo
|
||||
POST /revenue-dna
|
||||
POST /simulate-opportunity
|
||||
POST /competitive-move/analyze
|
||||
GET /board-brief/demo
|
||||
POST /decisions/record
|
||||
GET /decisions/preferences
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. اختبارات
|
||||
|
||||
`tests/unit/test_intelligence_layer.py` — تغطية لكل الوحدات الـ10:
|
||||
- growth brain autopilot threshold
|
||||
- command feed Arabic + ≤3 buttons + critical types
|
||||
- action graph add/summary + unknown edge type raises
|
||||
- missions list + kill feature + recommend
|
||||
- decision memory records/aggregates/empty/invalid
|
||||
- trust score (cold blocked, safe, risky phrases, freq cap lowers)
|
||||
- revenue DNA best channel + defaults
|
||||
- simulator pipeline + cold_pct warning + unknown sector default
|
||||
- competitive move urgency + unknown type + funding action
|
||||
- board brief structure (3 من كل: قرار/فرصة/مخاطرة)
|
||||
|
||||
---
|
||||
|
||||
## 15. ما لا تفعله هذه الطبقة
|
||||
|
||||
- **لا** ترسل أي شيء فعلياً (تحت سقف tool_gateway).
|
||||
- **لا** تتجاوز سياسات platform_services.
|
||||
- **لا** تستخدم بيانات بدون consent.
|
||||
- **لا** تنفذ ميشن بدون اعتماد المالك (إلا بعد `is_ready_for_autopilot()`).
|
||||
|
||||
---
|
||||
|
||||
## 16. الاندماج مع Platform Services
|
||||
|
||||
```
|
||||
Platform Services Intelligence Layer
|
||||
──────────────── ────────────────────
|
||||
event_bus ←→ action_graph (يستهلك الأحداث)
|
||||
identity ←→ growth_brain (هوية → سياق)
|
||||
channel_registry ←→ simulator (rates_used per channel)
|
||||
action_policy ←→ trust_score (verdict → policy gate)
|
||||
tool_gateway ←→ command_feed (cards تُنفّذ عبر gateway)
|
||||
unified_inbox ←→ command_feed (نفس البنية، طبقة أعلى)
|
||||
action_ledger ←→ decision_memory (يقرأ الـledger)
|
||||
proof_ledger ←→ board_brief (money_summary مصدره proof)
|
||||
service_catalog ←→ mission_engine (الميشنات → خدمات قابلة للبيع)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. الـ Kill Feature
|
||||
|
||||
**"10 فرص في 10 دقائق"** — `first_10_opportunities`:
|
||||
|
||||
1. عند بدء العميل، نسأل: قطاع + منطقة + قناة مفضلة.
|
||||
2. خلال 10 دقائق نُسلم 10 بطاقات `opportunity` بالعربي مع `recommended_action_ar`.
|
||||
3. كل بطاقة draft → اعتماد → تنفيذ.
|
||||
4. إذا قبل المالك ≥4 → نزيد signal_count + accept_rate → نقترب من autopilot.
|
||||
|
||||
هذه الميزة تكسر "blank canvas problem" وتُري قيمة فورية قبل أن يفتح المستخدم WhatsApp Web.
|
||||
|
||||
---
|
||||
|
||||
## 18. ما يلي
|
||||
|
||||
- ربط `command_feed` بإشارات حقيقية (Gmail / WA Business / GBP / website forms).
|
||||
- استبدال الـin-memory `_MEMORY` بـ Supabase.
|
||||
- جدولة `board_brief` يوم الأحد 7 ص (Cron + email/WhatsApp).
|
||||
- شحن أول 100 عميل تحت "Approval-First" لجمع أول 3,000 قرار → تدريب decision_memory الحقيقي.
|
||||
196
dealix/docs/PLATFORM_SERVICES_STRATEGY.md
Normal file
196
dealix/docs/PLATFORM_SERVICES_STRATEGY.md
Normal file
@ -0,0 +1,196 @@
|
||||
# Platform Services Strategy — برج التحكم بالنمو
|
||||
## (Dealix Growth Control Tower)
|
||||
|
||||
> **الهدف:** تحويل Dealix من "WhatsApp Growth Operator" إلى **منصة نمو متعددة القنوات** تشتغل تحت سقف واحد، بسياسات أمان موحدة، ومسار اعتماد واحد، وبروتوكول أحداث موحد.
|
||||
|
||||
---
|
||||
|
||||
## 1. لماذا Platform Services؟
|
||||
|
||||
كل قناة (WhatsApp, Gmail, Calendar, LinkedIn, X, Instagram, GBP, Sheets, CRM, Moyasar, Website Forms) تحتاج:
|
||||
- تطبيع الإشارات (signal normalization).
|
||||
- سياسة قبول/رفض موحدة (PDPL-aware).
|
||||
- حل هوية متقاطع (cross-channel identity).
|
||||
- مدخل تنفيذي موحد (single tool gateway) لمنع الإرسال البارد، تسريب الأسرار، أو الدفع بدون تأكيد.
|
||||
- صندوق بريد موحد (unified inbox) ببطاقات قابلة للاعتماد.
|
||||
- سجل أفعال (action ledger) للمراجعة (SDAIA / PDPL).
|
||||
- سجل أثر (proof ledger) لتسويق "كم وفّرنا، كم سحبنا، كم منعنا من مخاطر".
|
||||
|
||||
بدون هذه الطبقة، كل ميزة جديدة تحتاج تكامل مخصص → فوضى أمنية + أمنية + قانونية.
|
||||
|
||||
---
|
||||
|
||||
## 2. الوحدات (10 modules)
|
||||
|
||||
| # | الوحدة | الدور |
|
||||
|---|--------|------|
|
||||
| 1 | `event_bus` | تصنيف موحد لـ27 نوع حدث (whatsapp/email/calendar/lead/payment/review/social/partner/sheet/crm/action). |
|
||||
| 2 | `identity_resolution` | دمج phone + email + CRM ID + social handles → هوية موحدة. |
|
||||
| 3 | `channel_registry` | 11 قناة، لكل واحدة capabilities + allowed/blocked actions + PDPL notes. |
|
||||
| 4 | `action_policy` | محرك قواعد (block_cold_whatsapp, block_payment_no_confirm, block_secrets, external_send_needs_approval...). |
|
||||
| 5 | `tool_gateway` | المخرج التنفيذي الوحيد. كل أداة تمر من هنا → سياسة → draft / approval_required / blocked / ready. |
|
||||
| 6 | `unified_inbox` | بطاقات قرار (≤3 أزرار، عربية، type+risk+recommended_action). |
|
||||
| 7 | `action_ledger` | سجل كل فعل بمراحله (requested → approved → executed). |
|
||||
| 8 | `proof_ledger` | عدّاد أثر (leads, meetings, drafts, sends, payments, revenue, risks_blocked, time_saved). |
|
||||
| 9 | `service_catalog` | 12 خدمة قابلة للبيع تحت Dealix Operator OS. |
|
||||
| 10 | (router + tests) | `api/routers/platform_services.py` + اختبارات شاملة. |
|
||||
|
||||
---
|
||||
|
||||
## 3. القنوات الـ11
|
||||
|
||||
```
|
||||
whatsapp, gmail, google_calendar, moyasar, linkedin_lead_forms,
|
||||
x_api, instagram_graph, google_business_profile, google_sheets,
|
||||
crm, website_forms
|
||||
```
|
||||
|
||||
كل قناة لها:
|
||||
- `capabilities`
|
||||
- `beta_status` (`live` / `beta` / `coming_soon`)
|
||||
- `allowed_actions` / `blocked_actions`
|
||||
- `risk_level`
|
||||
- `notes_ar`
|
||||
|
||||
مثال: WhatsApp **يحظر** `cold_send_without_consent`. Gmail يستخدم `gmail.compose` فقط (drafts). Calendar `live_inserted=False` حتى يربط OAuth.
|
||||
|
||||
---
|
||||
|
||||
## 4. سياسة الأمان (Action Policy)
|
||||
|
||||
**قواعد block أساسية:**
|
||||
1. WhatsApp بارد بدون consent → **blocked** (PDPL).
|
||||
2. أي charge/refund بدون `user_confirmed=true` → **blocked**.
|
||||
3. أي payload يحوي `api_key/secret/token/...` → **blocked**.
|
||||
|
||||
**قواعد approval_required:**
|
||||
- أي إرسال خارجي (`send_*`) → اعتماد إنساني.
|
||||
- إدراج موعد في تقويم → اعتماد.
|
||||
- DM على سوشل → اعتماد + opt-in.
|
||||
- صفقة قيمتها ≥ 200,000 ريال → اعتماد.
|
||||
|
||||
**default:** allow (للـ read-only data ops).
|
||||
|
||||
---
|
||||
|
||||
## 5. Tool Gateway
|
||||
|
||||
كل أداة (`whatsapp.send_message`, `gmail.compose`, `calendar.insert_event`, `moyasar.refund`, `gbp.reply_review`, ...) **يجب** تمر من `invoke_tool()`.
|
||||
|
||||
النتائج المحتملة:
|
||||
- `unsupported` — أداة غير مسجلة.
|
||||
- `blocked` — السياسة منعت.
|
||||
- `approval_required` — تحتاج قبول إنساني.
|
||||
- `draft_created` — افتراضياً (live env flag = OFF).
|
||||
- `ready_for_adapter` — جاهز للتنفيذ الحقيقي إذا اشتغل live env flag.
|
||||
|
||||
**Live env flags** (افتراضياً كلها OFF):
|
||||
```
|
||||
WHATSAPP_ALLOW_LIVE_SEND
|
||||
GMAIL_ALLOW_LIVE_SEND
|
||||
CALENDAR_ALLOW_LIVE_INSERT
|
||||
MOYASAR_ALLOW_LIVE_CHARGE
|
||||
GBP_ALLOW_LIVE_REPLY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. صندوق البريد الموحد (Unified Inbox)
|
||||
|
||||
8 أنواع بطاقات:
|
||||
```
|
||||
opportunity, email_lead, whatsapp_reply, payment,
|
||||
meeting_prep, review_response, partner_suggestion, action_required
|
||||
```
|
||||
|
||||
كل بطاقة:
|
||||
- ≤3 أزرار (تطبيق قيد WhatsApp Reply Buttons).
|
||||
- عربية (title_ar, summary_ar, why_it_matters_ar, recommended_action_ar).
|
||||
- `risk_level` (low/medium/high).
|
||||
|
||||
البطاقات تُبنى تلقائياً من `PlatformEvent` عبر `build_card_from_event()`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Proof Ledger
|
||||
|
||||
عدّاد يقيس الأثر العملي للمنصة:
|
||||
```
|
||||
leads_created, meetings_booked, drafts_approved,
|
||||
messages_sent, payments_initiated, payments_paid,
|
||||
revenue_influenced_sar, risks_blocked, time_saved_hours,
|
||||
partner_opportunities, by_channel
|
||||
```
|
||||
|
||||
هذا هو **Marketing Asset** — لتُري العميل: "في 30 يوم، نحن ساعدناك تعمل X، منعنا Y مخاطر، وفرنا Z ساعة".
|
||||
|
||||
---
|
||||
|
||||
## 8. خدمات قابلة للبيع (Service Catalog)
|
||||
|
||||
12 خدمة تجارية:
|
||||
1. `growth_operator_subscription` — اشتراك شهري للمنصة.
|
||||
2. `channel_setup_service` — ربط القنوات (one-time).
|
||||
3. `lead_intelligence_service` — إثراء + تأهيل لقاءات.
|
||||
4. `outreach_approval_service` — drafts + approval workflow.
|
||||
5. `partnership_sprint` — فرص تعاون عبر Partner Graph.
|
||||
6. `email_revenue_rescue` — استعادة عملاء إيميل.
|
||||
7. `social_growth_os` — تنبيهات + drafts + جدولة.
|
||||
8. `local_business_growth` — GBP + reviews + visibility.
|
||||
9. `ai_visibility_aeo_sprint` — Answer Engine Optimization.
|
||||
10. `revenue_proof_pack_service` — تقرير أثر لمستثمرين / عملاء.
|
||||
11. `customer_success_operator` — خفض churn + توسيع.
|
||||
12. `payments_collections_operator` — تذكير + تحصيل (Moyasar).
|
||||
|
||||
---
|
||||
|
||||
## 9. Endpoints (`/api/v1/platform/...`)
|
||||
|
||||
```
|
||||
GET /services/catalog
|
||||
GET /channels
|
||||
GET /channels/{channel_key}
|
||||
GET /policy/rules
|
||||
POST /actions/evaluate
|
||||
POST /actions/approve
|
||||
GET /ledger/summary
|
||||
POST /events/ingest
|
||||
GET /inbox/feed
|
||||
POST /identity/resolve
|
||||
GET /identity/resolve-demo
|
||||
POST /tools/invoke
|
||||
GET /proof-ledger/demo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. اختبارات
|
||||
|
||||
`tests/unit/test_platform_services.py` — تغطية لكل الوحدات الـ10:
|
||||
- catalog completeness
|
||||
- channel coverage + cold-send blocked
|
||||
- event validation
|
||||
- policy (cold WA blocked, secrets blocked, payment confirmation, external send approval, high-value review)
|
||||
- gateway (unsupported / blocked / draft default / live flag check)
|
||||
- identity multi-signal merge
|
||||
- inbox card validation (≤3 buttons + valid type)
|
||||
- action ledger summary
|
||||
- proof ledger structure
|
||||
|
||||
---
|
||||
|
||||
## 11. ما لا تفعله هذه الطبقة
|
||||
|
||||
- **لا** ترسل واتساب فعلياً (افتراضياً draft).
|
||||
- **لا** ترسل Gmail فعلياً.
|
||||
- **لا** تدرج موعد في Google Calendar.
|
||||
- **لا** تأخذ أو تعيد دفعة بدون user_confirmed.
|
||||
- **لا** تخزن مفاتيح API في payload.
|
||||
|
||||
---
|
||||
|
||||
## 12. ما يلي
|
||||
|
||||
- ربط Adapters حقيقية (WhatsApp Cloud, Gmail, Calendar) خلف الـenv flags.
|
||||
- استبدال in-memory ledgers بـ Supabase.
|
||||
- تشغيل `proof_ledger` على بيانات إنتاج مع تجربة عميل واحد.
|
||||
281
dealix/tests/unit/test_intelligence_layer.py
Normal file
281
dealix/tests/unit/test_intelligence_layer.py
Normal file
@ -0,0 +1,281 @@
|
||||
"""Unit tests for the Intelligence Layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from auto_client_acquisition.intelligence_layer import (
|
||||
DecisionMemory,
|
||||
EDGE_TYPES,
|
||||
INTEL_MISSIONS,
|
||||
ActionGraph,
|
||||
analyze_competitive_move,
|
||||
build_board_brief,
|
||||
build_command_feed_demo,
|
||||
build_growth_brain,
|
||||
build_revenue_dna_demo,
|
||||
compute_trust_score,
|
||||
extract_revenue_dna,
|
||||
learn_from_decision,
|
||||
list_intel_missions,
|
||||
recommend_missions,
|
||||
simulate_opportunity,
|
||||
)
|
||||
|
||||
|
||||
# ── Growth Brain ─────────────────────────────────────────────
|
||||
def test_growth_brain_builds_with_defaults():
|
||||
brain = build_growth_brain()
|
||||
assert brain.customer_id == "demo"
|
||||
assert "whatsapp" in brain.channels_connected
|
||||
assert brain.preferred_tone == "warm"
|
||||
|
||||
|
||||
def test_growth_brain_autopilot_readiness():
|
||||
new_brain = build_growth_brain({
|
||||
"learning_signal_count": 5, "accept_rate_30d": 0.2,
|
||||
"channels_connected": ("whatsapp",),
|
||||
})
|
||||
assert new_brain.is_ready_for_autopilot() is False
|
||||
|
||||
mature_brain = build_growth_brain({
|
||||
"learning_signal_count": 50, "accept_rate_30d": 0.55,
|
||||
"channels_connected": ("whatsapp", "gmail"),
|
||||
})
|
||||
assert mature_brain.is_ready_for_autopilot() is True
|
||||
|
||||
|
||||
# ── Command Feed ─────────────────────────────────────────────
|
||||
def test_command_feed_returns_arabic_cards():
|
||||
out = build_command_feed_demo()
|
||||
assert out["feed_size"] >= 5
|
||||
for card in out["cards"]:
|
||||
assert len(card["buttons_ar"]) <= 3
|
||||
assert any("" <= ch <= "ۿ" for ch in card["title_ar"])
|
||||
|
||||
|
||||
def test_command_feed_includes_critical_card_types():
|
||||
out = build_command_feed_demo()
|
||||
types = {c["type"] for c in out["cards"]}
|
||||
for required in ("opportunity", "revenue_leak", "partner_suggestion",
|
||||
"meeting_prep", "review_response"):
|
||||
assert required in types
|
||||
|
||||
|
||||
# ── Action Graph ─────────────────────────────────────────────
|
||||
def test_action_graph_add_and_summarize():
|
||||
g = ActionGraph()
|
||||
g.add_edge(
|
||||
edge_type="signal_created_opportunity",
|
||||
src_id="signal_1", dst_id="opp_1", customer_id="c1",
|
||||
)
|
||||
g.add_edge(
|
||||
edge_type="message_triggered_reply",
|
||||
src_id="msg_1", dst_id="reply_1", customer_id="c1",
|
||||
)
|
||||
summary = g.what_works_summary("c1")
|
||||
assert summary["total_edges"] == 2
|
||||
assert "signal_created_opportunity" in summary["by_edge_type"]
|
||||
|
||||
|
||||
def test_action_graph_unknown_edge_type_raises():
|
||||
g = ActionGraph()
|
||||
with pytest.raises(ValueError):
|
||||
g.add_edge(edge_type="bogus", src_id="a", dst_id="b", customer_id="c")
|
||||
|
||||
|
||||
def test_edge_types_cover_essentials():
|
||||
for required in ("signal_created_opportunity", "message_triggered_reply",
|
||||
"approval_allowed_send", "blocked_action_prevented_risk"):
|
||||
assert required in EDGE_TYPES
|
||||
|
||||
|
||||
# ── Mission Engine ───────────────────────────────────────────
|
||||
def test_missions_include_first_10():
|
||||
out = list_intel_missions()
|
||||
ids = {m["id"] for m in out["missions"]}
|
||||
assert "first_10_opportunities" in ids
|
||||
assert out["kill_feature_id"] == "first_10_opportunities"
|
||||
|
||||
|
||||
def test_missions_include_aeo_and_competitive():
|
||||
ids = {m["id"] for m in INTEL_MISSIONS}
|
||||
assert "ai_visibility_sprint" in ids
|
||||
assert "competitive_response" in ids
|
||||
|
||||
|
||||
def test_recommend_missions_prioritizes_kill_feature():
|
||||
"""Kill feature should always be near the top."""
|
||||
brain = build_growth_brain({
|
||||
"channels_connected": ("whatsapp",),
|
||||
"growth_priorities": ("fill_pipeline",),
|
||||
})
|
||||
rec = recommend_missions(brain, limit=3)
|
||||
ids = [m["id"] for m in rec["recommended"]]
|
||||
assert "first_10_opportunities" in ids
|
||||
|
||||
|
||||
def test_recommend_missions_without_brain():
|
||||
rec = recommend_missions(None, limit=2)
|
||||
assert len(rec["recommended"]) == 2
|
||||
|
||||
|
||||
# ── Decision Memory ──────────────────────────────────────────
|
||||
def test_decision_memory_records_and_aggregates():
|
||||
mem = DecisionMemory(customer_id="c1")
|
||||
learn_from_decision(memory=mem, decision="accept",
|
||||
action_type="send_whatsapp", channel="whatsapp",
|
||||
sector="real_estate", tone="warm")
|
||||
learn_from_decision(memory=mem, decision="accept",
|
||||
action_type="send_whatsapp", channel="whatsapp",
|
||||
tone="warm")
|
||||
learn_from_decision(memory=mem, decision="skip",
|
||||
action_type="send_email", channel="gmail")
|
||||
prefs = mem.preferences()
|
||||
assert prefs["accept_rate"] == 0.6667 or 0.6 < prefs["accept_rate"] < 0.7
|
||||
assert "whatsapp" in prefs["preferred_channels"]
|
||||
assert "warm" in prefs["preferred_tones"]
|
||||
assert "send_email" in prefs["rejected_action_types"]
|
||||
|
||||
|
||||
def test_decision_memory_unknown_decision_raises():
|
||||
mem = DecisionMemory(customer_id="c1")
|
||||
with pytest.raises(ValueError):
|
||||
mem.append(decision="bogus", action_type="x", channel="y")
|
||||
|
||||
|
||||
def test_decision_memory_empty():
|
||||
mem = DecisionMemory(customer_id="c1")
|
||||
prefs = mem.preferences()
|
||||
assert prefs["samples"] == 0
|
||||
assert prefs["accept_rate"] == 0.0
|
||||
|
||||
|
||||
# ── Trust Score ──────────────────────────────────────────────
|
||||
def test_trust_blocks_cold_whatsapp_no_optin():
|
||||
out = compute_trust_score(
|
||||
source_quality="cold", opt_in=False, channel="whatsapp",
|
||||
message_text="hello", approval_status="pending",
|
||||
)
|
||||
assert out["verdict"] == "blocked"
|
||||
|
||||
|
||||
def test_trust_safe_for_existing_customer_with_consent():
|
||||
out = compute_trust_score(
|
||||
source_quality="customer", opt_in=True, channel="whatsapp",
|
||||
message_text="مرحباً، تحديث للعميل العزيز.",
|
||||
approval_status="approved",
|
||||
)
|
||||
assert out["verdict"] == "safe"
|
||||
assert out["score"] >= 70
|
||||
|
||||
|
||||
def test_trust_blocks_risky_phrases():
|
||||
out = compute_trust_score(
|
||||
source_quality="customer", opt_in=True, channel="whatsapp",
|
||||
message_text="ضمان 100% نتائج مضمونة آخر فرصة",
|
||||
approval_status="approved",
|
||||
)
|
||||
assert out["verdict"] in ("blocked", "needs_review")
|
||||
|
||||
|
||||
def test_trust_freq_cap_lowers_score():
|
||||
"""Hitting the weekly cap should lower the trust score vs not hitting it."""
|
||||
base = compute_trust_score(
|
||||
source_quality="customer", opt_in=True, channel="whatsapp",
|
||||
message_text="hello", frequency_count_this_week=0, weekly_cap=2,
|
||||
approval_status="approved",
|
||||
)
|
||||
capped = compute_trust_score(
|
||||
source_quality="customer", opt_in=True, channel="whatsapp",
|
||||
message_text="hello", frequency_count_this_week=2, weekly_cap=2,
|
||||
approval_status="approved",
|
||||
)
|
||||
assert capped["score"] < base["score"]
|
||||
assert any("سقف" in r or "weekly" in r.lower() or "تجاوز" in r
|
||||
for r in capped["reasons_ar"])
|
||||
|
||||
|
||||
# ── Revenue DNA ──────────────────────────────────────────────
|
||||
def test_revenue_dna_extracts_best_channel():
|
||||
out = extract_revenue_dna(
|
||||
customer_id="c1",
|
||||
won_deals=[
|
||||
{"channel": "whatsapp", "segment": "inbound_lead", "message_angle": "value", "cycle_days": 18},
|
||||
{"channel": "whatsapp", "segment": "inbound_lead", "message_angle": "value", "cycle_days": 20},
|
||||
{"channel": "email", "segment": "referral", "message_angle": "warm", "cycle_days": 30},
|
||||
],
|
||||
)
|
||||
assert out["best_channel"] == "whatsapp"
|
||||
assert out["deals_observed"] == 3
|
||||
|
||||
|
||||
def test_revenue_dna_demo_has_next_experiment():
|
||||
out = build_revenue_dna_demo()
|
||||
assert "next_experiment_ar" in out
|
||||
assert any("" <= ch <= "ۿ" for ch in out["next_experiment_ar"])
|
||||
|
||||
|
||||
def test_revenue_dna_empty_input_returns_defaults():
|
||||
out = extract_revenue_dna(customer_id="c1")
|
||||
assert out["best_channel"] == "whatsapp" # safe default
|
||||
assert out["deals_observed"] == 0
|
||||
|
||||
|
||||
# ── Opportunity Simulator ────────────────────────────────────
|
||||
def test_simulator_returns_pipeline_estimate():
|
||||
out = simulate_opportunity(
|
||||
target_count=100, sector="real_estate",
|
||||
avg_deal_value_sar=50_000, channel="whatsapp", cold_pct=0,
|
||||
)
|
||||
assert out["expected_replies"] >= 0
|
||||
assert out["expected_pipeline_sar"] >= 0
|
||||
assert "rates_used" in out
|
||||
assert out["approval_required"] is True
|
||||
|
||||
|
||||
def test_simulator_warns_high_cold_pct():
|
||||
out = simulate_opportunity(
|
||||
target_count=100, sector="saas", channel="whatsapp", cold_pct=0.6,
|
||||
)
|
||||
assert out["risk_score"] >= 70
|
||||
assert any("PDPL" in r or "cold" in r for r in out["risks_ar"])
|
||||
|
||||
|
||||
def test_simulator_unknown_sector_uses_default():
|
||||
out = simulate_opportunity(
|
||||
target_count=50, sector="totally_unknown_xyz", channel="whatsapp", cold_pct=0,
|
||||
)
|
||||
assert "rates_used" in out
|
||||
assert out["expected_pipeline_sar"] >= 0
|
||||
|
||||
|
||||
# ── Competitive Moves ────────────────────────────────────────
|
||||
def test_competitive_move_price_change_drop_high_urgency():
|
||||
out = analyze_competitive_move(
|
||||
competitor_name="X", move_type="price_change",
|
||||
payload={"price_delta_pct": -25},
|
||||
)
|
||||
assert out["urgency"] == "high"
|
||||
assert out["approval_required"] is True
|
||||
|
||||
|
||||
def test_competitive_move_unknown_type():
|
||||
out = analyze_competitive_move(competitor_name="X", move_type="bogus_type")
|
||||
assert "error" in out
|
||||
|
||||
|
||||
def test_competitive_move_funding_returns_action():
|
||||
out = analyze_competitive_move(competitor_name="X", move_type="funding")
|
||||
assert "recommended_action_ar" in out
|
||||
|
||||
|
||||
# ── Board Brief ──────────────────────────────────────────────
|
||||
def test_board_brief_returns_decisions_opportunities_risks():
|
||||
out = build_board_brief()
|
||||
assert len(out["decisions_required_ar"]) >= 3
|
||||
assert len(out["top_opportunities_ar"]) >= 3
|
||||
assert len(out["top_risks_ar"]) >= 3
|
||||
assert "key_relationship_ar" in out
|
||||
assert "experiment_to_run_ar" in out
|
||||
assert "metric_to_watch_ar" in out
|
||||
298
dealix/tests/unit/test_platform_services.py
Normal file
298
dealix/tests/unit/test_platform_services.py
Normal file
@ -0,0 +1,298 @@
|
||||
"""Unit tests for the Platform Services Layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from auto_client_acquisition.platform_services import (
|
||||
ALL_CHANNELS,
|
||||
EVENT_TYPES,
|
||||
POLICY_RULES,
|
||||
SELLABLE_SERVICES,
|
||||
build_card_from_event,
|
||||
build_demo_feed,
|
||||
build_demo_platform_proof,
|
||||
evaluate_action,
|
||||
get_channel,
|
||||
invoke_tool,
|
||||
list_services,
|
||||
make_event,
|
||||
resolve_identity,
|
||||
)
|
||||
from auto_client_acquisition.platform_services.action_ledger import ActionLedger
|
||||
from auto_client_acquisition.platform_services.channel_registry import channels_summary
|
||||
from auto_client_acquisition.platform_services.unified_inbox import CARD_TYPES, InboxCard
|
||||
|
||||
|
||||
# ── Service catalog ──────────────────────────────────────────
|
||||
def test_service_catalog_returns_all_services():
|
||||
out = list_services()
|
||||
assert out["total"] == len(SELLABLE_SERVICES) >= 12
|
||||
|
||||
|
||||
def test_service_catalog_includes_critical_services():
|
||||
out = list_services()
|
||||
keys = {s["key"] for s in out["services"]}
|
||||
for required in (
|
||||
"growth_operator_subscription", "channel_setup_service",
|
||||
"lead_intelligence_service", "partnership_sprint",
|
||||
"ai_visibility_aeo_sprint", "customer_success_operator",
|
||||
):
|
||||
assert required in keys
|
||||
|
||||
|
||||
# ── Channel registry ─────────────────────────────────────────
|
||||
def test_channels_include_core_channels():
|
||||
keys = {c.key for c in ALL_CHANNELS}
|
||||
for required in (
|
||||
"whatsapp", "gmail", "google_calendar", "moyasar",
|
||||
"linkedin_lead_forms", "x_api", "instagram_graph",
|
||||
"google_business_profile", "google_sheets", "crm", "website_forms",
|
||||
):
|
||||
assert required in keys
|
||||
|
||||
|
||||
def test_channels_summary_aggregates():
|
||||
s = channels_summary()
|
||||
assert s["total"] == len(ALL_CHANNELS)
|
||||
assert "by_beta_status" in s and "by_risk_level" in s
|
||||
|
||||
|
||||
def test_get_channel_unknown():
|
||||
assert get_channel("bogus_channel") is None
|
||||
|
||||
|
||||
def test_whatsapp_blocks_cold_send():
|
||||
"""Channel registry asserts cold send is blocked."""
|
||||
wa = get_channel("whatsapp")
|
||||
assert wa is not None
|
||||
assert "cold_send_without_consent" in wa.blocked_actions
|
||||
|
||||
|
||||
# ── Event bus ────────────────────────────────────────────────
|
||||
def test_event_types_include_payment_lifecycle():
|
||||
for et in ("payment.initiated", "payment.paid", "payment.failed"):
|
||||
assert et in EVENT_TYPES
|
||||
|
||||
|
||||
def test_make_event_validates():
|
||||
with pytest.raises(ValueError):
|
||||
make_event(event_type="totally.fake", channel="whatsapp", customer_id="c")
|
||||
|
||||
|
||||
def test_make_event_round_trip():
|
||||
e = make_event(
|
||||
event_type="lead.form_submitted", channel="website_forms",
|
||||
customer_id="c", payload={"company": "X"},
|
||||
)
|
||||
d = e.to_dict()
|
||||
assert d["event_type"] == "lead.form_submitted"
|
||||
assert d["payload"]["company"] == "X"
|
||||
|
||||
|
||||
# ── Action policy ────────────────────────────────────────────
|
||||
def test_policy_blocks_cold_whatsapp():
|
||||
d = evaluate_action(
|
||||
action="send_whatsapp",
|
||||
context={"source": "cold_list", "consent": False},
|
||||
)
|
||||
assert d.decision == "blocked"
|
||||
|
||||
|
||||
def test_policy_blocks_payment_without_confirmation():
|
||||
d = evaluate_action(
|
||||
action="charge_payment",
|
||||
context={"user_confirmed": False},
|
||||
)
|
||||
assert d.decision == "blocked"
|
||||
|
||||
|
||||
def test_policy_blocks_secrets_in_payload():
|
||||
d = evaluate_action(
|
||||
action="create_draft",
|
||||
context={"payload": {"api_key": "ghp_xxx"}},
|
||||
)
|
||||
assert d.decision == "blocked"
|
||||
|
||||
|
||||
def test_policy_external_send_needs_approval():
|
||||
d = evaluate_action(
|
||||
action="send_email",
|
||||
context={"approval_status": "pending"},
|
||||
)
|
||||
assert d.decision == "approval_required"
|
||||
|
||||
|
||||
def test_policy_calendar_insert_needs_approval():
|
||||
d = evaluate_action(
|
||||
action="calendar_insert_event",
|
||||
context={"approval_status": "pending"},
|
||||
)
|
||||
assert d.decision == "approval_required"
|
||||
|
||||
|
||||
def test_policy_high_value_deal_review():
|
||||
d = evaluate_action(
|
||||
action="send_whatsapp",
|
||||
context={
|
||||
"deal_value_sar": 250_000, "approval_status": "approved",
|
||||
"source": "existing_customer",
|
||||
},
|
||||
)
|
||||
assert d.decision == "approval_required"
|
||||
|
||||
|
||||
def test_policy_safe_internal_action_allowed():
|
||||
d = evaluate_action(action="read_data", context={})
|
||||
assert d.decision == "allow"
|
||||
|
||||
|
||||
# ── Tool gateway ─────────────────────────────────────────────
|
||||
def test_gateway_blocks_unsupported_tool():
|
||||
r = invoke_tool(tool="bogus.action")
|
||||
assert r.status == "unsupported"
|
||||
|
||||
|
||||
def test_gateway_blocks_cold_whatsapp():
|
||||
r = invoke_tool(
|
||||
tool="whatsapp.send_message",
|
||||
context={"source": "cold_list", "consent": False, "approval_status": "pending"},
|
||||
)
|
||||
assert r.status == "blocked"
|
||||
|
||||
|
||||
def test_gateway_external_send_default_draft_only():
|
||||
"""No live env flag → drafts even when policy allows."""
|
||||
import os
|
||||
os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
|
||||
r = invoke_tool(
|
||||
tool="whatsapp.send_message",
|
||||
context={
|
||||
"source": "existing_customer", "consent": True,
|
||||
"approval_status": "approved",
|
||||
},
|
||||
)
|
||||
# Either draft_created (no flag) or approval_required (defensive)
|
||||
assert r.status in ("draft_created", "approval_required")
|
||||
|
||||
|
||||
def test_gateway_internal_action_passes():
|
||||
r = invoke_tool(tool="gmail.read_thread", context={})
|
||||
assert r.status in ("draft_created", "approval_required")
|
||||
|
||||
|
||||
def test_gateway_payment_charge_needs_confirmation():
|
||||
r = invoke_tool(
|
||||
tool="moyasar.refund",
|
||||
context={"user_confirmed": False, "approval_status": "approved"},
|
||||
)
|
||||
assert r.status == "blocked"
|
||||
|
||||
|
||||
# ── Identity resolution ──────────────────────────────────────
|
||||
def test_identity_resolves_multi_signal():
|
||||
out = resolve_identity(signals=[
|
||||
{"phone": "+966500000001", "company": "X", "source": "wa"},
|
||||
{"email": "x@example.sa", "company": "X", "source": "gmail"},
|
||||
{"crm_id": "crm_1", "company": "X", "source": "crm"},
|
||||
])
|
||||
assert out.primary_phone == "+966500000001"
|
||||
assert out.primary_email == "x@example.sa"
|
||||
assert out.crm_id == "crm_1"
|
||||
assert out.confidence > 0
|
||||
assert "wa" in out.sources
|
||||
|
||||
|
||||
def test_identity_empty_signals():
|
||||
out = resolve_identity(signals=[])
|
||||
assert out.confidence == 0
|
||||
|
||||
|
||||
# ── Unified inbox ────────────────────────────────────────────
|
||||
def test_inbox_card_validates_button_count():
|
||||
with pytest.raises(ValueError):
|
||||
InboxCard(
|
||||
card_id="c", type="opportunity", channel="whatsapp",
|
||||
title_ar="x", summary_ar="x", why_it_matters_ar="x",
|
||||
recommended_action_ar="x", risk_level="low",
|
||||
buttons_ar=("a", "b", "c", "d"), # 4 → invalid
|
||||
)
|
||||
|
||||
|
||||
def test_inbox_card_validates_card_type():
|
||||
with pytest.raises(ValueError):
|
||||
InboxCard(
|
||||
card_id="c", type="bogus_type", channel="x",
|
||||
title_ar="x", summary_ar="x", why_it_matters_ar="x",
|
||||
recommended_action_ar="x", risk_level="low",
|
||||
)
|
||||
|
||||
|
||||
def test_build_card_from_event_payment_failed():
|
||||
e = make_event(
|
||||
event_type="payment.failed", channel="moyasar", customer_id="c",
|
||||
payload={"customer_id": "c1", "amount_sar": 2999},
|
||||
)
|
||||
card = build_card_from_event(e)
|
||||
assert card is not None
|
||||
assert card.type == "payment"
|
||||
assert len(card.buttons_ar) <= 3
|
||||
|
||||
|
||||
def test_build_card_from_event_review_low_rating_high_risk():
|
||||
e = make_event(
|
||||
event_type="review.created", channel="google_business_profile",
|
||||
customer_id="c", payload={"rating": 1, "text": "bad"},
|
||||
)
|
||||
card = build_card_from_event(e)
|
||||
assert card is not None
|
||||
assert card.risk_level == "high"
|
||||
|
||||
|
||||
def test_demo_feed_arabic_and_buttons_capped():
|
||||
feed = build_demo_feed()
|
||||
assert feed["feed_size"] >= 5
|
||||
for card in feed["cards"]:
|
||||
assert len(card["buttons_ar"]) <= 3
|
||||
# Has Arabic content somewhere
|
||||
text = card["title_ar"] + card["summary_ar"]
|
||||
assert any("" <= ch <= "ۿ" for ch in text)
|
||||
|
||||
|
||||
def test_card_types_cover_inbox_cases():
|
||||
assert {"opportunity", "email_lead", "whatsapp_reply", "payment",
|
||||
"meeting_prep", "review_response", "partner_suggestion"}.issubset(set(CARD_TYPES))
|
||||
|
||||
|
||||
# ── Action ledger ────────────────────────────────────────────
|
||||
def test_action_ledger_append_and_summary():
|
||||
led = ActionLedger()
|
||||
led.append(
|
||||
customer_id="c1", action_type="send_whatsapp",
|
||||
channel="whatsapp", stage="requested",
|
||||
)
|
||||
led.append(
|
||||
customer_id="c1", action_type="send_whatsapp",
|
||||
channel="whatsapp", stage="approved",
|
||||
)
|
||||
s = led.summary("c1")
|
||||
assert s["total"] == 2
|
||||
assert s["by_stage"]["requested"] == 1
|
||||
assert s["by_stage"]["approved"] == 1
|
||||
|
||||
|
||||
def test_action_ledger_unknown_stage_raises():
|
||||
led = ActionLedger()
|
||||
with pytest.raises(ValueError):
|
||||
led.append(customer_id="c1", action_type="x", channel="y", stage="bogus")
|
||||
|
||||
|
||||
# ── Platform proof ledger ────────────────────────────────────
|
||||
def test_platform_proof_demo_structure():
|
||||
p = build_demo_platform_proof()
|
||||
d = p.to_dict()
|
||||
assert "totals" in d and "by_channel" in d
|
||||
assert d["totals"]["leads_created"] > 0
|
||||
assert d["totals"]["risks_blocked"] > 0
|
||||
# Cross-channel coverage
|
||||
assert "whatsapp" in d["by_channel"] or "gmail" in d["by_channel"]
|
||||
Loading…
Reference in New Issue
Block a user