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:
Dealix Builder 2026-05-01 16:05:12 +03:00
parent 8942c6e84c
commit 4e969131c7
29 changed files with 3891 additions and 0 deletions

View File

@ -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)

View 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()}

View 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()

View File

@ -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 : signalactionoutcome 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",
]

View File

@ -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],
}

View File

@ -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,
},
}

View File

@ -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.",
}

View File

@ -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,
}

View File

@ -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(),
}

View File

@ -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)),
)

View File

@ -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 العميل + القنوات المربوطة.",
}

View File

@ -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,
}

View File

@ -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"},
],
)

View 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,
}

View 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+socialone 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",
]

View File

@ -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,
}

View File

@ -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)

View File

@ -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,
}

View 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,
)

View File

@ -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
)

View File

@ -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},
},
)

View File

@ -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],
}

View 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",
}

View File

@ -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.",
}

View File

@ -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).
---
**الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً.

View 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 الحقيقي.

View 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` على بيانات إنتاج مع تجربة عميل واحد.

View 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

View 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"]