From ef08649efe5b7532386ae7f570c190a1a1bf4aac Mon Sep 17 00:00:00 2001 From: Dealix Builder Date: Fri, 1 May 2026 17:50:32 +0300 Subject: [PATCH] =?UTF-8?q?feat(autonomous-revenue-os):=20Dealix=20becomes?= =?UTF-8?q?=20a=20Category=20=E2=80=94=20Autonomous=20Revenue=20Company=20?= =?UTF-8?q?OS=20=E2=80=94=2026=20modules=20+=2047=20endpoints=20+=2081=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Dealix is no longer "a platform". It is a new category: # An Autonomous Revenue Company OS that runs growth FOR Saudi businesses # as if Growth + Sales + Partnerships + Customer Success + Strategy + # Compliance + Data sat in one self-improving system. Autonomous Service Operator (16 modules) — البوت المركزي - intent_classifier: 16 supported intents (Arabic + English keywords; deterministic; no LLM) - conversation_router: route_message + handle_message — single entry point that classifies, routes to handler, recommends a bundle, builds intake + initial pipeline - session_state: 13 valid states + UUID-based sessions + audit history - intake_collector: per-intent intake question sets + parse + validation - approval_manager: Arabic approval cards (capped at 3 buttons) + decision processing (approve/edit/skip/reject including Arabic verbs) - service_orchestrator: 11-step canonical pipeline (intake→data_check→targeting→contactability→strategy→drafting→approval→execution_or_export→tracking→proof→upsell) - workflow_runner: advance + completion check - tool_action_planner: HARD-BLOCKS linkedin.scrape_profile, linkedin.auto_dm, linkedin.auto_connect, social.scrape_followers; high-risk tools require approval; draft-safe tools return draft_only; unknown tools default to approval_required - proof_pack_dispatcher: per-service Proof Pack envelope with required metrics - upsell_engine: 3 deterministic verdicts (upsell_now / iterate_first / gentle_upsell) based on csat + pipeline + meetings - whatsapp_renderer: render any card / approval / daily brief as WhatsApp draft (≤3 buttons, Arabic body, never live) - operator_memory: in-process sessions + customer_facts + preferences + audit log (production = Supabase) - service_bundles: 6 customer-facing bundles instead of 20 raw services (Growth Starter, Data to Revenue, Executive Growth OS, Partnership Growth, Local Growth OS, Full Growth Control Tower) - executive_mode: CEO command center + daily brief + revenue risks (3) + next 3 moves - client_mode: Growth Manager dashboard with 4 panels - agency_mode: multi-client roster + co-branded Proof Pack + revenue share calc Revenue Company OS (10 modules) — الذكاء عبر القنوات - event_to_card: 13 event types → Arabic decision cards (email/whatsapp/form/review/payment/risk/partner/meeting/service.completed/...) each with title_ar/summary_ar/why_now_ar/recommended_action_ar/risk_level/buttons_ar (≤3) - command_feed_engine: aggregate events for a customer + sort by risk (high first) + by_type and by_risk counts - action_graph: 14 typed edges (signal_created_opportunity → message_triggered_reply → reply_led_to_meeting → meeting_led_to_proposal → proposal_led_to_payment → ...) with what_works_for_customer scoring (outcome edges weigh more) - revenue_work_units: 19 RWU types (Salesforce-inspired): opportunity_created, draft_created, approval_collected, meeting_drafted, payment_received, risk_blocked, etc. + aggregate_work_units (counts/revenue/risks) - channel_health: cross-channel reputation snapshot (email/whatsapp/linkedin) + overall_score + channels_at_risk - opportunity_factory: turn (sector, city) into 5 opportunity cards via targeting_os.recommend_accounts + buying committee - service_factory: instantiate any service for a customer (intake + workflow + quote) - proof_ledger (revenue-tier, NOT platform_services.proof_ledger): customer-facing scoreboard with totals + summary_ar + by_type breakdown - growth_memory: anonymized cross-customer aggregates — sector_message_winrate, sector_channel_winrate, common_objections, blocked_action_reasons, successful_playbooks; best_message_for_sector + best_channel_for_sector - self_improvement_loop: weekly Arabic recommendations from real metrics (approval_rate, reply_rate, meeting_rate, blocked_actions, service_revenue) + best_service_id + next_experiment Routers (2 new) — 47 endpoints - /api/v1/operator/* (28): chat (message/decision/classify), sessions (new/transition/context/get), cards (approval/whatsapp/render), intake (questions/validate), service (start), tools (plan), proof-pack (dispatch), upsell (recommend/card), bundles (list/recommend), modes (ceo/ceo-daily-brief/ceo-risks/client/agency/agency-add-client/agency-revenue-share/agency-co-branded-proof), demos (whatsapp-daily-brief/proof-pack) - /api/v1/revenue-os/* (19): command-feed (demo/build/events-ingest), work-units (types/build/aggregate/demo), proof-ledger/demo, action-graph (edge-types/demo), channel-health (snapshot/demo), opportunity-factory (run/demo), service-factory (instantiate/demo), growth-memory/demo, self-improvement (weekly-report/demo) Tests (2 new files, 81 tests) - test_autonomous_service_operator.py: 50 tests * 8 intent classification tests (want_more_customers, has_contact_list, partnerships, whatsapp, pricing, approve, unknown fallback) * 4 conversation router (recommends correct service per intent + bundle, processes approval decisions) * 4 session lifecycle (UUID, transition validation, memory store, context build) * 4 intake (questions per intent, validation detects missing fields, complete intake passes) * 4 approval (≤3 buttons, approve/skip Arabic, unknown decision returns error) * 5 tool planner (linkedin scrape blocked, auto_dm blocked, high-risk → approval, draft-safe → draft_only, unknown → approval_required) * 4 bundles (6 total, agency → partnership_growth, local → local_growth_os, default → growth_starter) * 7 modes (CEO Arabic, daily brief 3 decisions, 3 risks, client panels, agency aggregation, revenue share calc, co-branded includes both names) * 3 WhatsApp renderer (no live send, ≤3 buttons, Arabic morning text) * 4 proof + upsell (Proof Pack draft, upsell_now for strong, iterate_first for weak, ≤3 buttons) - test_revenue_company_os.py: 31 tests * 4 event → card (email Arabic, low review high-risk, risk.blocked high, unknown → action_required) * 3 command feed (demo 8 events, sorts high-risk first, empty handling) * 4 RWUs (≥18 types, build validates, aggregate sums revenue, risks_blocked counted) * 4 action graph (≥12 edge types, validates type, demo 2 customers, what_works scoring) * 2 channel health (returns score, flags risky channel) * 2 opportunity factory (5 opps no live send, blocks unsafe in notes) * 3 service factory (instantiate known + unknown errors, demo 4 services) * 3 proof ledger (appends, rejects unknown, demo has revenue + risks) * 2 growth memory (top objections, best message per sector) * 3 self-improvement (low approval recommends fix, high blocked recommends review, returns best service) Docs (1 new + 1 updated) - AUTONOMOUS_REVENUE_COMPANY_OS.md (Arabic): 12-layer architecture + service bundles + safety + endpoints + competitive positioning - DEALIX_100_PERCENT_LAUNCH_PLAN.md: added §44 Autonomous Revenue Company OS Test results - 81/81 new tests pass - Full suite: 905 passed, 2 skipped (missing API keys, unrelated) - 0 existing tests broken Safety + integration - All 47 new endpoints: live_send_allowed=False, approval_required=True - LinkedIn scrape/auto-DM/auto-connect HARD-BLOCKED in tool_action_planner - High-risk tools (whatsapp.send_message, gmail.send, calendar.insert_event, moyasar.charge, gbp.publish_review_reply, social.publish_dm, social.publish_post) → approval_required forced - Cold WhatsApp blocked via existing contactability_matrix - Operator memory hashes nothing yet — production must wire to security_curator.trace_redactor before any persistence - 6 bundles unify the 12 productized services from Service Tower - Modes integrate platform_services + intelligence_layer + service_excellence - Action Graph + Revenue Work Units + Proof Ledger together form Dealix's Saudi Revenue Graph - Self-improvement loop reads metrics that flow from agent_observability + growth_curator Integration with everything before - Autonomous Service Operator orchestrates Service Tower, Service Excellence OS, Targeting OS, Platform Services, Intelligence Layer - Revenue Company OS reads from platform_services event_bus + intelligence_layer mission_engine + targeting_os reputation_guard - Service factory uses service_tower.get_service + build_intake_questions + quote_service - Opportunity factory uses targeting_os.recommend_accounts + map_buying_committee - Channel health uses targeting_os.calculate_channel_reputation - Tool planner integrates with platform_services.tool_gateway policies - WhatsApp renderer aligns with launch_ops button caps - Bundles map to service_tower upgrade_paths Co-Authored-By: Claude Opus 4.7 (1M context) --- dealix/api/main.py | 4 + .../routers/autonomous_service_operator.py | 304 ++++++++++++++ dealix/api/routers/revenue_company_os.py | 172 ++++++++ .../autonomous_service_operator/__init__.py | 125 ++++++ .../agency_mode.py | 133 ++++++ .../approval_manager.py | 87 ++++ .../client_mode.py | 55 +++ .../conversation_router.py | 114 ++++++ .../executive_mode.py | 92 +++++ .../intake_collector.py | 129 ++++++ .../intent_classifier.py | 180 +++++++++ .../operator_memory.py | 104 +++++ .../proof_pack_dispatcher.py | 72 ++++ .../service_bundles.py | 215 ++++++++++ .../service_orchestrator.py | 94 +++++ .../session_state.py | 95 +++++ .../tool_action_planner.py | 102 +++++ .../upsell_engine.py | 94 +++++ .../whatsapp_renderer.py | 75 ++++ .../workflow_runner.py | 43 ++ .../revenue_company_os/__init__.py | 67 ++++ .../revenue_company_os/action_graph.py | 123 ++++++ .../revenue_company_os/channel_health.py | 58 +++ .../revenue_company_os/command_feed_engine.py | 61 +++ .../revenue_company_os/event_to_card.py | 172 ++++++++ .../revenue_company_os/growth_memory.py | 108 +++++ .../revenue_company_os/opportunity_factory.py | 54 +++ .../revenue_company_os/proof_ledger.py | 130 ++++++ .../revenue_company_os/revenue_work_units.py | 95 +++++ .../self_improvement_loop.py | 97 +++++ .../revenue_company_os/service_factory.py | 54 +++ dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md | 200 +++++++++ dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md | 26 ++ .../unit/test_autonomous_service_operator.py | 379 ++++++++++++++++++ dealix/tests/unit/test_revenue_company_os.py | 253 ++++++++++++ 35 files changed, 4166 insertions(+) create mode 100644 dealix/api/routers/autonomous_service_operator.py create mode 100644 dealix/api/routers/revenue_company_os.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/__init__.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/session_state.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py create mode 100644 dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/__init__.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/action_graph.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/channel_health.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/event_to_card.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/growth_memory.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py create mode 100644 dealix/auto_client_acquisition/revenue_company_os/service_factory.py create mode 100644 dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md create mode 100644 dealix/tests/unit/test_autonomous_service_operator.py create mode 100644 dealix/tests/unit/test_revenue_company_os.py diff --git a/dealix/api/main.py b/dealix/api/main.py index 619e828d..be9ad39d 100644 --- a/dealix/api/main.py +++ b/dealix/api/main.py @@ -17,6 +17,7 @@ from api.routers import ( admin, agent_observability, agents, + autonomous_service_operator, automation, autonomous, business, @@ -45,6 +46,7 @@ from api.routers import ( prospect, public, revenue, + revenue_company_os, revenue_launch, revenue_os, sales, @@ -174,6 +176,8 @@ def create_app() -> FastAPI: app.include_router(service_excellence.router) app.include_router(launch_ops.router) app.include_router(revenue_launch.router) + app.include_router(autonomous_service_operator.router) + app.include_router(revenue_company_os.router) app.include_router(public.router) app.include_router(admin.router) diff --git a/dealix/api/routers/autonomous_service_operator.py b/dealix/api/routers/autonomous_service_operator.py new file mode 100644 index 00000000..f6591cfc --- /dev/null +++ b/dealix/api/routers/autonomous_service_operator.py @@ -0,0 +1,304 @@ +"""Autonomous Service Operator router — chat + decisions + sessions + bundles.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body, HTTPException + +from auto_client_acquisition.autonomous_service_operator import ( + OperatorMemory, + add_agency_client, + build_agency_dashboard, + build_approval_card, + build_ceo_command_center, + build_client_dashboard, + build_co_branded_proof_pack, + build_executive_daily_brief, + build_intake_questions_for_intent, + build_new_session, + build_revenue_risks_summary, + build_service_pipeline, + build_session_context, + build_upsell_card, + classify_intent, + dispatch_proof_pack, + handle_message, + list_bundles, + list_agency_revenue_share, + plan_tool_action, + process_approval_decision, + recommend_bundle, + recommend_upsell_after_service, + render_approval_card_for_whatsapp, + render_card_for_whatsapp, + render_daily_brief_for_whatsapp, + transition_session, + validate_intake_completeness, +) + +router = APIRouter(prefix="/api/v1/operator", tags=["autonomous-service-operator"]) + +# Process-level memory (demo). Production = Redis/Supabase. +_MEMORY = OperatorMemory() + + +# ── Chat ───────────────────────────────────────────────────── +@router.post("/chat/message") +async def chat_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + """Send a message to the operator. Classifies intent + recommends action.""" + return handle_message( + message=payload.get("message", ""), + customer_id=payload.get("customer_id"), + has_contact_list=bool(payload.get("has_contact_list", False)), + is_agency=bool(payload.get("is_agency", False)), + is_local_business=bool(payload.get("is_local_business", False)), + budget_sar=int(payload.get("budget_sar", 1000)), + ) + + +@router.post("/chat/decision") +async def chat_decision(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + """Process an approval/edit/skip decision on an action card.""" + card = payload.get("card") or build_approval_card( + action_type="example", + title_ar="فعل مثال", + summary_ar="مثال", + ) + return process_approval_decision( + card, + decision=payload.get("decision", "skip"), + decided_by=payload.get("decided_by", "user"), + note=payload.get("note", ""), + ) + + +@router.post("/chat/classify") +async def chat_classify(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return classify_intent(payload.get("message", "")) + + +# ── Sessions ───────────────────────────────────────────────── +@router.post("/sessions/new") +async def sessions_new(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + session = build_new_session(customer_id=payload.get("customer_id")) + _MEMORY.upsert_session(session) + return session.to_dict() + + +@router.get("/sessions/{session_id}") +async def sessions_get(session_id: str) -> dict[str, Any]: + session = _MEMORY.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="session not found") + return session.to_dict() + + +@router.post("/sessions/{session_id}/transition") +async def sessions_transition( + session_id: str, + payload: dict[str, Any] = Body(...), +) -> dict[str, Any]: + session = _MEMORY.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="session not found") + transition_session( + session, + new_state=payload.get("new_state", "new"), + note=payload.get("note", ""), + ) + return session.to_dict() + + +@router.get("/sessions/{session_id}/context") +async def sessions_context(session_id: str) -> dict[str, Any]: + return build_session_context(memory=_MEMORY, session_id=session_id) + + +# ── Cards / Approvals ──────────────────────────────────────── +@router.post("/cards/approval") +async def cards_approval(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_approval_card( + action_type=payload.get("action_type", "unknown"), + title_ar=payload.get("title_ar", ""), + summary_ar=payload.get("summary_ar", ""), + risk_level=payload.get("risk_level", "low"), + why_now_ar=payload.get("why_now_ar", ""), + recommended_action_ar=payload.get("recommended_action_ar", ""), + expected_impact_sar=float(payload.get("expected_impact_sar", 0)), + service_id=payload.get("service_id"), + customer_id=payload.get("customer_id"), + action_id=payload.get("action_id"), + ) + + +@router.post("/cards/whatsapp/render") +async def cards_whatsapp_render(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + kind = payload.get("kind", "card") + if kind == "approval": + return render_approval_card_for_whatsapp(payload.get("card") or {}) + if kind == "daily_brief": + return render_daily_brief_for_whatsapp(payload.get("brief") or {}) + return render_card_for_whatsapp(payload.get("card") or {}) + + +# ── Intake ─────────────────────────────────────────────────── +@router.get("/intake/questions/{intent}") +async def intake_questions(intent: str) -> dict[str, Any]: + return build_intake_questions_for_intent(intent) + + +@router.post("/intake/validate") +async def intake_validate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return validate_intake_completeness( + payload.get("intent", "ask_services"), + payload.get("payload") or {}, + ) + + +# ── Service workflow ───────────────────────────────────────── +@router.post("/service/start") +async def service_start(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_service_pipeline( + service_id=payload.get("service_id", ""), + customer_id=payload.get("customer_id", ""), + ) + + +@router.post("/tools/plan") +async def tools_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return plan_tool_action( + tool=payload.get("tool", ""), + payload=payload.get("payload"), + customer_id=payload.get("customer_id"), + context=payload.get("context"), + ) + + +# ── Proof + Upsell ─────────────────────────────────────────── +@router.post("/proof-pack/dispatch") +async def proof_pack_dispatch(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return dispatch_proof_pack( + service_id=payload.get("service_id", ""), + customer_id=payload.get("customer_id"), + channel=payload.get("channel", "email"), + metrics=payload.get("metrics"), + ) + + +@router.post("/upsell/recommend") +async def upsell_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return recommend_upsell_after_service( + completed_service_id=payload.get("completed_service_id", ""), + pilot_metrics=payload.get("pilot_metrics"), + ) + + +@router.post("/upsell/card") +async def upsell_card(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_upsell_card( + completed_service_id=payload.get("completed_service_id", ""), + pilot_metrics=payload.get("pilot_metrics"), + ) + + +# ── Bundles ────────────────────────────────────────────────── +@router.get("/bundles") +async def bundles() -> dict[str, Any]: + return list_bundles() + + +@router.post("/bundles/recommend") +async def bundles_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return recommend_bundle( + intent=payload.get("intent"), + has_contact_list=bool(payload.get("has_contact_list", False)), + is_agency=bool(payload.get("is_agency", False)), + is_local_business=bool(payload.get("is_local_business", False)), + budget_sar=int(payload.get("budget_sar", 1000)), + ) + + +# ── Modes ──────────────────────────────────────────────────── +@router.post("/mode/ceo") +async def mode_ceo(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_ceo_command_center( + company_name=payload.get("company_name", ""), + sector=payload.get("sector", "saas"), + ) + + +@router.post("/mode/ceo/daily-brief") +async def mode_ceo_daily(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_executive_daily_brief( + company_name=payload.get("company_name", ""), + sector=payload.get("sector", "saas"), + ) + + +@router.post("/mode/ceo/risks") +async def mode_ceo_risks() -> dict[str, Any]: + return build_revenue_risks_summary() + + +@router.post("/mode/client") +async def mode_client(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_client_dashboard( + customer_id=payload.get("customer_id", ""), + company_name=payload.get("company_name", ""), + active_services=payload.get("active_services") or [], + open_actions=int(payload.get("open_actions", 0)), + proof_pack_due=bool(payload.get("proof_pack_due", False)), + ) + + +@router.post("/mode/agency") +async def mode_agency(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_agency_dashboard( + agency_id=payload.get("agency_id", "agency_demo"), + agency_name=payload.get("agency_name", ""), + clients=payload.get("clients") or [], + ) + + +@router.post("/mode/agency/add-client") +async def mode_agency_add_client(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return add_agency_client( + agency_id=payload.get("agency_id", "agency_demo"), + client_company_name=payload.get("client_company_name", ""), + sector=payload.get("sector", ""), + monthly_subscription_sar=int(payload.get("monthly_subscription_sar", 0)), + revenue_share_pct=int(payload.get("revenue_share_pct", 20)), + ) + + +@router.post("/mode/agency/revenue-share") +async def mode_agency_revenue_share(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return list_agency_revenue_share(clients=payload.get("clients") or []) + + +@router.post("/mode/agency/co-branded-proof") +async def mode_agency_co_branded_proof(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_co_branded_proof_pack( + agency_name=payload.get("agency_name", ""), + client_company_name=payload.get("client_company_name", ""), + metrics=payload.get("metrics"), + ) + + +# ── Demos ──────────────────────────────────────────────────── +@router.get("/whatsapp/daily-brief/demo") +async def whatsapp_daily_brief_demo() -> dict[str, Any]: + brief = build_executive_daily_brief(company_name="Acme") + return render_daily_brief_for_whatsapp(brief) + + +@router.get("/proof-pack/demo") +async def proof_pack_demo() -> dict[str, Any]: + return dispatch_proof_pack( + service_id="first_10_opportunities_sprint", + customer_id="demo", + metrics={"opportunities_generated": 10, "drafts_approved": 6, + "meetings_drafted": 2, "pipeline_influenced_sar": 30000, + "risks_blocked": 3}, + ) diff --git a/dealix/api/routers/revenue_company_os.py b/dealix/api/routers/revenue_company_os.py new file mode 100644 index 00000000..1214f94a --- /dev/null +++ b/dealix/api/routers/revenue_company_os.py @@ -0,0 +1,172 @@ +"""Revenue Company OS router — command feed + work units + proof + memory.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body + +from auto_client_acquisition.revenue_company_os import ( + REVENUE_EDGE_TYPES, + REVENUE_WORK_UNIT_TYPES, + aggregate_work_units, + build_card_from_event, + build_channel_health_snapshot, + build_command_feed_for_customer, + build_growth_memory_demo, + build_opportunity_factory_demo, + build_revenue_action_graph_demo, + build_revenue_proof_ledger_demo, + build_revenue_work_unit, + build_service_factory_demo, + build_weekly_self_improvement_report, + instantiate_service, + revenue_os_command_feed_demo, +) + +router = APIRouter(prefix="/api/v1/revenue-os", tags=["revenue-company-os"]) + + +# ── Command Feed ───────────────────────────────────────────── +@router.get("/command-feed/demo") +async def command_feed_demo() -> dict[str, Any]: + return revenue_os_command_feed_demo() + + +@router.post("/events/ingest") +async def events_ingest(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + """Convert one event → Arabic decision card. Never executes anything.""" + return build_card_from_event(payload) + + +@router.post("/command-feed/build") +async def command_feed_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_command_feed_for_customer( + customer_id=payload.get("customer_id", "demo"), + events=payload.get("events", []), + ) + + +# ── Work Units ─────────────────────────────────────────────── +@router.get("/work-units/types") +async def work_unit_types() -> dict[str, Any]: + return {"types": list(REVENUE_WORK_UNIT_TYPES)} + + +@router.post("/work-units/build") +async def work_units_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + try: + return build_revenue_work_unit( + unit_type=payload.get("unit_type", ""), + service_id=payload.get("service_id", ""), + customer_id=payload.get("customer_id", ""), + risk_level=payload.get("risk_level", "low"), + revenue_influenced_sar=float(payload.get("revenue_influenced_sar", 0)), + proof_event=payload.get("proof_event", ""), + notes=payload.get("notes", ""), + ) + except ValueError as exc: + return {"error": str(exc)} + + +@router.post("/work-units/aggregate") +async def work_units_aggregate( + units: list[dict[str, Any]] = Body(default_factory=list, embed=True), +) -> dict[str, Any]: + return aggregate_work_units(units) + + +@router.get("/work-units/demo") +async def work_units_demo() -> dict[str, Any]: + """Demo aggregation across 12 sample units.""" + return build_revenue_proof_ledger_demo() + + +# ── Proof Ledger ───────────────────────────────────────────── +@router.get("/proof-ledger/demo") +async def proof_ledger_demo() -> dict[str, Any]: + return build_revenue_proof_ledger_demo() + + +# ── Action Graph ───────────────────────────────────────────── +@router.get("/action-graph/edge-types") +async def action_graph_edge_types() -> dict[str, Any]: + return {"edge_types": list(REVENUE_EDGE_TYPES)} + + +@router.get("/action-graph/demo") +async def action_graph_demo() -> dict[str, Any]: + return build_revenue_action_graph_demo() + + +# ── Channel Health ─────────────────────────────────────────── +@router.post("/channel-health/snapshot") +async def channel_health_snapshot(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_channel_health_snapshot( + metrics_per_channel=payload.get("metrics_per_channel"), + ) + + +@router.get("/channel-health/demo") +async def channel_health_demo() -> dict[str, Any]: + return build_channel_health_snapshot() + + +# ── Opportunity Factory ────────────────────────────────────── +@router.post("/opportunity-factory") +async def opportunity_factory(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_opportunity_factory_demo( + sector=payload.get("sector", "training"), + city=payload.get("city", "Riyadh"), + limit=int(payload.get("limit", 5)), + ) + + +@router.get("/opportunity-factory/demo") +async def opportunity_factory_demo() -> dict[str, Any]: + return build_opportunity_factory_demo() + + +# ── Service Factory ────────────────────────────────────────── +@router.post("/service-factory") +async def service_factory(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return instantiate_service( + service_id=payload.get("service_id", ""), + customer_id=payload.get("customer_id", ""), + company_size=payload.get("company_size", "small"), + urgency=payload.get("urgency", "normal"), + ) + + +@router.get("/service-factory/demo") +async def service_factory_demo() -> dict[str, Any]: + return build_service_factory_demo() + + +# ── Growth Memory ──────────────────────────────────────────── +@router.get("/growth-memory/demo") +async def growth_memory_demo() -> dict[str, Any]: + return build_growth_memory_demo() + + +# ── Self-Improvement Loop ──────────────────────────────────── +@router.post("/self-improvement/weekly-report") +async def self_improvement_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_weekly_self_improvement_report(weekly_metrics=payload) + + +@router.get("/self-improvement/demo") +async def self_improvement_demo() -> dict[str, Any]: + return build_weekly_self_improvement_report(weekly_metrics={ + "approval_rate": 0.42, + "reply_rate": 0.05, + "meeting_rate": 0.018, + "blocked_actions": 12, + "service_revenue_sar": { + "first_10_opportunities_sprint": 1500, + "list_intelligence": 999, + "growth_os_monthly": 2999, + }, + "top_objections": ["price", "timing"], + "channel_outcomes": {"email": "healthy", "whatsapp": "watch"}, + }) diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py b/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py new file mode 100644 index 00000000..15255081 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py @@ -0,0 +1,125 @@ +"""Autonomous Service Operator — البوت المركزي الذي يدير الخدمات. + +Not a chatbot — a **service operator**: understands the customer's goal, +recommends a service, collects intake, runs workflow, requests approval, +delivers Proof Pack, suggests upgrade. +""" + +from __future__ import annotations + +from .agency_mode import ( + add_agency_client, + build_agency_dashboard, + build_co_branded_proof_pack, + list_agency_revenue_share, +) +from .approval_manager import ( + APPROVAL_STATES, + build_approval_card, + process_approval_decision, +) +from .client_mode import ( + build_client_dashboard, + build_client_session_summary, +) +from .conversation_router import ( + INTENT_TO_HANDLER, + handle_message, + route_message, +) +from .executive_mode import ( + build_ceo_command_center, + build_executive_daily_brief, + build_revenue_risks_summary, +) +from .intake_collector import ( + build_intake_questions_for_intent, + parse_intake_payload, + validate_intake_completeness, +) +from .intent_classifier import ( + SUPPORTED_INTENTS, + classify_intent, + intent_to_service, +) +from .operator_memory import ( + OperatorMemory, + build_session_context, +) +from .proof_pack_dispatcher import ( + dispatch_proof_pack, + proof_pack_for_service, +) +from .service_bundles import ( + BUNDLES, + get_bundle, + list_bundles, + recommend_bundle, +) +from .service_orchestrator import ( + SERVICE_PIPELINE_STEPS, + build_service_pipeline, + run_service_step, +) +from .session_state import ( + SessionState, + build_new_session, + transition_session, +) +from .tool_action_planner import ( + plan_tool_action, + review_planned_action, +) +from .upsell_engine import ( + build_upsell_card, + recommend_upsell_after_service, +) +from .whatsapp_renderer import ( + render_approval_card_for_whatsapp, + render_card_for_whatsapp, + render_daily_brief_for_whatsapp, +) +from .workflow_runner import ( + advance_workflow, + build_workflow_state, + is_workflow_complete, +) + +__all__ = [ + # conversation_router + "INTENT_TO_HANDLER", "handle_message", "route_message", + # intent_classifier + "SUPPORTED_INTENTS", "classify_intent", "intent_to_service", + # service_orchestrator + "SERVICE_PIPELINE_STEPS", "build_service_pipeline", "run_service_step", + # session_state + "SessionState", "build_new_session", "transition_session", + # intake_collector + "build_intake_questions_for_intent", "parse_intake_payload", + "validate_intake_completeness", + # approval_manager + "APPROVAL_STATES", "build_approval_card", "process_approval_decision", + # workflow_runner + "advance_workflow", "build_workflow_state", "is_workflow_complete", + # tool_action_planner + "plan_tool_action", "review_planned_action", + # proof_pack_dispatcher + "dispatch_proof_pack", "proof_pack_for_service", + # upsell_engine + "build_upsell_card", "recommend_upsell_after_service", + # whatsapp_renderer + "render_approval_card_for_whatsapp", "render_card_for_whatsapp", + "render_daily_brief_for_whatsapp", + # operator_memory + "OperatorMemory", "build_session_context", + # service_bundles + "BUNDLES", "get_bundle", "list_bundles", "recommend_bundle", + # executive_mode + "build_ceo_command_center", "build_executive_daily_brief", + "build_revenue_risks_summary", + # client_mode + "build_client_dashboard", "build_client_session_summary", + # agency_mode + "add_agency_client", "build_agency_dashboard", + "build_co_branded_proof_pack", "list_agency_revenue_share", +] diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py new file mode 100644 index 00000000..d24b11f0 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py @@ -0,0 +1,133 @@ +"""Agency Mode — manage multiple clients + co-branded Proof Pack + revenue share.""" + +from __future__ import annotations + +from typing import Any + + +def add_agency_client( + *, + agency_id: str, + client_company_name: str, + sector: str = "", + monthly_subscription_sar: int = 0, + revenue_share_pct: int = 20, + clients: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Add a new client to an agency's roster + return the entry.""" + entry: dict[str, Any] = { + "agency_id": agency_id, + "client_company_name": client_company_name, + "sector": sector, + "monthly_subscription_sar": int(monthly_subscription_sar), + "revenue_share_pct": int(revenue_share_pct), + "status": "onboarding", + "co_branded_proof_pack": True, + "approval_required": True, + } + if clients is not None: + clients.append(entry) + return entry + + +def build_agency_dashboard( + *, + agency_id: str, + agency_name: str = "", + clients: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Build the agency's dashboard summary.""" + clients = clients or [] + total_clients = len(clients) + active = sum(1 for c in clients if c.get("status") in ("active", "onboarding")) + monthly_revenue_total = sum( + float(c.get("monthly_subscription_sar", 0) or 0) for c in clients + ) + avg_share_pct = ( + round( + sum(int(c.get("revenue_share_pct", 0) or 0) for c in clients) + / max(1, total_clients), + 1, + ) + if total_clients else 0.0 + ) + + return { + "mode": "agency", + "agency_id": agency_id, + "agency_name": agency_name, + "metrics": { + "total_clients": total_clients, + "active_clients": active, + "monthly_revenue_sar": round(monthly_revenue_total, 2), + "avg_revenue_share_pct": avg_share_pct, + }, + "summary_ar": [ + f"عملاء الوكالة: {total_clients} (نشط: {active}).", + f"الإيراد الشهري الكلي: {monthly_revenue_total:.0f} ريال.", + f"متوسط revenue share: {avg_share_pct}%.", + ], + "panels_ar": [ + "Add Client — إضافة عميل جديد", + "Run Diagnostic — تشخيص لعميل", + "Co-Branded Proof Pack — Proof بعلامة الوكالة", + "Referral Tracking — متابعة الإحالات", + "Partner Scorecard — تقييم الأداء", + ], + "approval_required": True, + "live_send_allowed": False, + } + + +def list_agency_revenue_share( + *, clients: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Compute revenue share owed to an agency for the current month.""" + clients = clients or [] + line_items: list[dict[str, Any]] = [] + total_share_sar = 0.0 + for c in clients: + sub = float(c.get("monthly_subscription_sar", 0) or 0) + pct = int(c.get("revenue_share_pct", 0) or 0) + share = round(sub * pct / 100.0, 2) + total_share_sar += share + line_items.append({ + "client_company_name": c.get("client_company_name"), + "monthly_subscription_sar": sub, + "revenue_share_pct": pct, + "agency_share_sar": share, + }) + return { + "line_items": line_items, + "total_share_sar": round(total_share_sar, 2), + "currency": "SAR", + } + + +def build_co_branded_proof_pack( + *, + agency_name: str, + client_company_name: str, + metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build a co-branded Proof Pack envelope for an agency client.""" + metrics = metrics or {} + return { + "title_ar": ( + f"Proof Pack — {client_company_name} (تنفيذ: {agency_name})" + ), + "co_branded": True, + "agency_name": agency_name, + "client_company_name": client_company_name, + "sections_ar": [ + "ملخص تنفيذي للعميل", + "ما عملته الوكالة + Dealix", + "النتائج بالأرقام", + "Action Ledger", + "المخاطر التي منعتها الوكالة", + "التوصية بالخطوة التالية", + ], + "metrics": dict(metrics), + "approval_required": True, + "live_send_allowed": False, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py b/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py new file mode 100644 index 00000000..04346cf7 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py @@ -0,0 +1,87 @@ +"""Approval manager — Arabic approval cards (≤3 buttons) + decision processing.""" + +from __future__ import annotations + +from typing import Any + +APPROVAL_STATES: tuple[str, ...] = ( + "pending", + "approved", + "edited", + "rejected", + "expired", +) + + +def build_approval_card( + *, + action_type: str, + title_ar: str, + summary_ar: str, + risk_level: str = "low", + why_now_ar: str = "", + recommended_action_ar: str = "", + expected_impact_sar: float = 0.0, + service_id: str | None = None, + customer_id: str | None = None, + action_id: str | None = None, +) -> dict[str, Any]: + """Build a structured Arabic approval card.""" + return { + "type": "approval", + "action_id": action_id, + "action_type": action_type, + "service_id": service_id, + "customer_id": customer_id, + "title_ar": title_ar[:140], + "summary_ar": summary_ar[:280], + "why_now_ar": why_now_ar[:200], + "recommended_action_ar": recommended_action_ar[:200], + "risk_level": risk_level if risk_level in ( + "low", "medium", "high", + ) else "medium", + "expected_impact_sar": float(expected_impact_sar), + "buttons_ar": ["اعتمد", "عدّل", "تخطي"], + "state": "pending", + "approval_required": True, + "live_send_allowed": False, + } + + +def process_approval_decision( + card: dict[str, Any], + *, + decision: str, + decided_by: str = "user", + note: str = "", +) -> dict[str, Any]: + """ + Process an approval decision (`approve` / `edit` / `skip` / `reject`). + + Returns the updated card with new state + audit info. + """ + decision_lc = (decision or "").strip().lower() + if decision_lc in ("approve", "approved", "موافق", "اعتمد", "نعم"): + new_state = "approved" + next_action = "execute_with_audit" + elif decision_lc in ("edit", "عدّل", "تعديل"): + new_state = "edited" + next_action = "rewrite_then_resend_for_approval" + elif decision_lc in ("skip", "تخطي", "تجاوز"): + new_state = "rejected" + next_action = "archive" + elif decision_lc in ("reject", "ارفض", "لا"): + new_state = "rejected" + next_action = "archive_with_reason" + else: + return { + "error": f"unknown decision: {decision}", + "valid_decisions": ["approve", "edit", "skip", "reject"], + } + + out = dict(card) + out["state"] = new_state + out["decided_by"] = decided_by + out["decision_note"] = note[:200] + out["next_action"] = next_action + return out diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py new file mode 100644 index 00000000..548c8f56 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py @@ -0,0 +1,55 @@ +"""Client Mode — dashboard for the customer (Growth Manager) view.""" + +from __future__ import annotations + +from typing import Any + + +def build_client_dashboard( + *, + customer_id: str = "", + company_name: str = "", + active_services: list[str] | None = None, + open_actions: int = 0, + proof_pack_due: bool = False, +) -> dict[str, Any]: + """Build the client-facing dashboard.""" + active_services = active_services or [] + return { + "mode": "client", + "customer_id": customer_id, + "company_name": company_name, + "active_services": list(active_services), + "open_actions": open_actions, + "proof_pack_due": proof_pack_due, + "today_panels_ar": [ + "Command Feed — قرارات اليوم", + "Approvals Center — رسائل تنتظر اعتمادك", + "Pipeline Tracker — مرحلة كل عميل", + "Proof Pack — آخر تقرير + الـ ROI", + ], + "buttons_ar": ["اعرض القرارات", "اعتمد جماعي", "افتح Proof Pack"], + "approval_required": True, + "live_send_allowed": False, + } + + +def build_client_session_summary( + *, + session_id: str, + customer_id: str = "", + last_intent: str = "", + last_recommended_service: str = "", +) -> dict[str, Any]: + """Build a session summary for the client view.""" + return { + "mode": "client", + "session_id": session_id, + "customer_id": customer_id, + "last_intent": last_intent, + "last_recommended_service": last_recommended_service, + "next_step_ar": ( + "أكمل الـ intake للحصول على workflow الخدمة + أول Proof Pack." + ), + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py b/dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py new file mode 100644 index 00000000..1cddb566 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py @@ -0,0 +1,114 @@ +"""Conversation router — single entry point for any operator message.""" + +from __future__ import annotations + +from typing import Any + +from .approval_manager import ( + build_approval_card, + process_approval_decision, +) +from .intake_collector import build_intake_questions_for_intent +from .intent_classifier import classify_intent, intent_to_service +from .service_bundles import recommend_bundle +from .service_orchestrator import build_service_pipeline + + +# Map: intent → handler name +INTENT_TO_HANDLER: dict[str, str] = { + "want_more_customers": "start_first_10_opportunities", + "has_contact_list": "start_list_intelligence", + "want_partnerships": "start_partner_sprint", + "want_daily_growth": "start_growth_os", + "want_meetings": "start_meeting_sprint", + "want_email_rescue": "start_email_rescue", + "want_whatsapp_setup": "start_whatsapp_compliance", + "ask_pricing": "show_pricing", + "approve_action": "process_approval", + "edit_action": "process_edit", + "skip_action": "process_skip", + "ask_demo": "send_demo", + "ask_proof": "send_proof_pack", + "ask_services": "show_bundles", + "ask_partnership": "show_agency_partner", + "ask_revenue_today": "show_revenue_today_plan", +} + + +def route_message(message: str) -> dict[str, Any]: + """Classify a message + return the routed handler + recommended service.""" + classification = classify_intent(message) + intent = classification["intent"] + handler = INTENT_TO_HANDLER.get(intent, "show_bundles") + service_id = intent_to_service(intent) + + return { + "message": (message or "")[:300], + "classification": classification, + "intent": intent, + "handler": handler, + "recommended_service_id": service_id, + } + + +def handle_message( + message: str, + *, + customer_id: str | None = None, + has_contact_list: bool = False, + is_agency: bool = False, + is_local_business: bool = False, + budget_sar: int = 1000, +) -> dict[str, Any]: + """ + Full single-shot handler — classifies + plans + returns operator response. + + Never executes any external action. Just plans + drafts. + """ + routed = route_message(message) + intent = routed["intent"] + handler = routed["handler"] + + # Recommend a bundle (high-level package). + bundle_rec = recommend_bundle( + intent=intent, + has_contact_list=has_contact_list, + is_agency=is_agency, + is_local_business=is_local_business, + budget_sar=budget_sar, + ) + + # If a service is recommended, build its initial pipeline + intake form. + response: dict[str, Any] = { + "intent": intent, + "handler": handler, + "bundle_recommendation": bundle_rec, + "service_id": routed["recommended_service_id"], + "approval_required": True, + "live_send_allowed": False, + } + + if intent in ("approve_action", "edit_action", "skip_action"): + # Approvals are handled separately — surface a placeholder card. + decision = ( + "approve" if intent == "approve_action" + else "edit" if intent == "edit_action" + else "skip" + ) + sample_card = build_approval_card( + action_type="example_action", + title_ar="فعل مثال", + summary_ar="هذا مثال على approval card", + ) + response["decision_processed"] = process_approval_decision( + sample_card, decision=decision, decided_by=customer_id or "user", + ) + return response + + if routed["recommended_service_id"]: + response["intake_questions"] = build_intake_questions_for_intent(intent) + response["initial_pipeline"] = build_service_pipeline( + routed["recommended_service_id"], customer_id=customer_id or "", + ) + + return response diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py new file mode 100644 index 00000000..3ebec18d --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py @@ -0,0 +1,92 @@ +"""Executive Mode — CEO command center + daily brief + revenue risks.""" + +from __future__ import annotations + +from typing import Any + + +def build_executive_daily_brief( + *, + company_name: str = "", + sector: str = "saas", +) -> dict[str, Any]: + """Build the CEO's daily brief (Arabic).""" + return { + "title_ar": f"موجز اليوم التنفيذي — {company_name or '(الشركة)'}", + "summary_ar": [ + f"3 قرارات تنتظر اعتمادك في قطاع {sector}.", + "5 رسائل drafts معدّة بـ Saudi tone.", + "2 leads متأخرة في المتابعة (>72 ساعة).", + "1 شريك وكالة جاهز لاجتماع.", + "1 خطر سمعة على قناة (يحتاج مراجعة).", + ], + "priority_decisions_ar": [ + "اعتمد 5 رسائل إيميل (10 دقائق).", + "راجع 12 رقم بدون مصدر واضح قبل أي واتساب.", + "احجز ديمو شريك الوكالة.", + ], + "metric_to_watch_ar": ( + "نسبة approval_rate الأسبوعية — هي المؤشر الأقوى لجودة " + "الـ targeting + الـ Saudi Tone." + ), + "buttons_ar": ["اعرض القرارات", "Proof Pack", "لاحقاً"], + "approval_required": True, + } + + +def build_revenue_risks_summary( + *, + open_risks: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Build a 3-risk summary (Arabic).""" + open_risks = open_risks or [ + { + "id": "wa_quality", + "title_ar": "جودة واتساب", + "summary_ar": "نسبة الحظر على رقم واتساب الرئيسي تقترب من حد التحذير.", + "severity": "high", + "action_ar": "خفّض الحجم 50% + راجع الرسائل.", + }, + { + "id": "list_freshness", + "title_ar": "قائمة قديمة", + "summary_ar": "60% من القائمة لم يتم تحديثها منذ 9 أشهر.", + "severity": "medium", + "action_ar": "شغّل List Intelligence لتنظيفها.", + }, + { + "id": "single_threading", + "title_ar": "صفقة بشخص واحد", + "summary_ar": "صفقة كبيرة (250K) معتمدة على شخص واحد بدون buying committee.", + "severity": "high", + "action_ar": "ادعُ صانع قرار ثانٍ من نفس الشركة.", + }, + ] + return { + "title_ar": "أعلى 3 مخاطر إيراد اليوم", + "risks": open_risks[:3], + "approval_required": True, + } + + +def build_ceo_command_center( + *, + company_name: str = "", + sector: str = "saas", +) -> dict[str, Any]: + """Build the full CEO command-center page.""" + return { + "mode": "ceo", + "company_name": company_name, + "daily_brief": build_executive_daily_brief( + company_name=company_name, sector=sector, + ), + "revenue_risks": build_revenue_risks_summary(), + "next_three_moves_ar": [ + "اعتمد رسائل اليوم (5).", + "ابدأ Pilot 7 أيام لقطاع جديد (testing).", + "حدد منسّق Approvals بديل خلال 24 ساعة.", + ], + "approval_required": True, + "live_send_allowed": False, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py b/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py new file mode 100644 index 00000000..8772c51d --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py @@ -0,0 +1,129 @@ +"""Intake collector — builds intake questions per intent + validates payloads.""" + +from __future__ import annotations + +from typing import Any + +# Intake questions per intent (Arabic). +_INTAKE_QUESTIONS_BY_INTENT: dict[str, list[dict[str, Any]]] = { + "want_more_customers": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "city", "label_ar": "المدينة", "required": True}, + {"key": "offer", "label_ar": "العرض الرئيسي", "required": True}, + {"key": "ideal_customer", "label_ar": "العميل المثالي", + "required": True}, + ], + "has_contact_list": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "list_size", "label_ar": "حجم القائمة (تقريباً)", + "required": True}, + {"key": "list_source", "label_ar": "مصدر القائمة (CRM/event/upload)", + "required": True}, + {"key": "channels_available", "label_ar": "القنوات المتاحة", + "required": True}, + ], + "want_partnerships": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "partner_goal", + "label_ar": "هدف الشراكة (وكالات/موزعين/co-marketing)", + "required": True}, + {"key": "current_partners", "label_ar": "شركاء حاليين (إن وجد)", + "required": False}, + ], + "want_daily_growth": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "team_size", "label_ar": "حجم فريق المبيعات/النمو", + "required": True}, + {"key": "channels", "label_ar": "القنوات الحالية", "required": True}, + {"key": "approval_owner", "label_ar": "من يوافق على الرسائل؟", + "required": True}, + ], + "want_meetings": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "prospect_count", "label_ar": "عدد الـ prospects", + "required": True}, + {"key": "calendar_link", "label_ar": "رابط Calendar (لو وُجد)", + "required": False}, + ], + "want_email_rescue": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "gmail_label", + "label_ar": "اسم الـ label/الـ folder المستهدف", + "required": True}, + {"key": "ICP", "label_ar": "العميل المثالي", "required": True}, + ], + "want_whatsapp_setup": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "list_size", + "label_ar": "حجم قاعدة الواتساب الحالية", "required": True}, + {"key": "current_practice", + "label_ar": "الطريقة الحالية في إرسال الرسائل", "required": True}, + ], + "ask_revenue_today": [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "city", "label_ar": "المدينة", "required": True}, + {"key": "offer", "label_ar": "العرض الرئيسي", "required": True}, + ], + # Default minimal intake for any "ask_*" intent. + "ask_services": [ + {"key": "goal", "label_ar": "ما هدفك الأساسي؟", "required": True}, + ], +} + + +def build_intake_questions_for_intent(intent: str) -> dict[str, Any]: + """Return intake questions for an intent. Falls back to ask_services.""" + questions = _INTAKE_QUESTIONS_BY_INTENT.get(intent) + if questions is None: + questions = _INTAKE_QUESTIONS_BY_INTENT["ask_services"] + return { + "intent": intent, + "questions": [dict(q) for q in questions], + "estimated_minutes": max(2, len(questions) * 1), + "approval_required": True, + } + + +def parse_intake_payload( + intent: str, raw_payload: dict[str, Any] | None, +) -> dict[str, Any]: + """Parse + sanitize an intake payload against the intent's question set.""" + raw_payload = raw_payload or {} + questions = _INTAKE_QUESTIONS_BY_INTENT.get( + intent, _INTAKE_QUESTIONS_BY_INTENT["ask_services"], + ) + parsed: dict[str, Any] = {} + for q in questions: + key = q["key"] + val = raw_payload.get(key) + if val is None: + continue + # Strings get truncated to 500 chars. + if isinstance(val, str): + val = val.strip()[:500] + parsed[key] = val + return parsed + + +def validate_intake_completeness( + intent: str, payload: dict[str, Any], +) -> dict[str, Any]: + """Check that all required intake fields are present.""" + questions = _INTAKE_QUESTIONS_BY_INTENT.get( + intent, _INTAKE_QUESTIONS_BY_INTENT["ask_services"], + ) + missing: list[str] = [] + for q in questions: + if q.get("required") and not payload.get(q["key"]): + missing.append(str(q["key"])) + return { + "intent": intent, + "complete": not missing, + "missing_fields": missing, + "missing_count": len(missing), + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py b/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py new file mode 100644 index 00000000..d2fa613b --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py @@ -0,0 +1,180 @@ +"""Deterministic intent classifier — Arabic + English keywords → 16 intents.""" + +from __future__ import annotations + +import re +from typing import Any + +# 16 supported intents that drive the operator. +SUPPORTED_INTENTS: tuple[str, ...] = ( + "want_more_customers", + "has_contact_list", + "want_partnerships", + "want_daily_growth", + "want_meetings", + "want_email_rescue", + "want_whatsapp_setup", + "ask_pricing", + "approve_action", + "edit_action", + "skip_action", + "ask_demo", + "ask_proof", + "ask_services", + "ask_partnership", + "ask_revenue_today", +) + +# Each intent → (Arabic keywords, English keywords). +_KEYWORDS: dict[str, tuple[list[str], list[str]]] = { + "want_more_customers": ( + ["عملاء", "فرص", "leads", "ليدز", "عميل جديد", "مبيعات", + "أبغى عملاء", "زيادة عملاء"], + ["customers", "leads", "more sales", "new clients", "pipeline"], + ), + "has_contact_list": ( + ["قائمة", "أرقام", "إيميلات", "CSV", "قائمتي", "عملاء قدامى", + "اللستة", "ملف"], + ["list", "csv", "old customers", "spreadsheet", "contacts"], + ), + "want_partnerships": ( + ["شراكات", "شريك", "وكالة", "تعاون", "موزع", "شركاء"], + ["partnership", "partner", "agency deal", "referral"], + ), + "want_daily_growth": ( + ["تشغيل يومي", "نمو شهري", "Growth OS", "اشتراك", "يومياً", + "مدير نمو"], + ["daily growth", "growth os", "subscription", "monthly"], + ), + "want_meetings": ( + ["اجتماعات", "ديمو", "meeting", "موعد", "احجز", "مكالمة", + "demo"], + ["meeting", "demo", "book", "schedule call"], + ), + "want_email_rescue": ( + ["إيميل", "Gmail", "Outlook", "إنباكس", "بريد", "ضائعة"], + ["email rescue", "inbox", "gmail", "missed emails"], + ), + "want_whatsapp_setup": ( + ["واتساب", "WhatsApp", "opt-in", "حملة واتساب", "أرقامي"], + ["whatsapp", "compliance", "opt-in"], + ), + "ask_pricing": ( + ["السعر", "كم", "بكم", "تكلفة", "اشتراك"], + ["price", "cost", "how much", "pricing"], + ), + "approve_action": ( + ["اعتمد", "موافق", "وافق", "تمام", "نعم"], + ["approve", "ok", "yes", "go ahead", "confirm"], + ), + "edit_action": ( + ["عدّل", "تعديل", "غير", "بدّل"], + ["edit", "change", "modify", "tweak"], + ), + "skip_action": ( + ["تخطي", "تخطى", "تجاوز", "خطّي", "لا"], + ["skip", "no", "pass", "later"], + ), + "ask_demo": ( + ["ديمو", "عرض", "أشوف", "جرب", "تجربة"], + ["demo", "try", "show me", "trial"], + ), + "ask_proof": ( + ["proof", "نتائج", "case study", "إثبات", "تقرير"], + ["proof", "results", "case study", "report"], + ), + "ask_services": ( + ["الخدمات", "وش عندكم", "ماذا تقدمون", "العروض", "bundles"], + ["services", "what do you offer", "bundles", "packages"], + ), + "ask_partnership": ( + ["وكالة شريكة", "Agency Partner", "revenue share", "شراكة وكالة"], + ["agency partner", "revenue share", "white label"], + ), + "ask_revenue_today": ( + ["دخل اليوم", "أبيع اليوم", "اول pilot", "ابدأ اليوم"], + ["revenue today", "sell today", "first pilot", "private beta"], + ), +} + +# Map intent → recommended service ID (in service_tower.service_catalog). +INTENT_TO_SERVICE: dict[str, str] = { + "want_more_customers": "first_10_opportunities_sprint", + "has_contact_list": "list_intelligence", + "want_partnerships": "partner_sprint", + "want_daily_growth": "growth_os_monthly", + "want_meetings": "meeting_booking_sprint", + "want_email_rescue": "email_revenue_rescue", + "want_whatsapp_setup": "whatsapp_compliance_setup", + "ask_pricing": "free_growth_diagnostic", + "ask_demo": "free_growth_diagnostic", + "ask_proof": "free_growth_diagnostic", + "ask_services": "free_growth_diagnostic", + "ask_partnership": "agency_partner_program", + "ask_revenue_today": "first_10_opportunities_sprint", +} + + +def classify_intent(message: str) -> dict[str, Any]: + """ + Classify a free-text message → intent + confidence. + + Deterministic, keyword-based. No LLM. Returns: + { + "intent": str, + "confidence": float (0..1), + "matched_keywords": list[str], + "all_scores": dict[intent, score], + } + """ + text = (message or "").strip() + if not text: + return { + "intent": "ask_services", + "confidence": 0.1, + "matched_keywords": [], + "all_scores": {}, + } + + text_lc = text.lower() + scores: dict[str, int] = {} + matched_by_intent: dict[str, list[str]] = {} + + for intent, (ar_kw, en_kw) in _KEYWORDS.items(): + matches: list[str] = [] + for kw in ar_kw: + if kw in text: + matches.append(kw) + for kw in en_kw: + if kw.lower() in text_lc: + matches.append(kw) + scores[intent] = len(matches) + if matches: + matched_by_intent[intent] = matches + + if not any(scores.values()): + return { + "intent": "ask_services", + "confidence": 0.2, + "matched_keywords": [], + "all_scores": scores, + } + + best_intent = max(scores, key=lambda k: scores[k]) + total_matches = sum(scores.values()) + confidence = ( + round(scores[best_intent] / max(1, total_matches), 3) + if total_matches else 0.0 + ) + + return { + "intent": best_intent, + "confidence": confidence, + "matched_keywords": matched_by_intent.get(best_intent, []), + "all_scores": scores, + } + + +def intent_to_service(intent: str) -> str | None: + """Return the service-tower service ID linked to an intent (or None).""" + return INTENT_TO_SERVICE.get(intent) diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py b/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py new file mode 100644 index 00000000..27389d5e --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py @@ -0,0 +1,104 @@ +"""Operator memory — minimal in-process store for sessions + facts.""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from typing import Any + +from .session_state import SessionState + + +@dataclass +class OperatorMemory: + """In-process memory for the operator. Production = Supabase/Redis.""" + sessions: dict[str, SessionState] = field(default_factory=dict) + customer_facts: dict[str, dict[str, Any]] = field(default_factory=dict) + customer_preferences: dict[str, dict[str, Any]] = field(default_factory=dict) + blocked_actions_log: list[dict[str, Any]] = field(default_factory=list) + approved_actions_log: list[dict[str, Any]] = field(default_factory=list) + pivots_log: list[dict[str, Any]] = field(default_factory=list) + + # ── sessions ──────────────────────────────────────────── + def upsert_session(self, session: SessionState) -> SessionState: + self.sessions[session.session_id] = session + return session + + def get_session(self, session_id: str) -> SessionState | None: + return self.sessions.get(session_id) + + def list_sessions_for_customer(self, customer_id: str) -> list[SessionState]: + return [s for s in self.sessions.values() + if s.customer_id == customer_id] + + # ── customer facts ────────────────────────────────────── + def remember_fact(self, customer_id: str, key: str, value: Any) -> None: + bucket = self.customer_facts.setdefault(customer_id, {}) + bucket[key] = value + + def get_fact(self, customer_id: str, key: str) -> Any: + return self.customer_facts.get(customer_id, {}).get(key) + + def all_facts(self, customer_id: str) -> dict[str, Any]: + return dict(self.customer_facts.get(customer_id, {})) + + # ── preferences ───────────────────────────────────────── + def update_preference( + self, customer_id: str, *, key: str, value: Any, + ) -> None: + bucket = self.customer_preferences.setdefault(customer_id, {}) + bucket[key] = value + + def get_preferences(self, customer_id: str) -> dict[str, Any]: + return dict(self.customer_preferences.get(customer_id, {})) + + # ── action audit ──────────────────────────────────────── + def log_blocked_action( + self, *, action_type: str, reason_ar: str, + customer_id: str | None = None, + ) -> None: + self.blocked_actions_log.append({ + "ts": time.time(), + "action_type": action_type, + "reason_ar": reason_ar[:200], + "customer_id": customer_id, + }) + + def log_approved_action( + self, *, action_type: str, + customer_id: str | None = None, + notes: str = "", + ) -> None: + self.approved_actions_log.append({ + "ts": time.time(), + "action_type": action_type, + "customer_id": customer_id, + "notes": notes[:200], + }) + + def summarize_audit(self) -> dict[str, Any]: + return { + "blocked_count": len(self.blocked_actions_log), + "approved_count": len(self.approved_actions_log), + "blocked_recent": self.blocked_actions_log[-5:], + "approved_recent": self.approved_actions_log[-5:], + } + + +def build_session_context( + *, + memory: OperatorMemory, + session_id: str, +) -> dict[str, Any]: + """Build a context blob for a session — facts + recent audit + state.""" + session = memory.get_session(session_id) + if session is None: + return {"error": "unknown session"} + + customer_id = session.customer_id or "" + return { + "session": session.to_dict(), + "customer_facts": memory.all_facts(customer_id), + "preferences": memory.get_preferences(customer_id), + "audit": memory.summarize_audit(), + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py b/dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py new file mode 100644 index 00000000..3c3885b0 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py @@ -0,0 +1,72 @@ +"""Proof Pack dispatcher — generates + delivers Proof Packs per service.""" + +from __future__ import annotations + +from typing import Any + + +def proof_pack_for_service( + service_id: str, *, metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build a Proof Pack template for any service.""" + metrics = metrics or {} + return { + "service_id": service_id, + "title_ar": f"Proof Pack — {service_id}", + "sections_ar": [ + "ملخص تنفيذي (5 أسطر)", + "ما عمله Dealix", + "النتائج (الأرقام)", + "أبرز الردود/الاعتراضات", + "المخاطر التي تم منعها", + "Action Ledger مختصر", + "التوصية بالخطوة التالية", + ], + "metrics_captured": dict(metrics), + "metrics_required": [ + "opportunities_generated", + "drafts_approved", + "positive_replies", + "meetings_drafted", + "pipeline_influenced_sar", + "risks_blocked", + "time_saved_hours", + ], + "delivery_format": ["pdf", "json", "whatsapp_summary"], + "approval_required": True, + "live_send_allowed": False, + } + + +def dispatch_proof_pack( + *, + service_id: str, + customer_id: str | None = None, + channel: str = "email", + metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Dispatch a Proof Pack to a customer. + + Returns a draft envelope — never sends. The actual delivery requires + customer/admin approval through the Approval Center. + """ + template = proof_pack_for_service(service_id, metrics=metrics) + return { + "service_id": service_id, + "customer_id": customer_id, + "channel": channel, + "envelope": { + "subject_ar": template["title_ar"], + "body_ar": ( + "مرفق Proof Pack الخاص بـ Pilot. " + "يحتوي على ملخص تنفيذي + النتائج + المخاطر التي تم منعها + " + "التوصية بالخطوة التالية." + ), + "attachments": ["proof_pack.pdf", "proof_pack.json"], + }, + "template": template, + "status": "draft", + "approval_required": True, + "live_send_allowed": False, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py b/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py new file mode 100644 index 00000000..d91da15e --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py @@ -0,0 +1,215 @@ +"""Service bundles — 6 packaged offerings instead of 20 raw services.""" + +from __future__ import annotations + +from typing import Any + +# 6 bundles that simplify the customer's choice. +BUNDLES: tuple[dict[str, Any], ...] = ( + { + "id": "growth_starter", + "name_ar": "Growth Starter", + "best_for_ar": "أي شركة تجرب Dealix لأول مرة", + "services": [ + "free_growth_diagnostic", + "first_10_opportunities_sprint", + ], + "deliverables_ar": [ + "تشخيص نمو مجاني خلال 24 ساعة", + "10 فرص + رسائل عربية", + "Proof Pack مختصر", + ], + "timeline_ar": "8 أيام (1 ديمو + 7 Pilot)", + "price_min_sar": 499, + "price_max_sar": 1500, + "proof_metrics": [ + "opportunities_count", "drafts_approved", + "positive_replies", "diagnostic_to_paid_conversion", + ], + "upgrade_path": ["executive_growth_os"], + }, + { + "id": "data_to_revenue", + "name_ar": "Data to Revenue", + "best_for_ar": "شركات لديها قائمة عملاء/أرقام لم تُستثمر", + "services": [ + "list_intelligence", + "first_10_opportunities_sprint", + ], + "deliverables_ar": [ + "قائمة منظفة + تصنيف مصادر", + "أفضل 50 target بالقنوات الآمنة", + "رسائل عربية لكل segment", + "Risk report + retention", + ], + "timeline_ar": "10 أيام", + "price_min_sar": 1500, + "price_max_sar": 3000, + "proof_metrics": [ + "contacts_classified", "safe_targets_found", + "risks_blocked", "pipeline_influenced_sar", + ], + "upgrade_path": ["executive_growth_os"], + }, + { + "id": "executive_growth_os", + "name_ar": "Executive Growth OS", + "best_for_ar": "CEO / Growth Manager — تشغيل شهري", + "services": [ + "growth_os_monthly", + "executive_growth_brief", + ], + "deliverables_ar": [ + "Daily Command Feed عربي", + "Approval Center عبر واتساب", + "First 10 Opportunities أسبوعياً", + "Proof Pack شهري", + "Founder Shadow Board أسبوعي", + "Revenue Leak Detector", + ], + "timeline_ar": "شهري متجدد (ابدأ بـPilot 30 يوم)", + "price_min_sar": 2999, + "price_max_sar": 2999, + "proof_metrics": [ + "monthly_pipeline_sar", "monthly_meetings", + "monthly_revenue_influenced", "monthly_risks_blocked", + ], + "upgrade_path": ["partnership_growth", "full_growth_control_tower"], + }, + { + "id": "partnership_growth", + "name_ar": "Partnership Growth", + "best_for_ar": "شركات تنمو عبر الشركاء/الوكالات/الموزعين", + "services": [ + "partner_sprint", + "meeting_booking_sprint", + ], + "deliverables_ar": [ + "20 شريك محتمل + scorecard", + "10 رسائل + drafts اجتماعات", + "Referral Agreement Draft", + "Partner-Proof Pack", + ], + "timeline_ar": "14 يوم", + "price_min_sar": 3000, + "price_max_sar": 7500, + "proof_metrics": [ + "partners_identified", "partner_meetings", + "referral_revenue_sar", + ], + "upgrade_path": ["full_growth_control_tower"], + }, + { + "id": "local_growth_os", + "name_ar": "Local Growth OS", + "best_for_ar": "عيادات / متاجر / فروع / خدمات محلية", + "services": [ + "local_growth_os", + "whatsapp_compliance_setup", + "list_intelligence", + ], + "deliverables_ar": [ + "Google Business reviews ledger + draft replies", + "WhatsApp opt-in audit + templates", + "Customer reactivation campaign drafts", + "Branch-level Proof Pack", + ], + "timeline_ar": "3 أسابيع", + "price_min_sar": 999, + "price_max_sar": 2999, + "proof_metrics": [ + "reviews_handled", "opt_ins_collected", + "customers_reactivated", "risks_blocked", + ], + "upgrade_path": ["executive_growth_os"], + }, + { + "id": "full_growth_control_tower", + "name_ar": "Full Growth Control Tower", + "best_for_ar": "مؤسسات تريد تشغيل كامل على 30+ يوم", + "services": [ + "growth_os_monthly", + "list_intelligence", + "first_10_opportunities_sprint", + "partner_sprint", + "executive_growth_brief", + "linkedin_lead_gen_setup", + ], + "deliverables_ar": [ + "كل خدمات Growth OS", + "Partnership Sprint موازٍ", + "LinkedIn Lead Gen campaign", + "Founder Shadow Board", + "Service Excellence weekly review", + ], + "timeline_ar": "30 يوم — قابل للتجديد", + "price_min_sar": 12000, + "price_max_sar": 25000, + "proof_metrics": [ + "monthly_pipeline_sar", "monthly_revenue_influenced", + "partners_signed", "monthly_meetings", + ], + "upgrade_path": [], + }, +) + + +def list_bundles() -> dict[str, Any]: + return { + "total": len(BUNDLES), + "bundles": [dict(b) for b in BUNDLES], + } + + +def get_bundle(bundle_id: str) -> dict[str, Any] | None: + return next((dict(b) for b in BUNDLES if b["id"] == bundle_id), None) + + +def recommend_bundle( + *, + intent: str | None = None, + has_contact_list: bool = False, + is_agency: bool = False, + is_local_business: bool = False, + budget_sar: int = 1000, +) -> dict[str, Any]: + """ + Recommend the best-fit bundle deterministically. + + Order of priority: + agency → partnership_growth + local business → local_growth_os + has list → data_to_revenue + monthly budget → executive_growth_os + partnerships intent → partnership_growth + default → growth_starter + """ + if is_agency: + chosen = "partnership_growth" + reason = "وكالة → Partnership Growth + ترقية لـ Agency Partner Program." + elif is_local_business: + chosen = "local_growth_os" + reason = "نشاط محلي → Local Growth OS." + elif has_contact_list: + chosen = "data_to_revenue" + reason = "العميل لديه قائمة → Data to Revenue." + elif intent == "want_partnerships": + chosen = "partnership_growth" + reason = "هدف الشراكات → Partnership Growth." + elif intent == "want_daily_growth" or budget_sar >= 2999: + chosen = "executive_growth_os" + reason = "تشغيل يومي/ميزانية شهرية → Executive Growth OS." + elif budget_sar >= 12000: + chosen = "full_growth_control_tower" + reason = "ميزانية كبيرة → Full Growth Control Tower." + else: + chosen = "growth_starter" + reason = "ابدأ بـ Growth Starter." + + bundle = get_bundle(chosen) + return { + "recommended_bundle_id": chosen, + "bundle": bundle, + "reason_ar": reason, + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py b/dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py new file mode 100644 index 00000000..d00a7c3f --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py @@ -0,0 +1,94 @@ +"""Service orchestrator — runs the canonical service pipeline.""" + +from __future__ import annotations + +from typing import Any + +# Canonical pipeline every service goes through. +SERVICE_PIPELINE_STEPS: tuple[str, ...] = ( + "intake", + "data_check", + "targeting", + "contactability", + "strategy", + "drafting", + "approval", + "execution_or_export", + "tracking", + "proof", + "upsell", +) + +_STEP_LABELS_AR: dict[str, str] = { + "intake": "جمع المدخلات", + "data_check": "فحص جودة البيانات", + "targeting": "تحديد الأهداف", + "contactability": "تقييم إمكانية التواصل", + "strategy": "صياغة الاستراتيجية", + "drafting": "كتابة المسودات", + "approval": "اعتماد بشري", + "execution_or_export": "تنفيذ أو تصدير", + "tracking": "متابعة النتائج", + "proof": "Proof Pack", + "upsell": "ترقية الخدمة", +} + + +def build_service_pipeline( + service_id: str, *, customer_id: str = "", +) -> dict[str, Any]: + """Build the canonical pipeline state for a service.""" + return { + "service_id": service_id, + "customer_id": customer_id, + "current_step": "intake", + "completed_steps": [], + "steps": [ + { + "step_id": s, + "label_ar": _STEP_LABELS_AR.get(s, s), + "completed": False, + "approval_required": s in { + "drafting", "approval", "execution_or_export", + }, + } + for s in SERVICE_PIPELINE_STEPS + ], + "approval_required": True, + "live_send_allowed": False, + } + + +def run_service_step( + pipeline: dict[str, Any], *, step_id: str | None = None, +) -> dict[str, Any]: + """ + Mark the current (or supplied) step as run + advance the pipeline. + + Does NOT execute any external action — only updates state. + """ + target = step_id or pipeline.get("current_step") + steps = list(pipeline.get("steps", [])) + found = False + for i, s in enumerate(steps): + if s.get("step_id") == target: + s["completed"] = True + steps[i] = s + found = True + # Move to next step. + if i + 1 < len(steps): + pipeline["current_step"] = steps[i + 1]["step_id"] + else: + pipeline["current_step"] = "done" + break + + if not found: + return {**pipeline, "error": f"unknown step: {target}"} + + completed = [s["step_id"] for s in steps if s["completed"]] + pipeline["steps"] = steps + pipeline["completed_steps"] = completed + pipeline["progress_pct"] = round( + 100 * len(completed) / max(1, len(steps)), 1, + ) + return pipeline diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py b/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py new file mode 100644 index 00000000..a0f8cb99 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py @@ -0,0 +1,95 @@ +"""Session state — minimal in-memory state for an operator conversation.""" + +from __future__ import annotations + +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +# Valid state transitions for the operator session. +_VALID_STATES: tuple[str, ...] = ( + "new", + "intent_classified", + "intake_collecting", + "intake_complete", + "service_recommended", + "workflow_running", + "approval_pending", + "approval_received", + "executing", + "proof_pending", + "proof_delivered", + "upsell_offered", + "closed", +) + + +@dataclass +class SessionState: + """A single operator conversation session.""" + session_id: str + customer_id: str | None = None + state: str = "new" + intent: str | None = None + recommended_service_id: str | None = None + bundle_id: str | None = None + intake_payload: dict[str, Any] = field(default_factory=dict) + actions_pending_approval: list[dict[str, Any]] = field(default_factory=list) + actions_approved: list[dict[str, Any]] = field(default_factory=list) + actions_blocked: list[dict[str, Any]] = field(default_factory=list) + proof_pack: dict[str, Any] | None = None + upsell_offer: dict[str, Any] | None = None + history: list[dict[str, Any]] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + updated_at: float = field(default_factory=time.time) + + def to_dict(self) -> dict[str, Any]: + return { + "session_id": self.session_id, + "customer_id": self.customer_id, + "state": self.state, + "intent": self.intent, + "recommended_service_id": self.recommended_service_id, + "bundle_id": self.bundle_id, + "intake_payload": dict(self.intake_payload), + "actions_pending_approval": list(self.actions_pending_approval), + "actions_approved": list(self.actions_approved), + "actions_blocked": list(self.actions_blocked), + "proof_pack": self.proof_pack, + "upsell_offer": self.upsell_offer, + "history_len": len(self.history), + "created_at": self.created_at, + "updated_at": self.updated_at, + } + + +def build_new_session(customer_id: str | None = None) -> SessionState: + """Build a fresh session with a generated UUID.""" + return SessionState( + session_id=str(uuid.uuid4()), + customer_id=customer_id, + ) + + +def transition_session( + session: SessionState, + *, + new_state: str, + note: str = "", +) -> SessionState: + """Move the session to a new state with audit trail.""" + if new_state not in _VALID_STATES: + raise ValueError( + f"Unknown session state: {new_state}. " + f"Valid: {', '.join(_VALID_STATES)}" + ) + session.history.append({ + "from": session.state, + "to": new_state, + "note": note[:200], + "ts": time.time(), + }) + session.state = new_state + session.updated_at = time.time() + return session diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py b/dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py new file mode 100644 index 00000000..9fc788d5 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py @@ -0,0 +1,102 @@ +"""Tool action planner — plan + review actions before they hit Tool Gateway.""" + +from __future__ import annotations + +from typing import Any + +# Tools that REQUIRE explicit human approval, no exceptions. +_HIGH_RISK_TOOLS: frozenset[str] = frozenset({ + "whatsapp.send_message", + "gmail.send", + "calendar.insert_event", + "moyasar.charge", + "google_business.publish_review_reply", + "social.publish_dm", + "social.publish_post", +}) + +# Tools that are safe in draft mode (still approval-required, never live-by-default). +_DRAFT_SAFE_TOOLS: frozenset[str] = frozenset({ + "whatsapp.draft_message", + "gmail.create_draft", + "calendar.draft_event", + "moyasar.create_invoice_draft", + "moyasar.create_payment_link_draft", + "google_business.draft_review_reply", + "social.draft_post", +}) + +# Tools never to plan, period. +_FORBIDDEN_TOOLS: frozenset[str] = frozenset({ + "linkedin.scrape_profile", + "linkedin.auto_dm", + "linkedin.auto_connect", + "social.scrape_followers", + "phone.cold_call_unscripted", +}) + + +def plan_tool_action( + *, + tool: str, + payload: dict[str, Any] | None = None, + customer_id: str | None = None, + context: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Plan a tool action — does NOT execute. Returns the plan + safety verdict. + + Verdicts: + - "blocked" (tool is forbidden or unsafe) + - "draft_only" (tool may run as draft, requires approval) + - "approval_required"(tool requires human approval before execution) + - "ready_for_gateway"(tool is safe internal — pass to Tool Gateway) + """ + payload = payload or {} + context = context or {} + tool_lc = (tool or "").strip().lower() + + if tool_lc in _FORBIDDEN_TOOLS: + return { + "tool": tool, "verdict": "blocked", + "reason_ar": "أداة محظورة (LinkedIn scraping/auto-DM/scraping social).", + "live_send_allowed": False, + } + + if tool_lc in _HIGH_RISK_TOOLS: + return { + "tool": tool, "verdict": "approval_required", + "reason_ar": ( + "أداة عالية المخاطرة — تحتاج اعتماد بشري + env flag مفعّل." + ), + "live_send_allowed": False, + } + + if tool_lc in _DRAFT_SAFE_TOOLS: + return { + "tool": tool, "verdict": "draft_only", + "reason_ar": "draft فقط — أرسل للمراجعة قبل الاعتماد.", + "live_send_allowed": False, + } + + # Unknown tool — default to safest verdict. + return { + "tool": tool, "verdict": "approval_required", + "reason_ar": "أداة غير مصنّفة — تحتاج مراجعة قبل التنفيذ.", + "live_send_allowed": False, + } + + +def review_planned_action(plan: dict[str, Any]) -> dict[str, Any]: + """ + Quick safety review on an already-planned action. Returns updated plan. + + Strips any 'live_send_allowed=True' and forces it back to False. + """ + out = dict(plan) + out["live_send_allowed"] = False + out["safety_reviewed"] = True + if out.get("verdict") == "ready_for_gateway": + # Even safe tools must be audited — promote to approval_required. + out["verdict"] = "approval_required" + return out diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py b/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py new file mode 100644 index 00000000..16fcae16 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py @@ -0,0 +1,94 @@ +"""Upsell engine — recommend the next service after current one delivers.""" + +from __future__ import annotations + +from typing import Any + +# Mapping: completed_service → next_recommended_service. +_UPSELL_MAP: dict[str, str] = { + "free_growth_diagnostic": "first_10_opportunities_sprint", + "list_intelligence": "growth_os_monthly", + "first_10_opportunities_sprint": "growth_os_monthly", + "self_growth_operator": "growth_os_monthly", + "email_revenue_rescue": "growth_os_monthly", + "meeting_booking_sprint": "growth_os_monthly", + "partner_sprint": "agency_partner_program", + "agency_partner_program": "growth_os_monthly", + "whatsapp_compliance_setup": "growth_os_monthly", + "linkedin_lead_gen_setup": "growth_os_monthly", + "executive_growth_brief": "growth_os_monthly", + "growth_os_monthly": "growth_os_monthly", # already at top — annual upgrade +} + +_UPSELL_PRICING_AR: dict[str, str] = { + "first_10_opportunities_sprint": "499–1,500 ريال (Sprint)", + "growth_os_monthly": "2,999 ريال شهرياً (أو سنوي بخصم 15%)", + "agency_partner_program": "10,000–50,000 ريال (Setup) + Revenue Share", +} + + +def recommend_upsell_after_service( + *, + completed_service_id: str, + pilot_metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Recommend an upsell based on the completed service + metrics. + + Strong outcomes (csat ≥ 8 + pipeline ≥ 25K OR meetings ≥ 2) → upsell now. + Weak outcomes (pipeline < 5K + meetings = 0) → iterate, don't upsell. + Otherwise: gentle upsell. + """ + next_id = _UPSELL_MAP.get(completed_service_id, "growth_os_monthly") + metrics = pilot_metrics or {} + pipeline_sar = float(metrics.get("pipeline_sar", 0)) + meetings = int(metrics.get("meetings", 0)) + csat = int(metrics.get("csat", 0)) + + if csat >= 8 and (pipeline_sar >= 25_000 or meetings >= 2): + verdict = "upsell_now" + urgency_ar = ( + "النتائج قوية — اعرض الترقية اليوم مع خصم سنوي 15%." + ) + elif pipeline_sar < 5_000 and meetings == 0: + verdict = "iterate_first" + urgency_ar = ( + "النتائج ضعيفة هذه الجولة. اقترح زاوية مختلفة قبل الترقية." + ) + else: + verdict = "gentle_upsell" + urgency_ar = ( + "النتائج واعدة. اعرض Pilot موسّع 30 يوم قبل الاشتراك الشهري." + ) + + return { + "completed_service_id": completed_service_id, + "recommended_next_service_id": next_id, + "verdict": verdict, + "pricing_ar": _UPSELL_PRICING_AR.get(next_id, "حسب الحاجة"), + "urgency_ar": urgency_ar, + "approval_required": True, + } + + +def build_upsell_card( + *, + completed_service_id: str, + pilot_metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build an Arabic upsell card to deliver after Proof Pack.""" + rec = recommend_upsell_after_service( + completed_service_id=completed_service_id, + pilot_metrics=pilot_metrics, + ) + return { + "type": "upsell", + "title_ar": f"الترقية المقترحة بعد {completed_service_id}", + "summary_ar": rec["urgency_ar"], + "next_service_id": rec["recommended_next_service_id"], + "pricing_ar": rec["pricing_ar"], + "verdict": rec["verdict"], + "buttons_ar": ["ابدأ الترقية", "اشرح أكثر", "لاحقاً"], + "approval_required": True, + "live_send_allowed": False, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py b/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py new file mode 100644 index 00000000..184646e1 --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py @@ -0,0 +1,75 @@ +"""WhatsApp renderer — convert cards/briefs to WhatsApp-ready format. + +Drafts only. Never sends. Always emits buttons_ar capped at 3 (WhatsApp Reply +Buttons limit) and Arabic body text. +""" + +from __future__ import annotations + +from typing import Any + + +def render_card_for_whatsapp(card: dict[str, Any]) -> dict[str, Any]: + """Render any decision card as a WhatsApp-style draft message.""" + title = str(card.get("title_ar", "")).strip()[:60] + summary = str(card.get("summary_ar", "")).strip()[:300] + why_now = str(card.get("why_now_ar", "")).strip()[:200] + action = str(card.get("recommended_action_ar", "")).strip()[:200] + risk = str(card.get("risk_level", "")).strip() + buttons = list(card.get("buttons_ar", []))[:3] + + body_lines: list[str] = [title] + if summary: + body_lines.append("") + body_lines.append(summary) + if why_now: + body_lines.append("") + body_lines.append(f"لماذا الآن: {why_now}") + if action: + body_lines.append(f"الإجراء المقترح: {action}") + if risk: + body_lines.append(f"المخاطرة: {risk}") + if buttons: + body_lines.append("") + body_lines.append("أزرار: " + " | ".join(buttons)) + + return { + "channel": "whatsapp", + "kind": "card_draft", + "body_ar": "\n".join(body_lines), + "buttons_ar": buttons, + "approval_required": True, + "live_send_allowed": False, + } + + +def render_approval_card_for_whatsapp( + card: dict[str, Any], +) -> dict[str, Any]: + """Render an approval card specifically — guarantees the 3 standard buttons.""" + out = render_card_for_whatsapp(card) + out["buttons_ar"] = card.get("buttons_ar") or ["اعتمد", "عدّل", "تخطي"] + out["kind"] = "approval_card" + return out + + +def render_daily_brief_for_whatsapp(brief: dict[str, Any]) -> dict[str, Any]: + """Render a CEO/Growth Manager daily brief as WhatsApp draft.""" + summary_lines = list(brief.get("summary_ar", []))[:8] + decisions = list(brief.get("priority_decisions_ar", []))[:3] + + body_lines = ["صباح الخير 👋", "", "أهم اليوم:"] + body_lines.extend(f"• {line}" for line in summary_lines) + if decisions: + body_lines.append("") + body_lines.append("3 قرارات تنتظر:") + body_lines.extend(f"{i + 1}. {d}" for i, d in enumerate(decisions)) + + return { + "channel": "whatsapp", + "kind": "daily_brief_draft", + "body_ar": "\n".join(body_lines), + "buttons_ar": ["اعرض القرارات", "Proof Pack", "لاحقاً"], + "approval_required": True, + "live_send_allowed": False, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py b/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py new file mode 100644 index 00000000..8ef47e3d --- /dev/null +++ b/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py @@ -0,0 +1,43 @@ +"""Workflow runner — advances service pipelines + checks completion.""" + +from __future__ import annotations + +from typing import Any + +from .service_orchestrator import ( + SERVICE_PIPELINE_STEPS, + build_service_pipeline, + run_service_step, +) + + +def build_workflow_state(service_id: str, *, customer_id: str = "") -> dict[str, Any]: + """Initialize a new workflow state for a service.""" + pipeline = build_service_pipeline(service_id, customer_id=customer_id) + return { + "service_id": service_id, + "customer_id": customer_id, + "pipeline": pipeline, + "human_approvals_received": 0, + "human_approvals_pending": 0, + "blocked_actions": 0, + } + + +def advance_workflow( + workflow_state: dict[str, Any], *, step_id: str | None = None, +) -> dict[str, Any]: + """Advance the underlying pipeline by one step.""" + pipeline = workflow_state.get("pipeline") or build_service_pipeline( + str(workflow_state.get("service_id", "")), + ) + pipeline = run_service_step(pipeline, step_id=step_id) + workflow_state["pipeline"] = pipeline + return workflow_state + + +def is_workflow_complete(workflow_state: dict[str, Any]) -> bool: + """True iff all canonical steps have run.""" + pipeline = workflow_state.get("pipeline", {}) + completed = pipeline.get("completed_steps", []) + return len(completed) >= len(SERVICE_PIPELINE_STEPS) diff --git a/dealix/auto_client_acquisition/revenue_company_os/__init__.py b/dealix/auto_client_acquisition/revenue_company_os/__init__.py new file mode 100644 index 00000000..40305bf1 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/__init__.py @@ -0,0 +1,67 @@ +"""Revenue Company OS — multi-channel command feed + Revenue Work Units + self-improvement. + +Sits above platform_services + intelligence_layer + service_tower: + - event_to_card: any event → Arabic decision card + - command_feed_engine: aggregate cards across channels for the day + - action_graph: signal → action → outcome → proof + - revenue_work_units: Dealix's unit of measurement (Salesforce-inspired) + - channel_health: cross-channel reputation snapshot + - opportunity_factory: turn signals into opportunity cards + - service_factory: instantiate a service from a customer + intent + - proof_ledger: revenue-tier proof aggregator (NOT platform_services.proof_ledger) + - growth_memory: long-term cross-customer learning store + - self_improvement_loop: weekly review + recommendations +""" + +from __future__ import annotations + +from .action_graph import ( + REVENUE_EDGE_TYPES, + RevenueActionGraph, + build_revenue_action_graph_demo, +) +from .channel_health import build_channel_health_snapshot +from .command_feed_engine import ( + build_command_feed_demo as revenue_os_command_feed_demo, + build_command_feed_for_customer, +) +from .event_to_card import EVENT_TO_CARD_TYPES, build_card_from_event +from .growth_memory import GrowthMemory, build_growth_memory_demo +from .opportunity_factory import build_opportunity_factory_demo +from .proof_ledger import ( + RevenueProofLedger, + build_revenue_proof_ledger_demo, +) +from .revenue_work_units import ( + REVENUE_WORK_UNIT_TYPES, + aggregate_work_units, + build_revenue_work_unit, +) +from .self_improvement_loop import build_weekly_self_improvement_report +from .service_factory import build_service_factory_demo, instantiate_service + +__all__ = [ + # action_graph + "REVENUE_EDGE_TYPES", "RevenueActionGraph", + "build_revenue_action_graph_demo", + # channel_health + "build_channel_health_snapshot", + # command_feed_engine + "build_command_feed_for_customer", + "revenue_os_command_feed_demo", + # event_to_card + "EVENT_TO_CARD_TYPES", "build_card_from_event", + # growth_memory + "GrowthMemory", "build_growth_memory_demo", + # opportunity_factory + "build_opportunity_factory_demo", + # proof_ledger + "RevenueProofLedger", "build_revenue_proof_ledger_demo", + # revenue_work_units + "REVENUE_WORK_UNIT_TYPES", "aggregate_work_units", + "build_revenue_work_unit", + # self_improvement_loop + "build_weekly_self_improvement_report", + # service_factory + "build_service_factory_demo", "instantiate_service", +] diff --git a/dealix/auto_client_acquisition/revenue_company_os/action_graph.py b/dealix/auto_client_acquisition/revenue_company_os/action_graph.py new file mode 100644 index 00000000..fb555c0d --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/action_graph.py @@ -0,0 +1,123 @@ +"""Revenue Action Graph — signal → action → outcome → proof relationships.""" + +from __future__ import annotations + +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +# 14 typed edges Dealix records to learn what works. +REVENUE_EDGE_TYPES: tuple[str, ...] = ( + "signal_created_opportunity", + "opportunity_drafted_message", + "message_triggered_reply", + "reply_led_to_meeting", + "meeting_led_to_proposal", + "proposal_led_to_payment", + "partner_introduced_customer", + "review_created_recovery_task", + "approval_allowed_send", + "blocked_action_prevented_risk", + "list_intel_top50_targets", + "service_completed_generated_proof", + "proof_triggered_upsell", + "upsell_converted_to_subscription", +) + + +@dataclass +class RevenueActionGraph: + """In-memory revenue action graph. Production = Supabase + pgvector.""" + edges: list[dict[str, Any]] = field(default_factory=list) + + def add_edge( + self, + *, + edge_type: str, + src_id: str, + dst_id: str, + customer_id: str = "", + weight: float = 1.0, + metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Add a typed edge. Validates edge_type.""" + if edge_type not in REVENUE_EDGE_TYPES: + raise ValueError( + f"Unknown edge_type: {edge_type}. " + f"Valid: {', '.join(REVENUE_EDGE_TYPES)}" + ) + edge: dict[str, Any] = { + "edge_id": str(uuid.uuid4()), + "edge_type": edge_type, + "src_id": src_id, + "dst_id": dst_id, + "customer_id": customer_id, + "weight": float(weight), + "metadata": dict(metadata or {}), + "ts": time.time(), + } + self.edges.append(edge) + return edge + + def what_works_for_customer(self, customer_id: str) -> dict[str, Any]: + """Aggregate edges for a customer → what's working.""" + edges = [e for e in self.edges if e["customer_id"] == customer_id] + by_type: dict[str, int] = {} + for e in edges: + by_type[e["edge_type"]] = by_type.get(e["edge_type"], 0) + 1 + + # Score: weighted edge counts. Outcome edges weigh more. + outcome_edges = { + "proposal_led_to_payment": 5, + "upsell_converted_to_subscription": 5, + "reply_led_to_meeting": 3, + "meeting_led_to_proposal": 3, + "blocked_action_prevented_risk": 2, + } + score = sum(by_type.get(e, 0) * w for e, w in outcome_edges.items()) + + return { + "customer_id": customer_id, + "total_edges": len(edges), + "by_type": by_type, + "outcome_score": score, + } + + +def build_revenue_action_graph_demo() -> dict[str, Any]: + """Demo graph with realistic edges across 2 customers.""" + g = RevenueActionGraph() + # Customer A — full funnel + g.add_edge(edge_type="signal_created_opportunity", + src_id="signal_1", dst_id="opp_1", customer_id="cust_A") + g.add_edge(edge_type="opportunity_drafted_message", + src_id="opp_1", dst_id="msg_1", customer_id="cust_A") + g.add_edge(edge_type="approval_allowed_send", + src_id="msg_1", dst_id="msg_1_approved", customer_id="cust_A") + g.add_edge(edge_type="message_triggered_reply", + src_id="msg_1_approved", dst_id="reply_1", customer_id="cust_A") + g.add_edge(edge_type="reply_led_to_meeting", + src_id="reply_1", dst_id="meeting_1", customer_id="cust_A") + g.add_edge(edge_type="meeting_led_to_proposal", + src_id="meeting_1", dst_id="proposal_1", customer_id="cust_A") + g.add_edge(edge_type="proposal_led_to_payment", + src_id="proposal_1", dst_id="payment_499", + customer_id="cust_A", weight=499) + g.add_edge(edge_type="service_completed_generated_proof", + src_id="payment_499", dst_id="proof_1", customer_id="cust_A") + g.add_edge(edge_type="proof_triggered_upsell", + src_id="proof_1", dst_id="upsell_1", customer_id="cust_A") + # Customer B — risk path + g.add_edge(edge_type="blocked_action_prevented_risk", + src_id="msg_2", dst_id="cold_wa_blocked", customer_id="cust_B") + g.add_edge(edge_type="review_created_recovery_task", + src_id="review_2", dst_id="recovery_1", customer_id="cust_B") + g.add_edge(edge_type="partner_introduced_customer", + src_id="partner_1", dst_id="customer_B_intro", + customer_id="cust_B") + return { + "edges": list(g.edges), + "summary_a": g.what_works_for_customer("cust_A"), + "summary_b": g.what_works_for_customer("cust_B"), + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/channel_health.py b/dealix/auto_client_acquisition/revenue_company_os/channel_health.py new file mode 100644 index 00000000..ccd4f0ba --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/channel_health.py @@ -0,0 +1,58 @@ +"""Channel health — cross-channel reputation snapshot for the customer.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.targeting_os.reputation_guard import ( + calculate_channel_reputation, +) + + +def build_channel_health_snapshot( + *, + metrics_per_channel: dict[str, dict[str, float]] | None = None, +) -> dict[str, Any]: + """ + Build a single snapshot of channel health across channels. + + Input: + metrics_per_channel = { + "email": {"bounce_rate": 0.005, "complaint_rate": 0.0001, ...}, + "whatsapp": {"block_rate": 0.01, "report_rate": 0.001, ...}, + ... + } + """ + metrics_per_channel = metrics_per_channel or { + "email": {"bounce_rate": 0.005, "complaint_rate": 0.0001, + "opt_out_rate": 0.01, "reply_rate": 0.04}, + "whatsapp": {"block_rate": 0.005, "report_rate": 0.001, + "opt_out_rate": 0.02, "reply_rate": 0.10}, + "linkedin": {"connection_decline": 0.25}, + } + + snapshot: dict[str, Any] = {} + for channel, metrics in metrics_per_channel.items(): + snapshot[channel] = calculate_channel_reputation( + metrics, channel=channel, + ) + + overall_score = ( + sum(int(s.get("score", 0) or 0) for s in snapshot.values()) + / max(1, len(snapshot)) + ) + risky = [c for c, s in snapshot.items() if s.get("verdict") == "pause"] + + return { + "channels": snapshot, + "overall_score": round(overall_score, 1), + "channels_at_risk": risky, + "summary_ar": [ + f"الدرجة الكلية: {round(overall_score, 1)} / 100", + ( + f"قنوات في حالة pause: {', '.join(risky)}." + if risky else + "جميع القنوات صحية الآن." + ), + ], + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py b/dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py new file mode 100644 index 00000000..b02fdeeb --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py @@ -0,0 +1,61 @@ +"""Command Feed engine — aggregates events across channels into a daily feed.""" + +from __future__ import annotations + +from typing import Any + +from .event_to_card import build_card_from_event + + +def build_command_feed_for_customer( + *, + customer_id: str, + events: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Build today's Arabic command feed for a customer.""" + events = events or [] + cards = [build_card_from_event(e) for e in events] + by_type: dict[str, int] = {} + by_risk: dict[str, int] = {"low": 0, "medium": 0, "high": 0} + for c in cards: + by_type[c["type"]] = by_type.get(c["type"], 0) + 1 + by_risk[c["risk_level"]] = by_risk.get(c["risk_level"], 0) + 1 + + # Sort: high risk first, then medium, then low. Stable. + risk_order = {"high": 0, "medium": 1, "low": 2} + cards_sorted = sorted(cards, key=lambda c: risk_order.get(c["risk_level"], 9)) + + return { + "customer_id": customer_id, + "feed_size": len(cards), + "by_type": by_type, + "by_risk": by_risk, + "cards": cards_sorted, + "approval_required": True, + } + + +def build_command_feed_demo() -> dict[str, Any]: + """Demo feed with 8 synthetic events across all channels.""" + demo_events = [ + {"event_type": "email.received", "customer_id": "demo", + "payload": {"from": "ali@example.sa", "subject": "نطلب عرض"}}, + {"event_type": "whatsapp.reply_received", "customer_id": "demo", + "payload": {"text": "شكرًا، أبغى أعرف باقات الشركات"}}, + {"event_type": "form.submitted", "customer_id": "demo", + "payload": {"company": "شركة نمو", "role": "Head of Sales"}}, + {"event_type": "review.created", "customer_id": "demo", + "payload": {"rating": 2, "text": "تأخير في الرد"}}, + {"event_type": "payment.link_created", "customer_id": "demo", + "payload": {"amount_sar": 499, "description": "Pilot 7d"}}, + {"event_type": "risk.blocked", "customer_id": "demo", + "payload": {"reason_ar": "محاولة cold WhatsApp بدون opt-in"}}, + {"event_type": "partner.suggested", "customer_id": "demo", + "payload": {"partner_type": "agency", + "reason_ar": "وكالة B2B لديها 20 عميل في قطاع التدريب"}}, + {"event_type": "service.completed", "customer_id": "demo", + "payload": {"service_id": "first_10_opportunities_sprint"}}, + ] + return build_command_feed_for_customer( + customer_id="demo", events=demo_events, + ) diff --git a/dealix/auto_client_acquisition/revenue_company_os/event_to_card.py b/dealix/auto_client_acquisition/revenue_company_os/event_to_card.py new file mode 100644 index 00000000..4e17a4be --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/event_to_card.py @@ -0,0 +1,172 @@ +"""Event → Card converter — every channel event becomes an Arabic decision card.""" + +from __future__ import annotations + +from typing import Any + +# Each event_type → card_type Dealix renders. +EVENT_TO_CARD_TYPES: dict[str, str] = { + "email.received": "email_lead", + "whatsapp.reply_received": "whatsapp_reply", + "form.submitted": "opportunity", + "lead.uploaded": "list_intake", + "meeting.drafted": "meeting_prep", + "meeting.completed": "meeting_outcome", + "payment.link_created": "payment", + "partner.suggested": "partner_suggestion", + "review.created": "review_response", + "social.comment_received": "social_signal", + "proof.generated": "proof_pack", + "risk.blocked": "risk_alert", + "service.completed": "service_outcome", +} + + +def build_card_from_event(event: dict[str, Any]) -> dict[str, Any]: + """ + Convert a typed event into an Arabic decision card. + + Returns a dict with title_ar/summary_ar/why_now/recommended_action_ar/ + risk_level/buttons_ar (≤3)/approval_required/live_send_allowed=False. + """ + event_type = str(event.get("event_type", "")) + payload = dict(event.get("payload", {}) or {}) + customer_id = event.get("customer_id") + + card_type = EVENT_TO_CARD_TYPES.get(event_type, "action_required") + + base = { + "type": card_type, + "event_type": event_type, + "customer_id": customer_id, + "approval_required": True, + "live_send_allowed": False, + "buttons_ar": ["اعتمد", "عدّل", "تخطي"], + } + + if event_type == "email.received": + return { + **base, + "title_ar": "إيميل جديد يحتوي إشارة شراء", + "summary_ar": ( + f"من: {payload.get('from', '?')}. " + f"الموضوع: {payload.get('subject', '?')}." + ), + "why_now_ar": "ينتظر رداً منذ آخر تفاعل.", + "recommended_action_ar": "جهّز رد عربي + احجز اجتماع", + "risk_level": "low", + } + + if event_type == "whatsapp.reply_received": + return { + **base, + "title_ar": "رد واتساب من Lead", + "summary_ar": ( + f"المحتوى: {str(payload.get('text', ''))[:120]}." + ), + "why_now_ar": "اهتمام نشط — احفظ الزخم.", + "recommended_action_ar": "اعتمد رد قصير + لا ترسل عرض PDF كامل", + "risk_level": "low", + } + + if event_type == "form.submitted": + return { + **base, + "title_ar": "Lead جديد من نموذج الموقع", + "summary_ar": ( + f"الشركة: {payload.get('company', '?')}. " + f"الدور: {payload.get('role', '?')}." + ), + "why_now_ar": "Inbound lead — أعلى أولوية اليوم.", + "recommended_action_ar": "اعتمد رسالة شكر + احجز ديمو 12 دقيقة", + "risk_level": "low", + } + + if event_type == "review.created": + rating = int(payload.get("rating", 5) or 5) + return { + **base, + "title_ar": f"تقييم جديد — {rating} نجوم", + "summary_ar": str(payload.get("text", ""))[:200], + "why_now_ar": "السمعة المحلية حساسة — لا تتأخر.", + "recommended_action_ar": ( + "رد علني قصير + تواصل خاص لتفاصيل." + if rating < 3 else + "شكر علني + سؤال ما الذي أعجبهم تحديداً." + ), + "risk_level": "high" if rating < 3 else "low", + } + + if event_type == "payment.link_created": + return { + **base, + "title_ar": "رابط دفع جاهز", + "summary_ar": ( + f"المبلغ: {payload.get('amount_sar', '?')} ريال — " + f"{payload.get('description', '')}." + ), + "why_now_ar": "العميل وافق — أرسل الرابط بعد المراجعة.", + "recommended_action_ar": "راجع المبلغ ثم أرسل من Moyasar dashboard", + "risk_level": "medium", + } + + if event_type == "risk.blocked": + return { + **base, + "title_ar": "تنبيه: تم منع فعل خطر تلقائياً", + "summary_ar": str(payload.get("reason_ar", ""))[:200], + "why_now_ar": "حماية القناة من الحظر/المخالفة.", + "recommended_action_ar": "راجع السياسة + جهّز بديل آمن", + "risk_level": "high", + "buttons_ar": ["فهم", "اعرض البديل", "أرشف"], + } + + if event_type == "partner.suggested": + return { + **base, + "title_ar": "اقتراح شريك جديد", + "summary_ar": ( + f"النوع: {payload.get('partner_type', '?')}. " + f"السبب: {payload.get('reason_ar', '')[:120]}." + ), + "why_now_ar": "نقطة تكامل واضحة + قاعدة عملاء مشتركة.", + "recommended_action_ar": "اكتب رسالة warm + احجز مكالمة 20 دقيقة", + "risk_level": "low", + } + + if event_type == "meeting.drafted": + return { + **base, + "title_ar": "مسودة اجتماع جاهزة", + "summary_ar": ( + f"مع: {payload.get('with_company', '?')} — " + f"{payload.get('proposed_time', 'الوقت المقترح')}" + ), + "why_now_ar": "اعتمد المسودة لإرسال الدعوة.", + "recommended_action_ar": "راجع الـ agenda + اعتمد", + "risk_level": "low", + } + + if event_type == "service.completed": + return { + **base, + "title_ar": "خدمة اكتملت — Proof Pack جاهز", + "summary_ar": ( + f"الخدمة: {payload.get('service_id', '?')}. " + "Proof Pack + توصية بالخطوة التالية معدّة." + ), + "why_now_ar": "وقت الترقية بينما النتائج طازجة.", + "recommended_action_ar": "اعتمد Proof Pack + ابدأ Upsell", + "risk_level": "low", + "buttons_ar": ["اعتمد Proof", "ابدأ Upsell", "لاحقاً"], + } + + # Default fallback. + return { + **base, + "title_ar": f"حدث: {event_type}", + "summary_ar": str(payload)[:200], + "why_now_ar": "حدث جديد يحتاج مراجعة.", + "recommended_action_ar": "افتح للمراجعة", + "risk_level": "low", + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py b/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py new file mode 100644 index 00000000..51d30d0a --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py @@ -0,0 +1,108 @@ +"""Growth memory — long-term cross-customer learning store (anonymized aggregates).""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class GrowthMemory: + """Cross-customer aggregates Dealix learns from (anonymized + bucketed).""" + sector_message_winrate: dict[str, dict[str, float]] = field(default_factory=dict) + sector_channel_winrate: dict[str, dict[str, float]] = field(default_factory=dict) + common_objections: dict[str, int] = field(default_factory=dict) + blocked_action_reasons: dict[str, int] = field(default_factory=dict) + successful_playbooks: list[dict[str, Any]] = field(default_factory=list) + + def record_message_outcome( + self, *, sector: str, message_id: str, won: bool, + ) -> None: + bucket = self.sector_message_winrate.setdefault(sector, {}) + # rolling success/fail count stored as floats in [0..1] + prev = bucket.get(message_id, 0.5) + bucket[message_id] = round((prev + (1.0 if won else 0.0)) / 2.0, 3) + + def record_channel_outcome( + self, *, sector: str, channel: str, won: bool, + ) -> None: + bucket = self.sector_channel_winrate.setdefault(sector, {}) + prev = bucket.get(channel, 0.5) + bucket[channel] = round((prev + (1.0 if won else 0.0)) / 2.0, 3) + + def record_objection(self, label: str) -> None: + self.common_objections[label] = self.common_objections.get(label, 0) + 1 + + def record_blocked_reason(self, reason: str) -> None: + self.blocked_action_reasons[reason] = ( + self.blocked_action_reasons.get(reason, 0) + 1 + ) + + def append_successful_playbook( + self, *, sector: str, name: str, win_rate: float, + ) -> None: + self.successful_playbooks.append({ + "ts": time.time(), + "sector": sector, + "name": name, + "win_rate": float(win_rate), + }) + + def best_message_for_sector(self, sector: str) -> dict[str, Any]: + bucket = self.sector_message_winrate.get(sector, {}) + if not bucket: + return {"sector": sector, "best_message_id": None, "win_rate": 0.0} + best = max(bucket.items(), key=lambda x: x[1]) + return {"sector": sector, "best_message_id": best[0], "win_rate": best[1]} + + def best_channel_for_sector(self, sector: str) -> dict[str, Any]: + bucket = self.sector_channel_winrate.get(sector, {}) + if not bucket: + return {"sector": sector, "best_channel": None, "win_rate": 0.0} + best = max(bucket.items(), key=lambda x: x[1]) + return {"sector": sector, "best_channel": best[0], "win_rate": best[1]} + + def summary(self) -> dict[str, Any]: + return { + "sector_message_winrate": { + k: dict(v) for k, v in self.sector_message_winrate.items() + }, + "sector_channel_winrate": { + k: dict(v) for k, v in self.sector_channel_winrate.items() + }, + "top_objections": sorted( + self.common_objections.items(), + key=lambda x: -x[1], + )[:5], + "top_blocked_reasons": sorted( + self.blocked_action_reasons.items(), + key=lambda x: -x[1], + )[:5], + "successful_playbooks": self.successful_playbooks[-5:], + } + + +def build_growth_memory_demo() -> dict[str, Any]: + """Build a demo memory with sample aggregates.""" + g = GrowthMemory() + g.record_message_outcome(sector="training", message_id="msg_warm_intro", won=True) + g.record_message_outcome(sector="training", message_id="msg_warm_intro", won=True) + g.record_message_outcome(sector="training", message_id="msg_cold_pitch", won=False) + g.record_channel_outcome(sector="training", channel="email", won=True) + g.record_channel_outcome(sector="training", channel="email", won=True) + g.record_channel_outcome(sector="training", channel="linkedin_lead_form", won=True) + g.record_objection("price") + g.record_objection("timing") + g.record_objection("price") + g.record_blocked_reason("cold_whatsapp") + g.record_blocked_reason("cold_whatsapp") + g.record_blocked_reason("payload_contains_secret") + g.append_successful_playbook( + sector="training", name="warm_intro_with_proof", win_rate=0.42, + ) + return { + "summary": g.summary(), + "best_message_training": g.best_message_for_sector("training"), + "best_channel_training": g.best_channel_for_sector("training"), + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py b/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py new file mode 100644 index 00000000..d23bffb4 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py @@ -0,0 +1,54 @@ +"""Opportunity factory — turn signals into opportunity cards using Targeting OS.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.targeting_os import ( + map_buying_committee, + recommend_accounts, +) + + +def build_opportunity_factory_demo( + *, + sector: str = "training", + city: str = "Riyadh", + limit: int = 5, +) -> dict[str, Any]: + """ + Build demo opportunities for a (sector, city). + + Each opportunity includes account fit + buying committee + recommended channel. + """ + accounts_data = recommend_accounts( + sector=sector, city=city, limit=limit, + ) + committee = map_buying_committee(sector=sector, company_size="small") + + enriched = [] + for acct in accounts_data["accounts"]: + enriched.append({ + "company": acct.get("name"), + "fit_score": acct.get("fit_score"), + "tier": acct.get("tier"), + "why_now_ar": acct.get("why_now_ar"), + "best_angle_ar": acct.get("best_angle_ar"), + "recommended_channel": acct.get("recommended_channel"), + "primary_decision_maker": committee["primary_decision_maker"], + "approval_required": True, + "live_send_allowed": False, + }) + + return { + "sector": sector, + "city": city, + "count": len(enriched), + "opportunities": enriched, + "buying_committee_template": committee, + "do_not_do_ar": [ + "لا scraping LinkedIn ولا auto-DM.", + "لا cold WhatsApp.", + "لا تواصل بدون موافقة المالك.", + ], + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py b/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py new file mode 100644 index 00000000..87894236 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py @@ -0,0 +1,130 @@ +"""Revenue Proof Ledger — revenue-tier proof aggregator across all services. + +Distinct from `platform_services.proof_ledger`: this aggregates Revenue Work +Units + Action Graph edges into a customer-facing scoreboard. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from typing import Any + +from .revenue_work_units import REVENUE_WORK_UNIT_TYPES, aggregate_work_units + + +@dataclass +class RevenueProofLedger: + """In-memory revenue proof ledger. Production = Supabase append-only.""" + work_units: list[dict[str, Any]] = field(default_factory=list) + notable_events: list[dict[str, Any]] = field(default_factory=list) + + def append_work_unit(self, unit: dict[str, Any]) -> None: + """Append an RWU after validating its type.""" + ut = str(unit.get("unit_type", "")) + if ut not in REVENUE_WORK_UNIT_TYPES: + raise ValueError(f"Unknown RWU type: {ut}") + self.work_units.append(dict(unit)) + + def append_notable_event( + self, *, event_type: str, summary_ar: str, customer_id: str = "", + ) -> None: + self.notable_events.append({ + "ts": time.time(), + "event_type": event_type, + "summary_ar": summary_ar[:200], + "customer_id": customer_id, + }) + + def summary_for_customer(self, customer_id: str) -> dict[str, Any]: + """Build the customer-facing Arabic Proof scoreboard.""" + units = [u for u in self.work_units + if u.get("customer_id") == customer_id] + agg = aggregate_work_units(units) + + opps = agg["by_type"].get("opportunity_created", 0) + approvals = agg["by_type"].get("approval_collected", 0) + meetings = agg["by_type"].get("meeting_drafted", 0) + meetings_held = agg["by_type"].get("meeting_held", 0) + risks_blocked = agg["risks_blocked"] + revenue = agg["total_revenue_influenced_sar"] + + events_for_customer = [ + e for e in self.notable_events + if e.get("customer_id") == customer_id + ] + + return { + "customer_id": customer_id, + "totals": { + "opportunities_created": opps, + "approvals_collected": approvals, + "meetings_drafted": meetings, + "meetings_held": meetings_held, + "risks_blocked": risks_blocked, + "revenue_influenced_sar": revenue, + }, + "summary_ar": [ + f"الفرص: {opps} | الاعتمادات: {approvals}.", + f"الاجتماعات: {meetings} drafted, {meetings_held} held.", + f"مخاطر منعت: {risks_blocked}.", + f"إيراد متأثر: {revenue:.0f} ريال.", + ], + "notable_events": events_for_customer[-5:], + "by_type": agg["by_type"], + } + + +def build_revenue_proof_ledger_demo() -> dict[str, Any]: + """Demo ledger with 12 sample RWUs for a single customer.""" + from .revenue_work_units import build_revenue_work_unit + led = RevenueProofLedger() + cust = "demo" + sample_units = [ + build_revenue_work_unit(unit_type="opportunity_created", + service_id="first_10_opportunities_sprint", + customer_id=cust, revenue_influenced_sar=18000), + build_revenue_work_unit(unit_type="opportunity_created", + service_id="first_10_opportunities_sprint", + customer_id=cust, revenue_influenced_sar=12000), + build_revenue_work_unit(unit_type="draft_created", + service_id="first_10_opportunities_sprint", + customer_id=cust), + build_revenue_work_unit(unit_type="draft_created", + service_id="first_10_opportunities_sprint", + customer_id=cust), + build_revenue_work_unit(unit_type="approval_collected", + service_id="first_10_opportunities_sprint", + customer_id=cust), + build_revenue_work_unit(unit_type="approval_collected", + service_id="first_10_opportunities_sprint", + customer_id=cust), + build_revenue_work_unit(unit_type="meeting_drafted", + service_id="meeting_booking_sprint", + customer_id=cust, revenue_influenced_sar=20000), + build_revenue_work_unit(unit_type="risk_blocked", + service_id="whatsapp_compliance_setup", + customer_id=cust, risk_level="high"), + build_revenue_work_unit(unit_type="risk_blocked", + service_id="whatsapp_compliance_setup", + customer_id=cust, risk_level="high"), + build_revenue_work_unit(unit_type="proof_generated", + service_id="growth_os_monthly", + customer_id=cust), + build_revenue_work_unit(unit_type="upsell_offered", + service_id="growth_os_monthly", + customer_id=cust), + build_revenue_work_unit(unit_type="payment_received", + customer_id=cust, revenue_influenced_sar=499), + ] + for u in sample_units: + led.append_work_unit(u) + led.append_notable_event( + event_type="risk.blocked", customer_id=cust, + summary_ar="منع cold WhatsApp بدون opt-in (PDPL).", + ) + led.append_notable_event( + event_type="service.completed", customer_id=cust, + summary_ar="اكتمل First 10 Opportunities Sprint بنجاح.", + ) + return led.summary_for_customer(cust) diff --git a/dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py b/dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py new file mode 100644 index 00000000..0c82ad3b --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py @@ -0,0 +1,95 @@ +"""Revenue Work Units — Dealix's unit of measurement (Salesforce-inspired). + +Each completed, measurable task by Dealix counts as 1 RWU. The platform +proves its value by RWUs delivered + risks blocked, not by abstract "AI usage". +""" + +from __future__ import annotations + +import time +import uuid +from typing import Any + +# Categories of Revenue Work Units. +REVENUE_WORK_UNIT_TYPES: tuple[str, ...] = ( + "opportunity_created", + "target_ranked", + "contact_blocked", + "draft_created", + "approval_collected", + "message_sent_after_approval", + "meeting_drafted", + "meeting_held", + "followup_created", + "proof_generated", + "partner_suggested", + "payment_link_drafted", + "payment_received", + "review_reply_drafted", + "list_classified", + "risk_blocked", + "service_completed", + "upsell_offered", + "subscription_started", +) + + +def build_revenue_work_unit( + *, + unit_type: str, + service_id: str = "", + customer_id: str = "", + risk_level: str = "low", + revenue_influenced_sar: float = 0.0, + proof_event: str = "", + notes: str = "", +) -> dict[str, Any]: + """Build a single RWU. Validates `unit_type` strictly.""" + if unit_type not in REVENUE_WORK_UNIT_TYPES: + raise ValueError( + f"Unknown RWU type: {unit_type}. " + f"Valid: {', '.join(REVENUE_WORK_UNIT_TYPES)}" + ) + return { + "unit_id": str(uuid.uuid4()), + "unit_type": unit_type, + "service_id": service_id, + "customer_id": customer_id, + "risk_level": risk_level if risk_level in ("low", "medium", "high") else "low", + "revenue_influenced_sar": float(revenue_influenced_sar), + "proof_event": proof_event, + "notes": notes[:200], + "ts": time.time(), + } + + +def aggregate_work_units( + units: list[dict[str, Any]] | None, +) -> dict[str, Any]: + """Aggregate RWUs → counts + total revenue + risks blocked.""" + units = units or [] + by_type: dict[str, int] = {} + by_customer: dict[str, int] = {} + total_revenue = 0.0 + risks_blocked = 0 + high_risk_count = 0 + + for u in units: + ut = str(u.get("unit_type", "")) + by_type[ut] = by_type.get(ut, 0) + 1 + cid = str(u.get("customer_id", "unknown")) + by_customer[cid] = by_customer.get(cid, 0) + 1 + total_revenue += float(u.get("revenue_influenced_sar", 0) or 0) + if ut == "risk_blocked": + risks_blocked += 1 + if u.get("risk_level") == "high": + high_risk_count += 1 + + return { + "total_units": len(units), + "by_type": by_type, + "by_customer": by_customer, + "total_revenue_influenced_sar": round(total_revenue, 2), + "risks_blocked": risks_blocked, + "high_risk_count": high_risk_count, + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py b/dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py new file mode 100644 index 00000000..ae027a76 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py @@ -0,0 +1,97 @@ +"""Self-improvement loop — weekly review across services + recommendations.""" + +from __future__ import annotations + +from typing import Any + + +def build_weekly_self_improvement_report( + *, + weekly_metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Build the weekly Arabic self-improvement report. + + Inputs: + weekly_metrics = { + "approval_rate": 0.42, + "reply_rate": 0.05, + "meeting_rate": 0.02, + "blocked_actions": 8, + "service_revenue_sar": {"first_10_opportunities_sprint": 1500, ...}, + "top_objections": ["price", "timing"], + "channel_outcomes": {"email": "healthy", "whatsapp": "watch", ...}, + } + """ + m = weekly_metrics or {} + approval_rate = float(m.get("approval_rate", 0)) + reply_rate = float(m.get("reply_rate", 0)) + meeting_rate = float(m.get("meeting_rate", 0)) + blocked_actions = int(m.get("blocked_actions", 0)) + service_revenue = m.get("service_revenue_sar", {}) or {} + top_objections = m.get("top_objections", []) or [] + channel_outcomes = m.get("channel_outcomes", {}) or {} + + recommendations: list[str] = [] + + if approval_rate < 0.30: + recommendations.append( + "approval_rate منخفضة — راجع Saudi Tone + قلل الـ length في الـ drafts." + ) + elif approval_rate < 0.50: + recommendations.append( + "approval_rate متوسطة — جرّب 3 صياغات مختلفة لكل رسالة." + ) + + if reply_rate < 0.03: + recommendations.append( + "reply_rate منخفضة — جرّب why-now أوضح + نقاط شراء أحدث." + ) + + if meeting_rate < 0.01: + recommendations.append( + "meeting_rate منخفضة — ضع CTA حجز اجتماع أسهل في الرسالة." + ) + + if blocked_actions >= 10: + recommendations.append( + f"تم منع {blocked_actions} فعل — راجع contactability + opt-in policies." + ) + + # Best-performing service + best_service = None + if service_revenue: + best_service = max(service_revenue, key=lambda k: service_revenue[k]) + recommendations.append( + f"الخدمة الأكثر إيراداً: {best_service} — ضاعف الإعلان عنها هذا الأسبوع." + ) + + # Channel risks + risky_channels = [ + ch for ch, v in channel_outcomes.items() if v == "pause" + ] + if risky_channels: + recommendations.append( + f"قنوات في حالة pause: {', '.join(risky_channels)} — أوقف الحملات حتى تستعيد السمعة." + ) + + next_experiment = ( + f"اختبر زاوية رسالة جديدة لقطاع 'training' لمدة 7 أيام." + if not recommendations else + "ابدأ بأعلى توصية في القائمة قبل أي تجربة جديدة." + ) + + return { + "captured_metrics": dict(m), + "summary_ar": [ + f"approval_rate: {approval_rate * 100:.1f}%", + f"reply_rate: {reply_rate * 100:.1f}%", + f"meeting_rate: {meeting_rate * 100:.1f}%", + f"actions blocked: {blocked_actions}", + f"top objections: {', '.join(top_objections) or 'لا شيء بارز'}", + ], + "recommendations_ar": recommendations, + "next_experiment_ar": next_experiment, + "best_service_id": best_service, + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/revenue_company_os/service_factory.py b/dealix/auto_client_acquisition/revenue_company_os/service_factory.py new file mode 100644 index 00000000..bf0739a4 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_company_os/service_factory.py @@ -0,0 +1,54 @@ +"""Service factory — instantiate a service for a customer.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import ( + build_intake_questions, + build_service_workflow, + get_service, + quote_service, +) + + +def instantiate_service( + *, + service_id: str, + customer_id: str = "", + company_size: str = "small", + urgency: str = "normal", +) -> dict[str, Any]: + """Instantiate a service for a customer + return ready-to-run state.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "customer_id": customer_id, + "intake": build_intake_questions(service_id), + "workflow": build_service_workflow(service_id), + "quote": quote_service( + service_id, company_size=company_size, urgency=urgency, + ), + "approval_required": True, + "live_send_allowed": False, + } + + +def build_service_factory_demo() -> dict[str, Any]: + """Demo: instantiate the 4 launch-day services for a sample customer.""" + services = [ + "free_growth_diagnostic", + "list_intelligence", + "first_10_opportunities_sprint", + "growth_os_monthly", + ] + return { + "instantiations": [ + instantiate_service(service_id=sid, customer_id="demo") + for sid in services + ], + } diff --git a/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md b/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md new file mode 100644 index 00000000..c985b957 --- /dev/null +++ b/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md @@ -0,0 +1,200 @@ +# Dealix Autonomous Revenue Company OS + +> **الفئة الجديدة:** Dealix ليس CRM ولا أداة واتساب ولا AI agent ولا lead scraper. +> هو **شركة نمو رقمية ذاتية التشغيل** تدخل أي بزنس، تفهمه، تبني خطة نمو، تشغّل الخدمات المناسبة، تطلب موافقات، تنسق القنوات، تفتح شراكات، ترتب اجتماعات، تجهز مدفوعات، وتثبت العائد. + +--- + +## 1. القيم الأساسية للنظام + +``` +Signal → Context → Service Recommendation → Workflow → +Risk Check → Draft → Approval → Execution/Export → +Outcome → Proof → Learning → Upgrade +``` + +كل event داخل Dealix يمر بهذه السلسلة. لا توجد فجوة بين "إشارة" و"إيراد". + +--- + +## 2. الطبقات الـ12 + +| الطبقة | الموقع | +|--------|--------| +| Autonomous Service Operator | `auto_client_acquisition/autonomous_service_operator/` | +| Service Tower | `auto_client_acquisition/service_tower/` | +| Service Excellence OS | `auto_client_acquisition/service_excellence/` | +| Targeting OS | `auto_client_acquisition/targeting_os/` | +| Safe Tool Gateway | `auto_client_acquisition/platform_services/tool_gateway.py` | +| Agent Runtime | كل layer يحدد الـ agents فيه | +| Workflow Engine | `service_orchestrator + workflow_runner` | +| Revenue Graph | `revenue_company_os/action_graph.py` | +| Proof Ledger | `revenue_company_os/proof_ledger.py` + `platform_services/proof_ledger.py` | +| Self-Improving Layer | `revenue_company_os/self_improvement_loop.py` + `growth_curator/` | +| Revenue Launch System | `revenue_launch/` + `launch_ops/` | +| Growth Memory | `revenue_company_os/growth_memory.py` | + +--- + +## 3. Autonomous Service Operator + +**16 module + 28 endpoint.** البوت المركزي: + +- **`intent_classifier`** — 16 intent عبر Arabic + English keywords (deterministic). +- **`conversation_router`** — كل intent → handler + خدمة موصى بها. +- **`session_state`** — 13 حالة جلسة + audit history. +- **`intake_collector`** — أسئلة intake لكل intent + validation. +- **`approval_manager`** — كروت ≤3 أزرار + decisions (approve/edit/skip/reject). +- **`service_orchestrator`** — pipeline 11-step canonical. +- **`workflow_runner`** — advance + completion check. +- **`tool_action_planner`** — يحظر LinkedIn scraping/auto-DM، يطلب approval لـ high-risk، draft فقط للآمنة. +- **`proof_pack_dispatcher`** — Proof Pack envelope per service. +- **`upsell_engine`** — 3 verdicts (upsell_now / iterate_first / gentle_upsell). +- **`whatsapp_renderer`** — ≤3 buttons، Arabic body. +- **`operator_memory`** — sessions + facts + preferences + audit. +- **`service_bundles`** — 6 bundles (Growth Starter, Data to Revenue, Executive Growth OS, Partnership Growth, Local Growth OS, Full Growth Control Tower). +- **`executive_mode`** — CEO command center. +- **`client_mode`** — Growth Manager dashboard. +- **`agency_mode`** — multi-client + co-branded Proof Pack + revenue share. + +--- + +## 4. Revenue Company OS + +**10 module + 19 endpoint.** الذكاء عبر القنوات: + +- **`event_to_card`** — 13 event types → Arabic decision cards (≤3 buttons). +- **`command_feed_engine`** — daily aggregation + sort by risk. +- **`action_graph`** — 14 typed edges signal → action → outcome → proof. +- **`revenue_work_units`** — 19 RWU types (Salesforce-inspired) + aggregation. +- **`channel_health`** — cross-channel reputation snapshot. +- **`opportunity_factory`** — turn signals into opportunity cards. +- **`service_factory`** — instantiate any service for a customer. +- **`proof_ledger`** — Revenue Proof scoreboard per customer. +- **`growth_memory`** — cross-customer aggregates (anonymized): best message/channel/objections. +- **`self_improvement_loop`** — weekly Arabic recommendations from real metrics. + +--- + +## 5. Service Bundles (6 customer-facing offerings) + +| Bundle | Best for | Price (SAR) | +|--------|----------|-------------| +| Growth Starter | أي شركة تجرب لأول مرة | 499–1,500 | +| Data to Revenue | شركات لديها قائمة | 1,500–3,000 | +| Executive Growth OS | CEO / Growth Manager شهرياً | 2,999 | +| Partnership Growth | شركات تنمو عبر الشركاء | 3,000–7,500 | +| Local Growth OS | عيادات/متاجر/فروع | 999–2,999 | +| Full Growth Control Tower | مؤسسات 30+ يوم | 12,000–25,000 | + +--- + +## 6. الأمان (Critical Gates) + +كل tool action يمر: +``` +Intent → Policy → Approval → Execution → Audit +``` + +أوضاع التنفيذ: +- `suggest_only` +- `draft_only` +- `approval_required` +- `approved_execute` (env flag مفعّل + اعتماد) +- `blocked` + +**الممنوع تماماً (حتى مع env flag):** +- LinkedIn scraping / auto-DM / auto-connect. +- cold WhatsApp بدون opt-in. +- Moyasar live charge من API. +- إرسال Gmail بدون اعتماد بشري. + +--- + +## 7. Endpoints الجديدة + +### Autonomous Service Operator (28) +``` +POST /api/v1/operator/chat/{message, decision, classify} +POST /api/v1/operator/sessions/{new, {id}/transition, {id}/context} +GET /api/v1/operator/sessions/{id} +POST /api/v1/operator/cards/{approval, whatsapp/render} +GET /api/v1/operator/intake/questions/{intent} +POST /api/v1/operator/intake/validate +POST /api/v1/operator/service/start +POST /api/v1/operator/tools/plan +POST /api/v1/operator/proof-pack/dispatch +POST /api/v1/operator/upsell/{recommend, card} +GET /api/v1/operator/bundles +POST /api/v1/operator/bundles/recommend +POST /api/v1/operator/mode/{ceo, ceo/daily-brief, ceo/risks, client, agency, agency/add-client, agency/revenue-share, agency/co-branded-proof} +GET /api/v1/operator/whatsapp/daily-brief/demo +GET /api/v1/operator/proof-pack/demo +``` + +### Revenue Company OS (19) +``` +GET /api/v1/revenue-os/command-feed/demo +POST /api/v1/revenue-os/{events/ingest, command-feed/build} +GET /api/v1/revenue-os/work-units/{types, demo} +POST /api/v1/revenue-os/work-units/{build, aggregate} +GET /api/v1/revenue-os/proof-ledger/demo +GET /api/v1/revenue-os/action-graph/{edge-types, demo} +POST /api/v1/revenue-os/channel-health/snapshot +GET /api/v1/revenue-os/channel-health/demo +POST /api/v1/revenue-os/opportunity-factory +GET /api/v1/revenue-os/opportunity-factory/demo +POST /api/v1/revenue-os/service-factory +GET /api/v1/revenue-os/service-factory/demo +GET /api/v1/revenue-os/growth-memory/demo +POST /api/v1/revenue-os/self-improvement/weekly-report +GET /api/v1/revenue-os/self-improvement/demo +``` + +--- + +## 8. اختبارات + +`tests/unit/test_autonomous_service_operator.py` — 50 tests. +`tests/unit/test_revenue_company_os.py` — 31 tests. + +تغطية: +- Intent classification (8 intents). +- Bundle recommendation per persona. +- Tool planner blocks LinkedIn scrape/auto-DM. +- Approval cards ≤3 buttons. +- Sessions transition + audit. +- Modes (CEO / Client / Agency) with revenue share calc. +- Event → card with risk levels. +- Action Graph what-works. +- RWU aggregation + revenue total. +- Self-improvement recommendations. + +--- + +## 9. الفرق الشاسع عن المنافسين + +| المنافس | ماذا يملك | أين Dealix يتفوق | +|---------|-----------|-----------------| +| CRM | بيانات وفرص | يقول ماذا تفعل اليوم | +| WhatsApp tool | إرسال | يقرر هل ترسل، لمن، ولماذا، وبأي موافقة | +| Email assistant | يكتب رد | يحول الإيميل إلى pipeline + meeting + Proof | +| Agency | تنفيذ يدوي | نظام قابل للتكرار + Proof Pack | +| Generic AI agent | ينفذ prompts | عنده خدمات + سياسات + Proof + موافقات + تحسين ذاتي | +| HubSpot/Gong/Salesforce | منصات قوية | سعودي/عربي/SMB/Service-first/WhatsApp-aware | + +--- + +## 10. الخلاصة + +Dealix الآن **فئة جديدة**: +- 12 طبقة معمارية متكاملة. +- 905 اختبار ناجح. +- 47 endpoint جديد في هذه الجولة. +- Approval-first في كل قناة. +- Self-improving أسبوعياً. +- Revenue Work Units قابلة للقياس. +- Proof Ledger يُثبت العائد. +- 6 bundles + Service Excellence Score يحكم كل خدمة. + +**لا يبيع features. يبيع نتائج منظمة.** diff --git a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md index 9a3212ea..d9a05c0f 100644 --- a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md +++ b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md @@ -316,6 +316,32 @@ OAuth Gmail/Calendar، حصص، سياسات. - `scripts/launch_readiness_check.py` — runs 10 gates locally + against optional staging URL; reports JSON or pretty output. - `scripts/smoke_staging.py` — already exists (preserved). +## 44. Autonomous Revenue Company OS + +> Dealix الآن **فئة جديدة** — ليس منصة، بل شركة نمو رقمية ذاتية التشغيل. + +**26 module جديد + 47 endpoint جديد + 81 اختبار**. **التفصيل:** [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md). + +### Autonomous Service Operator (16 modules) +البوت المركزي يدير كل المحادثات وتشغيل الخدمات: +- `intent_classifier` (16 intents) → `conversation_router` → `service_orchestrator`. +- `intake_collector` + `approval_manager` (≤3 buttons) + `workflow_runner` + `tool_action_planner` (LinkedIn scrape/auto-DM blocked). +- `proof_pack_dispatcher` + `upsell_engine` + `whatsapp_renderer` + `operator_memory`. +- `service_bundles` (6 bundles: Growth Starter / Data to Revenue / Executive Growth OS / Partnership Growth / Local Growth OS / Full Growth Control Tower). +- `executive_mode` (CEO) + `client_mode` (Growth Manager) + `agency_mode` (multi-client + co-branded + revenue share). + +### Revenue Company OS (10 modules) +الذكاء عبر القنوات: +- `event_to_card` (13 event types → Arabic decision cards). +- `command_feed_engine` (sort by risk) + `action_graph` (14 typed edges: signal→action→outcome→proof). +- `revenue_work_units` (19 RWU types, Salesforce-inspired) + `channel_health`. +- `opportunity_factory` + `service_factory` + `proof_ledger` (revenue-tier scoreboard). +- `growth_memory` (cross-customer aggregates) + `self_improvement_loop` (weekly Arabic recommendations). + +**Endpoints:** `/api/v1/operator/*` (28) + `/api/v1/revenue-os/*` (19). + +**الفرق الشاسع:** Dealix لا يبيع features ولا AI ولا منصة. يبيع **شركة نمو رقمية ذاتية التشغيل** — نتائج منظمة + تشغيل يومي + Proof Pack شهري. + --- **الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً. الإطلاق اليوم = Private Beta + Pilots + Proof Pack، ليس Public Launch. diff --git a/dealix/tests/unit/test_autonomous_service_operator.py b/dealix/tests/unit/test_autonomous_service_operator.py new file mode 100644 index 00000000..866670dc --- /dev/null +++ b/dealix/tests/unit/test_autonomous_service_operator.py @@ -0,0 +1,379 @@ +"""Unit tests for the Autonomous Service Operator.""" + +from __future__ import annotations + +import pytest + +from auto_client_acquisition.autonomous_service_operator import ( + OperatorMemory, + SUPPORTED_INTENTS, + add_agency_client, + build_agency_dashboard, + build_approval_card, + build_ceo_command_center, + build_client_dashboard, + build_co_branded_proof_pack, + build_executive_daily_brief, + build_intake_questions_for_intent, + build_new_session, + build_revenue_risks_summary, + build_service_pipeline, + build_session_context, + build_upsell_card, + classify_intent, + dispatch_proof_pack, + handle_message, + intent_to_service, + list_agency_revenue_share, + list_bundles, + plan_tool_action, + process_approval_decision, + recommend_bundle, + recommend_upsell_after_service, + render_approval_card_for_whatsapp, + render_card_for_whatsapp, + render_daily_brief_for_whatsapp, + transition_session, + validate_intake_completeness, +) + + +# ── Intent classification ──────────────────────────────────── +def test_intent_want_more_customers(): + out = classify_intent("أبغى عملاء أكثر لشركتي") + assert out["intent"] == "want_more_customers" + + +def test_intent_has_contact_list(): + out = classify_intent("عندي قائمة أرقام كبيرة") + assert out["intent"] == "has_contact_list" + + +def test_intent_partnerships(): + out = classify_intent("أبغى شراكات مع وكالات") + assert out["intent"] == "want_partnerships" + + +def test_intent_whatsapp_setup(): + out = classify_intent("نستخدم واتساب بدون opt-in") + assert out["intent"] == "want_whatsapp_setup" + + +def test_intent_pricing(): + out = classify_intent("بكم السعر؟") + assert out["intent"] == "ask_pricing" + + +def test_intent_approve(): + out = classify_intent("اعتمد") + assert out["intent"] == "approve_action" + + +def test_intent_unknown_falls_back_to_services(): + out = classify_intent("xyz random text") + assert out["intent"] == "ask_services" + + +def test_intent_to_service_mapping(): + assert intent_to_service("want_more_customers") == "first_10_opportunities_sprint" + assert intent_to_service("has_contact_list") == "list_intelligence" + assert intent_to_service("want_partnerships") == "partner_sprint" + + +def test_supported_intents_count(): + assert len(SUPPORTED_INTENTS) == 16 + + +# ── Conversation router ────────────────────────────────────── +def test_handle_message_recommends_first_10_for_want_more_customers(): + out = handle_message("أبغى عملاء أكثر") + assert out["service_id"] == "first_10_opportunities_sprint" + assert out["live_send_allowed"] is False + + +def test_handle_message_uses_agency_bundle_for_agency(): + out = handle_message("أبغى شراكات", is_agency=True) + assert out["bundle_recommendation"]["recommended_bundle_id"] == "partnership_growth" + + +def test_handle_message_uses_data_to_revenue_when_list_provided(): + out = handle_message("أبغى أستخدم قائمتي", has_contact_list=True) + assert out["bundle_recommendation"]["recommended_bundle_id"] == "data_to_revenue" + + +def test_handle_message_approval_processes_decision(): + out = handle_message("اعتمد") + assert "decision_processed" in out + assert out["decision_processed"]["state"] == "approved" + + +# ── Sessions ──────────────────────────────────────────────── +def test_new_session_has_uuid(): + s = build_new_session(customer_id="cust_1") + assert s.session_id + assert s.state == "new" + assert s.customer_id == "cust_1" + + +def test_session_transition_audit_trail(): + s = build_new_session() + transition_session(s, new_state="intent_classified", note="initial") + assert s.state == "intent_classified" + assert len(s.history) == 1 + assert s.history[0]["from"] == "new" + + +def test_session_transition_unknown_raises(): + s = build_new_session() + with pytest.raises(ValueError): + transition_session(s, new_state="bogus_state") + + +def test_operator_memory_stores_session(): + mem = OperatorMemory() + s = build_new_session(customer_id="cust_1") + mem.upsert_session(s) + assert mem.get_session(s.session_id) is s + ctx = build_session_context(memory=mem, session_id=s.session_id) + assert ctx["session"]["session_id"] == s.session_id + + +# ── Intake ────────────────────────────────────────────────── +def test_intake_questions_for_known_intent(): + out = build_intake_questions_for_intent("want_more_customers") + assert len(out["questions"]) >= 4 + + +def test_intake_questions_unknown_intent_falls_back(): + out = build_intake_questions_for_intent("totally_unknown_intent") + assert out["questions"] + + +def test_intake_validation_detects_missing(): + out = validate_intake_completeness( + "want_more_customers", + {"sector": "training"}, # only one field + ) + assert out["complete"] is False + assert "company_name" in out["missing_fields"] + + +def test_intake_validation_complete(): + out = validate_intake_completeness( + "want_more_customers", + {"company_name": "X", "sector": "training", "city": "Riyadh", + "offer": "Pilot 7 أيام", "ideal_customer": "B2B"}, + ) + assert out["complete"] is True + + +# ── Approval manager ──────────────────────────────────────── +def test_approval_card_has_three_buttons(): + card = build_approval_card( + action_type="send_email", title_ar="إرسال إيميل", + summary_ar="إيميل لـ Acme", + ) + assert len(card["buttons_ar"]) <= 3 + assert card["live_send_allowed"] is False + + +def test_approval_decision_approve(): + card = build_approval_card(action_type="x", title_ar="x", summary_ar="x") + out = process_approval_decision(card, decision="approve") + assert out["state"] == "approved" + assert out["next_action"] == "execute_with_audit" + + +def test_approval_decision_arabic_skip(): + card = build_approval_card(action_type="x", title_ar="x", summary_ar="x") + out = process_approval_decision(card, decision="تخطي") + assert out["state"] == "rejected" + + +def test_approval_decision_unknown_returns_error(): + card = build_approval_card(action_type="x", title_ar="x", summary_ar="x") + out = process_approval_decision(card, decision="bogus") + assert "error" in out + + +# ── Service pipeline ──────────────────────────────────────── +def test_service_pipeline_starts_at_intake(): + p = build_service_pipeline("first_10_opportunities_sprint") + assert p["current_step"] == "intake" + assert any(s["step_id"] == "approval" for s in p["steps"]) + + +# ── Tool action planner ───────────────────────────────────── +def test_plan_blocks_linkedin_scrape(): + out = plan_tool_action(tool="linkedin.scrape_profile") + assert out["verdict"] == "blocked" + + +def test_plan_blocks_linkedin_auto_dm(): + out = plan_tool_action(tool="linkedin.auto_dm") + assert out["verdict"] == "blocked" + + +def test_plan_high_risk_requires_approval(): + out = plan_tool_action(tool="whatsapp.send_message") + assert out["verdict"] == "approval_required" + assert out["live_send_allowed"] is False + + +def test_plan_draft_safe_returns_draft_only(): + out = plan_tool_action(tool="gmail.create_draft") + assert out["verdict"] == "draft_only" + + +def test_plan_unknown_defaults_to_approval_required(): + out = plan_tool_action(tool="bogus.tool") + assert out["verdict"] == "approval_required" + + +# ── Bundles ───────────────────────────────────────────────── +def test_list_bundles_returns_six(): + out = list_bundles() + assert out["total"] == 6 + + +def test_recommend_bundle_for_agency(): + out = recommend_bundle(is_agency=True) + assert out["recommended_bundle_id"] == "partnership_growth" + + +def test_recommend_bundle_for_local_business(): + out = recommend_bundle(is_local_business=True) + assert out["recommended_bundle_id"] == "local_growth_os" + + +def test_recommend_bundle_with_list(): + out = recommend_bundle(has_contact_list=True) + assert out["recommended_bundle_id"] == "data_to_revenue" + + +def test_recommend_bundle_default(): + out = recommend_bundle(budget_sar=500) + assert out["recommended_bundle_id"] == "growth_starter" + + +# ── Modes ─────────────────────────────────────────────────── +def test_ceo_command_center_arabic(): + out = build_ceo_command_center(company_name="Acme") + assert out["mode"] == "ceo" + assert any("؀" <= ch <= "ۿ" for ch in out["daily_brief"]["title_ar"]) + + +def test_executive_daily_brief_three_decisions(): + out = build_executive_daily_brief(company_name="Acme") + assert len(out["priority_decisions_ar"]) == 3 + assert len(out["buttons_ar"]) <= 3 + + +def test_revenue_risks_summary_three_risks(): + out = build_revenue_risks_summary() + assert len(out["risks"]) == 3 + + +def test_client_dashboard_has_panels(): + out = build_client_dashboard(customer_id="c1", company_name="Acme") + assert out["mode"] == "client" + assert len(out["today_panels_ar"]) >= 3 + + +def test_agency_dashboard_aggregates(): + clients = [ + {"client_company_name": "A", "monthly_subscription_sar": 2999, + "revenue_share_pct": 20, "status": "active"}, + {"client_company_name": "B", "monthly_subscription_sar": 1500, + "revenue_share_pct": 25, "status": "onboarding"}, + ] + out = build_agency_dashboard(agency_id="ag1", clients=clients) + assert out["metrics"]["total_clients"] == 2 + assert out["metrics"]["monthly_revenue_sar"] == 4499.0 + + +def test_agency_revenue_share_calculation(): + clients = [ + {"client_company_name": "A", "monthly_subscription_sar": 2999, + "revenue_share_pct": 20}, + ] + out = list_agency_revenue_share(clients=clients) + assert out["total_share_sar"] == 599.8 + + +def test_agency_add_client_appends(): + clients: list = [] + add_agency_client( + agency_id="ag1", client_company_name="Acme", + monthly_subscription_sar=2999, revenue_share_pct=20, + clients=clients, + ) + assert len(clients) == 1 + + +def test_co_branded_proof_pack_includes_both_names(): + out = build_co_branded_proof_pack( + agency_name="Vortex", client_company_name="Acme", + ) + assert out["co_branded"] is True + assert out["agency_name"] == "Vortex" + + +# ── WhatsApp renderer ──────────────────────────────────────── +def test_render_card_for_whatsapp_no_live_send(): + card = build_approval_card( + action_type="x", title_ar="فرصة", summary_ar="ملخص", + ) + out = render_card_for_whatsapp(card) + assert out["live_send_allowed"] is False + assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"]) + + +def test_render_approval_card_has_3_buttons(): + card = build_approval_card( + action_type="x", title_ar="فرصة", summary_ar="ملخص", + ) + out = render_approval_card_for_whatsapp(card) + assert len(out["buttons_ar"]) == 3 + + +def test_render_daily_brief_arabic(): + brief = build_executive_daily_brief(company_name="Acme") + out = render_daily_brief_for_whatsapp(brief) + assert "صباح" in out["body_ar"] + assert out["live_send_allowed"] is False + + +# ── Proof + Upsell ────────────────────────────────────────── +def test_proof_pack_dispatch_returns_draft(): + out = dispatch_proof_pack( + service_id="first_10_opportunities_sprint", + customer_id="c1", + ) + assert out["status"] == "draft" + assert out["live_send_allowed"] is False + + +def test_upsell_recommends_growth_os_after_first_10(): + out = recommend_upsell_after_service( + completed_service_id="first_10_opportunities_sprint", + pilot_metrics={"pipeline_sar": 30000, "meetings": 3, "csat": 9}, + ) + assert out["recommended_next_service_id"] == "growth_os_monthly" + assert out["verdict"] == "upsell_now" + + +def test_upsell_iterate_for_weak_outcome(): + out = recommend_upsell_after_service( + completed_service_id="first_10_opportunities_sprint", + pilot_metrics={"pipeline_sar": 1000, "meetings": 0, "csat": 5}, + ) + assert out["verdict"] == "iterate_first" + + +def test_upsell_card_has_three_buttons(): + out = build_upsell_card( + completed_service_id="first_10_opportunities_sprint", + ) + assert len(out["buttons_ar"]) == 3 + assert out["live_send_allowed"] is False diff --git a/dealix/tests/unit/test_revenue_company_os.py b/dealix/tests/unit/test_revenue_company_os.py new file mode 100644 index 00000000..f19c1c41 --- /dev/null +++ b/dealix/tests/unit/test_revenue_company_os.py @@ -0,0 +1,253 @@ +"""Unit tests for the Revenue Company OS layer.""" + +from __future__ import annotations + +import pytest + +from auto_client_acquisition.revenue_company_os import ( + REVENUE_EDGE_TYPES, + REVENUE_WORK_UNIT_TYPES, + RevenueActionGraph, + RevenueProofLedger, + aggregate_work_units, + build_card_from_event, + build_channel_health_snapshot, + build_command_feed_for_customer, + build_growth_memory_demo, + build_opportunity_factory_demo, + build_revenue_action_graph_demo, + build_revenue_proof_ledger_demo, + build_revenue_work_unit, + build_service_factory_demo, + build_weekly_self_improvement_report, + instantiate_service, + revenue_os_command_feed_demo, +) + + +# ── Event → card ──────────────────────────────────────────── +def test_email_event_returns_arabic_card(): + card = build_card_from_event({ + "event_type": "email.received", + "customer_id": "c1", + "payload": {"from": "ali@example.sa", "subject": "نطلب عرض"}, + }) + assert card["type"] == "email_lead" + assert any("؀" <= ch <= "ۿ" for ch in card["title_ar"]) + assert card["live_send_allowed"] is False + + +def test_low_review_returns_high_risk(): + card = build_card_from_event({ + "event_type": "review.created", + "payload": {"rating": 1, "text": "تأخير في الرد"}, + }) + assert card["risk_level"] == "high" + + +def test_risk_blocked_event_high_risk(): + card = build_card_from_event({ + "event_type": "risk.blocked", + "payload": {"reason_ar": "محاولة cold WhatsApp"}, + }) + assert card["risk_level"] == "high" + assert "فهم" in card["buttons_ar"] + + +def test_unknown_event_returns_action_required(): + card = build_card_from_event({"event_type": "totally.unknown"}) + assert card["type"] == "action_required" + assert card["live_send_allowed"] is False + + +# ── Command feed ──────────────────────────────────────────── +def test_command_feed_demo_has_8_events(): + feed = revenue_os_command_feed_demo() + assert feed["feed_size"] == 8 + + +def test_command_feed_sorts_high_risk_first(): + feed = revenue_os_command_feed_demo() + cards = feed["cards"] + assert cards[0]["risk_level"] == "high" + + +def test_command_feed_for_customer_empty(): + feed = build_command_feed_for_customer(customer_id="c1", events=[]) + assert feed["feed_size"] == 0 + assert feed["cards"] == [] + + +# ── Revenue Work Units ────────────────────────────────────── +def test_rwu_types_count(): + assert len(REVENUE_WORK_UNIT_TYPES) >= 18 + + +def test_build_rwu_validates_type(): + with pytest.raises(ValueError): + build_revenue_work_unit(unit_type="bogus") + + +def test_build_rwu_returns_valid_unit(): + u = build_revenue_work_unit( + unit_type="opportunity_created", + customer_id="c1", + revenue_influenced_sar=18000, + ) + assert u["unit_type"] == "opportunity_created" + assert u["revenue_influenced_sar"] == 18000.0 + + +def test_aggregate_work_units_sums_revenue(): + units = [ + build_revenue_work_unit(unit_type="opportunity_created", + customer_id="c1", revenue_influenced_sar=10000), + build_revenue_work_unit(unit_type="opportunity_created", + customer_id="c1", revenue_influenced_sar=20000), + build_revenue_work_unit(unit_type="risk_blocked", + customer_id="c1", risk_level="high"), + ] + agg = aggregate_work_units(units) + assert agg["total_units"] == 3 + assert agg["total_revenue_influenced_sar"] == 30000.0 + assert agg["risks_blocked"] == 1 + + +# ── Revenue Action Graph ──────────────────────────────────── +def test_action_graph_edge_types_count(): + assert len(REVENUE_EDGE_TYPES) >= 12 + + +def test_action_graph_add_edge_validates(): + g = RevenueActionGraph() + with pytest.raises(ValueError): + g.add_edge(edge_type="bogus", src_id="a", dst_id="b") + + +def test_action_graph_demo_has_two_customers(): + out = build_revenue_action_graph_demo() + assert "summary_a" in out + assert "summary_b" in out + assert out["summary_a"]["outcome_score"] > 0 + + +def test_action_graph_what_works(): + g = RevenueActionGraph() + g.add_edge(edge_type="proposal_led_to_payment", src_id="p1", dst_id="pay1", + customer_id="c1") + g.add_edge(edge_type="reply_led_to_meeting", src_id="r1", dst_id="m1", + customer_id="c1") + summary = g.what_works_for_customer("c1") + assert summary["total_edges"] == 2 + assert summary["outcome_score"] > 0 + + +# ── Channel Health ────────────────────────────────────────── +def test_channel_health_snapshot_returns_score(): + out = build_channel_health_snapshot() + assert "channels" in out + assert "overall_score" in out + + +def test_channel_health_flags_risky_channel(): + out = build_channel_health_snapshot(metrics_per_channel={ + "email": {"bounce_rate": 0.20, "complaint_rate": 0.01, + "opt_out_rate": 0.30, "reply_rate": 0.001}, + }) + assert "email" in out["channels_at_risk"] + + +# ── Opportunity factory ───────────────────────────────────── +def test_opportunity_factory_returns_5_opps(): + out = build_opportunity_factory_demo(limit=5) + assert out["count"] == 5 + for opp in out["opportunities"]: + assert opp["live_send_allowed"] is False + + +def test_opportunity_factory_blocks_unsafe_actions(): + out = build_opportunity_factory_demo() + notes = " ".join(out["do_not_do_ar"]) + assert "scraping" in notes.lower() or "scraping" in notes + + +# ── Service factory ──────────────────────────────────────── +def test_instantiate_service_known(): + out = instantiate_service( + service_id="first_10_opportunities_sprint", + customer_id="c1", + ) + assert "intake" in out + assert "workflow" in out + assert "quote" in out + assert out["live_send_allowed"] is False + + +def test_instantiate_service_unknown(): + out = instantiate_service(service_id="totally_unknown") + assert "error" in out + + +def test_service_factory_demo_returns_4_services(): + out = build_service_factory_demo() + assert len(out["instantiations"]) == 4 + + +# ── Proof Ledger ──────────────────────────────────────────── +def test_proof_ledger_appends_units(): + led = RevenueProofLedger() + led.append_work_unit(build_revenue_work_unit( + unit_type="opportunity_created", customer_id="c1", + revenue_influenced_sar=10000, + )) + summary = led.summary_for_customer("c1") + assert summary["totals"]["opportunities_created"] == 1 + + +def test_proof_ledger_rejects_unknown_type(): + led = RevenueProofLedger() + with pytest.raises(ValueError): + led.append_work_unit({"unit_type": "totally_bogus"}) + + +def test_proof_ledger_demo_has_revenue(): + out = build_revenue_proof_ledger_demo() + assert out["totals"]["revenue_influenced_sar"] > 0 + assert out["totals"]["risks_blocked"] >= 2 + + +# ── Growth Memory ─────────────────────────────────────────── +def test_growth_memory_demo_has_top_objections(): + out = build_growth_memory_demo() + assert out["summary"]["top_objections"] + + +def test_growth_memory_best_message(): + out = build_growth_memory_demo() + assert out["best_message_training"]["sector"] == "training" + + +# ── Self-improvement loop ─────────────────────────────────── +def test_self_improvement_low_approval_recommends_fix(): + out = build_weekly_self_improvement_report(weekly_metrics={ + "approval_rate": 0.10, + }) + assert out["recommendations_ar"] + assert any("approval_rate" in r for r in out["recommendations_ar"]) + + +def test_self_improvement_blocked_actions_high_recommends_review(): + out = build_weekly_self_improvement_report(weekly_metrics={ + "approval_rate": 0.5, "blocked_actions": 25, + }) + assert any("منع" in r for r in out["recommendations_ar"]) + + +def test_self_improvement_returns_best_service(): + out = build_weekly_self_improvement_report(weekly_metrics={ + "service_revenue_sar": { + "first_10_opportunities_sprint": 1500, + "growth_os_monthly": 5000, + }, + }) + assert out["best_service_id"] == "growth_os_monthly"