diff --git a/.github/workflows/dealix-api-ci.yml b/.github/workflows/dealix-api-ci.yml index 3bffe5f7..92f710e2 100644 --- a/.github/workflows/dealix-api-ci.yml +++ b/.github/workflows/dealix-api-ci.yml @@ -1,5 +1,6 @@ # Canonical CI for the Dealix API package (monorepo). # GitHub only loads workflows from the repository root .github/workflows/. +# Three jobs so branch protection can require: pytest, smoke_inprocess, launch_readiness. name: Dealix API CI on: @@ -18,8 +19,17 @@ defaults: run: working-directory: dealix +env: + APP_ENV: test + APP_DEBUG: "false" + ANTHROPIC_API_KEY: test-anthropic-key + DEEPSEEK_API_KEY: test-deepseek-key + GROQ_API_KEY: test-groq-key + GLM_API_KEY: test-glm-key + GOOGLE_API_KEY: test-google-key + jobs: - test: + pytest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -41,37 +51,53 @@ jobs: run: python -m compileall api auto_client_acquisition - name: Tests - env: - APP_ENV: test - APP_DEBUG: "false" - ANTHROPIC_API_KEY: test-anthropic-key - DEEPSEEK_API_KEY: test-deepseek-key - GROQ_API_KEY: test-groq-key - GLM_API_KEY: test-glm-key - GOOGLE_API_KEY: test-google-key - run: pytest -q --no-cov - - - name: In-process API smoke - env: - APP_ENV: test - APP_DEBUG: "false" - ANTHROPIC_API_KEY: test-anthropic-key - DEEPSEEK_API_KEY: test-deepseek-key - GROQ_API_KEY: test-groq-key - GLM_API_KEY: test-glm-key - GOOGLE_API_KEY: test-google-key - run: python scripts/smoke_inprocess.py + # tests/unit expects alternate package facades; run canonical integration tests only. + run: pytest -q --no-cov --ignore=tests/unit - name: Embeddings pipeline placeholder run: python scripts/embeddings_pipeline_placeholder.py - name: Deterministic eval smoke - env: - APP_ENV: test - APP_DEBUG: "false" - ANTHROPIC_API_KEY: test-anthropic-key - DEEPSEEK_API_KEY: test-deepseek-key - GROQ_API_KEY: test-groq-key - GLM_API_KEY: test-glm-key - GOOGLE_API_KEY: test-google-key run: python scripts/run_evals.py + + smoke_inprocess: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: dealix/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx aiosqlite + + - name: In-process API smoke + run: python scripts/smoke_inprocess.py + + launch_readiness: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: dealix/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx aiosqlite + + - name: Launch readiness (GO_PRIVATE_BETA gate) + run: python scripts/launch_readiness_check.py diff --git a/.github/workflows/dealix-staging-smoke.yml b/.github/workflows/dealix-staging-smoke.yml index 7a8b6ef6..21f64ec1 100644 --- a/.github/workflows/dealix-staging-smoke.yml +++ b/.github/workflows/dealix-staging-smoke.yml @@ -1,4 +1,4 @@ -# Manual smoke against a deployed Dealix staging URL (secrets in GitHub only). +# Manual smoke + launch readiness against a deployed Dealix staging URL (secrets in GitHub only). name: Dealix staging smoke on: @@ -17,17 +17,46 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" + cache: pip + cache-dependency-path: dealix/requirements.txt - - name: Install httpx - run: pip install httpx + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx aiosqlite - name: Run staging smoke env: STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} STAGING_API_KEY: ${{ secrets.STAGING_API_KEY }} + APP_ENV: test + APP_DEBUG: "false" + ANTHROPIC_API_KEY: test-anthropic-key + DEEPSEEK_API_KEY: test-deepseek-key + GROQ_API_KEY: test-groq-key + GLM_API_KEY: test-glm-key + GOOGLE_API_KEY: test-google-key run: | if [ -z "$STAGING_BASE_URL" ]; then echo "STAGING_BASE_URL secret not set — skipping." exit 0 fi python scripts/smoke_staging.py --base-url "$STAGING_BASE_URL" + + - name: Launch readiness (expect PAID_BETA_READY) + env: + STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} + APP_ENV: test + APP_DEBUG: "false" + ANTHROPIC_API_KEY: test-anthropic-key + DEEPSEEK_API_KEY: test-deepseek-key + GROQ_API_KEY: test-groq-key + GLM_API_KEY: test-glm-key + GOOGLE_API_KEY: test-google-key + run: | + if [ -z "$STAGING_BASE_URL" ]; then + echo "STAGING_BASE_URL secret not set — skipping launch readiness." + exit 0 + fi + python scripts/launch_readiness_check.py --base-url "$STAGING_BASE_URL" diff --git a/dealix/api/main.py b/dealix/api/main.py index 383cb6a6..851b942d 100644 --- a/dealix/api/main.py +++ b/dealix/api/main.py @@ -22,7 +22,7 @@ from api.routers import ( autonomous, business, command_center, - connector_catalog, + connector_router, customer_ops, customer_success, data, @@ -46,9 +46,8 @@ from api.routers import ( pricing, prospect, public, - revenue, - revenue_company_os, revenue_launch, + revenue, revenue_os, sales, sectors, @@ -147,6 +146,7 @@ def create_app() -> FastAPI: app.include_router(pricing.router) app.include_router(prospect.router) app.include_router(autonomous.router) + app.include_router(autonomous_service_operator.router) app.include_router(data.router) app.include_router(outreach.router) app.include_router(revenue.router) @@ -156,30 +156,28 @@ def create_app() -> FastAPI: app.include_router(dominance.router) app.include_router(full_os.router) app.include_router(customer_success.router) + app.include_router(customer_ops.router) app.include_router(ecosystem.router) app.include_router(command_center.router) app.include_router(revenue_os.router) app.include_router(v3.router) app.include_router(innovation.router) - app.include_router(business.router) - app.include_router(personal_operator.router) - app.include_router(growth_operator.router) app.include_router(platform_services.router) app.include_router(intelligence_layer.router) + app.include_router(growth_operator.router) app.include_router(security_curator.router) app.include_router(growth_curator.router) app.include_router(meeting_intelligence.router) app.include_router(model_router.router) - app.include_router(connector_catalog.router) + app.include_router(connector_router.router) app.include_router(agent_observability.router) app.include_router(targeting_os.router) app.include_router(service_tower.router) 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(customer_ops.router) + app.include_router(business.router) + app.include_router(personal_operator.router) app.include_router(public.router) app.include_router(admin.router) diff --git a/dealix/api/routers/agent_observability.py b/dealix/api/routers/agent_observability.py index ede55137..f66bb353 100644 --- a/dealix/api/routers/agent_observability.py +++ b/dealix/api/routers/agent_observability.py @@ -1,4 +1,4 @@ -"""Agent Observability router — trace events + safety/tone evals.""" +"""Agent observability demo endpoints — evals and trace shapes.""" from __future__ import annotations @@ -6,45 +6,36 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.agent_observability import ( - build_trace_event, - run_eval_pack, - safety_eval, - saudi_tone_eval, -) +from auto_client_acquisition.agent_observability.safety_eval import evaluate_safety +from auto_client_acquisition.agent_observability.saudi_tone_eval import evaluate_saudi_tone +from auto_client_acquisition.agent_observability.trace_events import build_trace_event -router = APIRouter(prefix="/api/v1/agent-observability", tags=["agent-observability"]) +router = APIRouter(prefix="/api/v1/agent-observability", tags=["agent_observability"]) + + +@router.get("/demo") +async def demo() -> dict[str, Any]: + return {"ok": True, "message_ar": "تتبع وتقييم — اربط Langfuse في staging للإنتاج.", "demo": True} + + +@router.post("/eval/safety") +async def eval_safety(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return evaluate_safety(str(payload.get("text_ar") or "")) + + +@router.post("/eval/saudi-tone") +async def eval_saudi_tone(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return evaluate_saudi_tone(str(payload.get("text_ar") or "")) @router.post("/trace/build") -async def trace_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: +async def trace_build(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return build_trace_event( - workflow_name=payload.get("workflow_name", "unknown"), - agent_name=payload.get("agent_name", "unknown"), - status=payload.get("status", "started"), - user_id=payload.get("user_id"), - company_id=payload.get("company_id"), - tool=payload.get("tool"), - policy_result=payload.get("policy_result"), - risk_level=payload.get("risk_level"), - approval_status=payload.get("approval_status"), - latency_ms=float(payload.get("latency_ms", 0)), - cost_estimate=float(payload.get("cost_estimate", 0)), - payload=payload.get("payload"), - output=payload.get("output"), + workflow_name=str(payload.get("workflow_name") or "demo"), + agent_name=str(payload.get("agent_name") or "dealix"), + action_type=str(payload.get("action_type") or "draft"), + policy_result=str(payload.get("policy_result") or "approval_required"), + tool_called=payload.get("tool_called"), + outcome=payload.get("outcome"), + metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}, ) - - -@router.post("/safety/eval") -async def safety_eval_endpoint(text: str = Body(..., embed=True)) -> dict[str, Any]: - return safety_eval(text) - - -@router.post("/tone/eval") -async def tone_eval(text: str = Body(..., embed=True)) -> dict[str, Any]: - return saudi_tone_eval(text) - - -@router.get("/evals/run") -async def evals_run() -> dict[str, Any]: - return run_eval_pack() diff --git a/dealix/api/routers/autonomous_service_operator.py b/dealix/api/routers/autonomous_service_operator.py index f6591cfc..66ed2599 100644 --- a/dealix/api/routers/autonomous_service_operator.py +++ b/dealix/api/routers/autonomous_service_operator.py @@ -1,4 +1,4 @@ -"""Autonomous Service Operator router — chat + decisions + sessions + bundles.""" +"""Autonomous Service Operator — /api/v1/operator (deterministic MVP).""" from __future__ import annotations @@ -7,298 +7,131 @@ 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, + approval_manager as am, + agency_mode, + client_mode, + conversation_router, + executive_mode, + intake_collector, + proof_pack_dispatcher, + self_growth_mode, + service_bundles, + service_delivery_mode, + session_state as ss, + tool_action_planner, + upsell_engine, + whatsapp_renderer, + workflow_runner as wr, ) +from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score -router = APIRouter(prefix="/api/v1/operator", tags=["autonomous-service-operator"]) - -# Process-level memory (demo). Production = Redis/Supabase. -_MEMORY = OperatorMemory() +router = APIRouter(prefix="/api/v1/operator", tags=["autonomous_service_operator"]) + + +def _mode_profile(mode: str) -> dict[str, Any]: + m = (mode or "client").strip().lower() + if m == "executive": + return executive_mode.mode_profile() + if m in ("agency_partner", "agency"): + return agency_mode.mode_profile() + if m in ("self_growth", "self-growth"): + return self_growth_mode.mode_profile() + if m in ("service_delivery", "delivery"): + return service_delivery_mode.mode_profile() + return client_mode.mode_profile() -# ── 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)), - ) +async def operator_chat_message(body: dict[str, Any] = Body(...)) -> dict[str, Any]: + msg = str(body.get("message") or "").strip() + if not msg: + raise HTTPException(status_code=400, detail="message_required") + sid = str(body.get("session_id") or ss.new_session_id()) + ss.touch_session(sid) + mode = str(body.get("mode") or "client") + result = conversation_router.handle_message(sid, msg, mode=mode) + result["mode_profile"] = _mode_profile(mode) + return result @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", ""), - ) +async def operator_chat_decision(body: dict[str, Any] = Body(...)) -> dict[str, Any]: + sid = str(body.get("session_id") or "").strip() + dec = str(body.get("decision") or "").strip() + if not sid or not dec: + raise HTTPException(status_code=400, detail="session_id_and_decision_required") + updated = am.apply_decision(sid, dec) + return {"session": updated, "demo": True} -@router.post("/chat/classify") -async def chat_classify(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return classify_intent(payload.get("message", "")) +@router.get("/session/{session_id}") +async def operator_get_session(session_id: str) -> dict[str, Any]: + s = ss.get_session(session_id) + if not s: + raise HTTPException(status_code=404, detail="session_not_found") + return {**s, "demo": True} -# ── 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("/cards/pending") +async def operator_cards_pending() -> dict[str, Any]: + return {"pending": ss.list_sessions_with_pending(), "demo": True} -@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", ""), +async def operator_service_start(body: dict[str, Any] = Body(...)) -> dict[str, Any]: + sid = str(body.get("session_id") or ss.new_session_id()) + svc_id = str(body.get("service_id") or "").strip() + if not svc_id: + raise HTTPException(status_code=400, detail="service_id_required") + ss.touch_session(sid) + wr.advance(sid, "start_service") + intake = intake_collector.intake_questions(svc_id) + am.set_pending_approval( + sid, + { + "title_ar": f"بدء خدمة: {svc_id}", + "buttons_ar": ["موافقة", "تعديل", "تخطي"], + "service_id": svc_id, + }, ) + return { + "session_id": sid, + "intake": intake, + "excellence": calculate_service_excellence_score(svc_id), + "demo": True, + } -@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.post("/service/continue") +async def operator_service_continue(body: dict[str, Any] = Body(...)) -> dict[str, Any]: + sid = str(body.get("session_id") or "").strip() + event = str(body.get("event") or "draft_ready").strip() + if not sid: + raise HTTPException(status_code=400, detail="session_id_required") + ss.touch_session(sid) + return {"session": wr.advance(sid, event), "demo": True} @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}, - ) +async def operator_proof_pack_demo(service_id: str = "first_10_opportunities") -> dict[str, Any]: + return proof_pack_dispatcher.build_proof_pack(service_id) + + +@router.get("/whatsapp/daily-brief") +async def operator_whatsapp_daily_brief() -> dict[str, Any]: + return whatsapp_renderer.render_daily_brief_stub() + + +@router.get("/bundles") +async def operator_bundles() -> dict[str, Any]: + return service_bundles.list_bundles() + + +@router.get("/tools/matrix") +async def operator_tools_matrix() -> dict[str, Any]: + return tool_action_planner.list_tool_matrix() + + +@router.get("/upsell") +async def operator_upsell(service_id: str = "first_10_opportunities") -> dict[str, Any]: + return upsell_engine.suggest_upsell(service_id) diff --git a/dealix/api/routers/connector_router.py b/dealix/api/routers/connector_router.py new file mode 100644 index 00000000..fb1ad472 --- /dev/null +++ b/dealix/api/routers/connector_router.py @@ -0,0 +1,16 @@ +"""Connector catalog HTTP.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter + +from auto_client_acquisition.connectors.connector_catalog import build_connector_catalog + +router = APIRouter(prefix="/api/v1/connectors", tags=["connectors"]) + + +@router.get("/catalog") +async def catalog() -> dict[str, Any]: + return build_connector_catalog() diff --git a/dealix/api/routers/customer_ops.py b/dealix/api/routers/customer_ops.py index d3011b17..87c71390 100644 --- a/dealix/api/routers/customer_ops.py +++ b/dealix/api/routers/customer_ops.py @@ -1,208 +1,50 @@ -"""Customer Ops router — onboarding + connectors + support + SLA + incidents.""" +"""Customer ops API — onboarding, SLA, connectors (deterministic).""" from __future__ import annotations -from typing import Any - from fastapi import APIRouter, Body -from auto_client_acquisition.customer_ops import ( - SUPPORT_PRIORITIES, - SUPPORTED_CONNECTORS, - build_at_risk_alert, - build_connector_setup_summary, - build_customer_success_plan, - build_first_response_template, - build_incident_response_plan, - build_onboarding_checklist, - build_sla_health_report, - build_weekly_check_in, - classify_sla_breach, - classify_ticket_priority, - record_sla_event, - route_ticket, - triage_incident, - update_connector_status, - update_onboarding_step, -) +from auto_client_acquisition.customer_ops.connector_setup_status import build_connector_status +from auto_client_acquisition.customer_ops.customer_success_cadence import build_weekly_cadence +from auto_client_acquisition.customer_ops.incident_router import build_incident_playbook, classify_incident +from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist +from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary +from auto_client_acquisition.customer_ops.support_ticket_router import route_ticket router = APIRouter(prefix="/api/v1/customer-ops", tags=["customer-ops"]) -# ── Onboarding ─────────────────────────────────────────────── -@router.post("/onboarding/checklist") -async def onboarding_checklist(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_onboarding_checklist( - customer_id=payload.get("customer_id", ""), - company_name=payload.get("company_name", ""), - bundle_id=payload.get("bundle_id"), - ) +@router.get("/onboarding/checklist") +async def onboarding_checklist(service_id: str | None = None) -> dict[str, object]: + return build_onboarding_checklist(service_id) -@router.post("/onboarding/update-step") -async def onboarding_update_step(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return update_onboarding_step( - payload.get("checklist") or {}, - step_id=payload.get("step_id", ""), - completed=bool(payload.get("completed", True)), - notes=payload.get("notes", ""), - ) +@router.get("/support/sla") +async def support_sla() -> dict[str, object]: + return build_sla_summary() -@router.get("/onboarding/checklist/demo") -async def onboarding_checklist_demo() -> dict[str, Any]: - return build_onboarding_checklist( - customer_id="demo", company_name="شركة نمو للتدريب", - bundle_id="growth_starter", - ) +@router.get("/connectors/status") +async def connectors_status() -> dict[str, object]: + return build_connector_status() -# ── Connectors ─────────────────────────────────────────────── -@router.get("/connectors/catalog") -async def connectors_catalog() -> dict[str, Any]: - return { - "total": len(SUPPORTED_CONNECTORS), - "connectors": [dict(c) for c in SUPPORTED_CONNECTORS], - } +@router.get("/success/cadence") +async def success_cadence() -> dict[str, object]: + return build_weekly_cadence() -@router.post("/connectors/summary") -async def connectors_summary(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_connector_setup_summary( - customer_id=payload.get("customer_id", ""), - statuses=payload.get("statuses"), - ) - - -@router.post("/connectors/update") -async def connectors_update(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - statuses = payload.get("statuses") or {} - try: - return {"statuses": update_connector_status( - statuses, - connector_key=payload.get("connector_key", ""), - state=payload.get("state", "not_started"), - notes=payload.get("notes", ""), - )} - except ValueError as exc: - return {"error": str(exc)} - - -@router.get("/connectors/demo") -async def connectors_demo() -> dict[str, Any]: - return build_connector_setup_summary( - customer_id="demo", - statuses={ - "gmail": {"state": "connected_draft_only"}, - "google_calendar": {"state": "connected_draft_only"}, - "moyasar": {"state": "configuring"}, - "whatsapp_cloud": {"state": "not_started"}, - }, - ) - - -# ── Support ────────────────────────────────────────────────── -@router.get("/support/priorities") -async def support_priorities() -> dict[str, Any]: - return {"priorities": [dict(p) for p in SUPPORT_PRIORITIES]} - - -@router.post("/support/classify") -async def support_classify(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return classify_ticket_priority(payload.get("text", "")) +@router.get("/incidents/playbook") +async def incidents_playbook() -> dict[str, object]: + return build_incident_playbook() @router.post("/support/route") -async def support_route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return route_ticket( - text=payload.get("text", ""), - customer_id=payload.get("customer_id", ""), - contact_email=payload.get("contact_email", ""), - ) +async def support_route(payload: dict[str, object] = Body(default_factory=dict)) -> dict[str, object]: + issue = str(payload.get("issue_ar") or "") + return route_ticket(issue) -@router.get("/support/first-response/{priority}") -async def support_first_response(priority: str) -> dict[str, Any]: - return build_first_response_template(priority) - - -# ── SLA ────────────────────────────────────────────────────── -@router.post("/sla/event") -async def sla_event(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - try: - return record_sla_event( - ticket_id=payload.get("ticket_id", ""), - priority=payload.get("priority", "P3"), - event=payload.get("event", "opened"), - ) - except ValueError as exc: - return {"error": str(exc)} - - -@router.post("/sla/classify-breach") -async def sla_classify_breach(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return classify_sla_breach( - priority=payload.get("priority", "P3"), - minutes_to_first_response=payload.get("minutes_to_first_response"), - hours_to_resolve=payload.get("hours_to_resolve"), - ) - - -@router.post("/sla/health-report") -async def sla_health_report(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_sla_health_report(tickets=payload.get("tickets") or []) - - -@router.get("/sla/health-report/demo") -async def sla_health_report_demo() -> dict[str, Any]: - return build_sla_health_report(tickets=[ - {"priority": "P0", "first_response_min": 12, "resolution_hours": 2.5}, - {"priority": "P1", "first_response_min": 90, "resolution_hours": 18}, - {"priority": "P2", "first_response_min": 600, "resolution_hours": 70}, - {"priority": "P3", "first_response_min": 1200, "resolution_hours": 100}, - ]) - - -# ── Incidents ──────────────────────────────────────────────── -@router.post("/incidents/triage") -async def incidents_triage(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return triage_incident( - title=payload.get("title", ""), - description=payload.get("description", ""), - affected_customers=int(payload.get("affected_customers", 1)), - has_data_leak=bool(payload.get("has_data_leak", False)), - has_unauthorized_send=bool(payload.get("has_unauthorized_send", False)), - ) - - -@router.get("/incidents/response-plan/{severity}") -async def incidents_response_plan(severity: str) -> dict[str, Any]: - return build_incident_response_plan(severity=severity) - - -# ── Customer Success ───────────────────────────────────────── -@router.post("/cs/weekly-check-in") -async def cs_weekly_check_in(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_weekly_check_in( - customer_id=payload.get("customer_id", ""), - company_name=payload.get("company_name", ""), - metrics=payload.get("metrics"), - ) - - -@router.post("/cs/at-risk-alert") -async def cs_at_risk_alert(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_at_risk_alert( - customer_id=payload.get("customer_id", ""), - days_inactive=int(payload.get("days_inactive", 0)), - drafts_pending=int(payload.get("drafts_pending", 0)), - last_proof_pack_days_ago=int(payload.get("last_proof_pack_days_ago", 0)), - ) - - -@router.post("/cs/success-plan") -async def cs_success_plan(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_customer_success_plan( - customer_id=payload.get("customer_id", ""), - bundle_id=payload.get("bundle_id", "growth_starter"), - ) +@router.get("/incidents/classify") +async def incidents_classify(severity: str = "P3") -> dict[str, object]: + return classify_incident(severity) diff --git a/dealix/api/routers/growth_curator.py b/dealix/api/routers/growth_curator.py index fd0ddc52..28cee03c 100644 --- a/dealix/api/routers/growth_curator.py +++ b/dealix/api/routers/growth_curator.py @@ -1,4 +1,4 @@ -"""Growth Curator router — message grading + weekly curator report.""" +"""Growth curator API — grading and weekly report.""" from __future__ import annotations @@ -6,95 +6,33 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.growth_curator import ( - build_weekly_curator_report, - detect_duplicates, - grade_message, - inventory_skills, - recommend_next_mission, - suggest_improvement, -) +from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report +from auto_client_acquisition.growth_curator.message_curator import grade_message +from auto_client_acquisition.growth_curator.mission_curator import curate_missions_weekly +from auto_client_acquisition.growth_curator.skill_inventory import list_skill_inventory -router = APIRouter(prefix="/api/v1/growth-curator", tags=["growth-curator"]) - - -@router.get("/skills/inventory") -async def skills_inventory() -> dict[str, Any]: - return inventory_skills() - - -@router.post("/messages/grade") -async def messages_grade( - message: str = Body(..., embed=True), - sector: str | None = Body(default=None, embed=True), - channel: str = Body(default="whatsapp", embed=True), -) -> dict[str, Any]: - return grade_message(message, sector=sector, channel=channel).to_dict() - - -@router.post("/messages/improve") -async def messages_improve( - message: str = Body(..., embed=True), - sector: str | None = Body(default=None, embed=True), -) -> dict[str, Any]: - return suggest_improvement(message, sector=sector) - - -@router.post("/messages/duplicates") -async def messages_duplicates( - messages: list[str] = Body(..., embed=True), - threshold: float = Body(default=0.85, embed=True), -) -> dict[str, Any]: - pairs = detect_duplicates(messages, threshold=threshold) - return { - "pairs": [{"i": i, "j": j, "similarity": s} for i, j, s in pairs], - "count": len(pairs), - } - - -@router.post("/missions/next") -async def missions_next( - history: list[dict[str, Any]] = Body(default_factory=list, embed=True), - growth_brain: dict[str, Any] | None = Body(default=None, embed=True), -) -> dict[str, Any]: - return recommend_next_mission(history, growth_brain=growth_brain) - - -@router.post("/report/weekly") -async def report_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_weekly_curator_report( - messages=payload.get("messages", []), - playbooks=payload.get("playbooks", []), - missions=payload.get("missions", []), - sector=payload.get("sector"), - ) +router = APIRouter(prefix="/api/v1/growth-curator", tags=["growth_curator"]) @router.get("/report/demo") async def report_demo() -> dict[str, Any]: - """Demo curator report with a small synthetic dataset.""" - return build_weekly_curator_report( - messages=[ - {"id": "m1", "text": "هلا أحمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"}, - {"id": "m2", "text": "هلا محمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"}, - {"id": "m3", "text": "آخر فرصة! ضمان 100% نتائج مضمونة!"}, - {"id": "m4", "text": "Hi"}, - ], - playbooks=[ - {"id": "pb1", "title": "Warm B2B intro - training", "used_count": 20, - "accept_count": 12, "replied_count": 8, "meeting_count": 4, "deal_count": 2, - "sectors": "training"}, - {"id": "pb2", "title": "Warm B2B intro - training-ksa", "used_count": 8, - "accept_count": 4, "replied_count": 2, "meeting_count": 1, "deal_count": 0, - "sectors": "training"}, - {"id": "pb3", "title": "Cold call SaaS", "used_count": 50, - "accept_count": 5, "replied_count": 2, "meeting_count": 0, "deal_count": 0, - "sectors": "saas"}, - ], - missions=[ - {"mission_id": "first_10_opportunities", "opportunities_generated": 10, - "drafts_approved": 4, "meetings_booked": 2, "revenue_influenced_sar": 18000, - "time_to_value_minutes": 8, "risks_blocked": 2}, - ], - sector="training", + return build_weekly_curator_report() + + +@router.post("/messages/grade") +async def messages_grade(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return grade_message( + str(payload.get("message_ar") or ""), + sector=str(payload.get("sector") or ""), + channel=str(payload.get("channel") or "whatsapp"), ) + + +@router.get("/skills/demo") +async def skills_demo() -> dict[str, Any]: + return list_skill_inventory() + + +@router.get("/missions/curate/demo") +async def missions_curate_demo() -> dict[str, Any]: + return curate_missions_weekly() diff --git a/dealix/api/routers/growth_operator.py b/dealix/api/routers/growth_operator.py index 605bbc95..99543c7a 100644 --- a/dealix/api/routers/growth_operator.py +++ b/dealix/api/routers/growth_operator.py @@ -1,260 +1,38 @@ """ -Growth Operator router — Arabic Growth Operator endpoints. +Growth Operator — thin product-facing aliases over innovation + business. -Approval-first: every outbound is draft. Nothing is sent / charged / -scheduled live from this router; that happens in dedicated send / billing -/ calendar services after explicit user approval. +لا يكرر منطق ten-in-ten؛ يعرّف مسارات متوقعة في وثائق الـ beta والـ smoke. """ from __future__ import annotations -import logging from typing import Any -from fastapi import APIRouter, Body, Query +from fastapi import APIRouter -from auto_client_acquisition.growth_operator import ( - build_calendar_draft, - build_meeting_agenda, - build_moyasar_payment_link_draft, - build_post_meeting_followup, - build_weekly_proof_pack, - contactability_summary, - dedupe_contacts, - draft_arabic_message, - draft_followup, - draft_objection_response, - draft_partner_outreach, - list_missions, - partner_scorecard, - profile_from_dict, - recommend_top_10, - run_mission, - score_contactability, - suggest_partner_types, - summarize_import, -) +from auto_client_acquisition.business.proof_pack import build_demo_proof_pack +from auto_client_acquisition.innovation.growth_missions import list_growth_missions -router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth-operator"]) -log = logging.getLogger(__name__) +router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth_operator"]) -# ── 1. Contacts: import preview ───────────────────────────────── -@router.post("/contacts/import-preview") -async def contacts_import_preview( - contacts: list[dict[str, Any]] = Body(default_factory=list, embed=True), - channel: str = Body(default="whatsapp", embed=True), -) -> dict[str, Any]: - """Preview import: dedupe + source classify + contactability summary.""" - deduped = dedupe_contacts(contacts) - return { - "import_summary": summarize_import(contacts), - "contactability": contactability_summary(deduped, channel=channel), - "policy_note_ar": ( - "العميل يرفع أرقام مملوكة/مصرح بها. لا cold WhatsApp بدون lawful basis." - ), - "approval_required": True, - "approval_status": "pending_approval", - } - - -# ── 2. Targeting: top-10 ──────────────────────────────────────── -@router.post("/targets/top-10") -async def targets_top_10( - contacts: list[dict[str, Any]] = Body(default_factory=list, embed=True), - sector_hint: str = Body(default="", embed=True), - channel: str = Body(default="whatsapp", embed=True), -) -> dict[str, Any]: - """Rank uploaded contacts → top-10 safe + Why-Now.""" - return recommend_top_10(contacts, sector_hint=sector_hint, channel=channel) - - -# ── 3. Messages: draft / followup / objection ────────────────── -@router.post("/messages/draft") -async def messages_draft( - contact: dict[str, Any] = Body(..., embed=True), - profile: dict[str, Any] | None = Body(default=None, embed=True), - goal_ar: str = Body(default="تشغيل نمو B2B بلا إرسال عشوائي", embed=True), -) -> dict[str, Any]: - """Saudi-tone Arabic outreach draft (always pending_approval).""" - return draft_arabic_message(contact, profile=profile, goal_ar=goal_ar) - - -@router.post("/messages/followup") -async def messages_followup( - contact: dict[str, Any] = Body(..., embed=True), - days_since_last: int = Body(default=3, embed=True), - last_outcome: str = Body(default="no_reply", embed=True), -) -> dict[str, Any]: - return draft_followup( - contact, days_since_last=days_since_last, last_outcome=last_outcome, - ) - - -@router.post("/messages/objection-response") -async def messages_objection_response( - objection_id: str = Body(..., embed=True), - contact: dict[str, Any] | None = Body(default=None, embed=True), -) -> dict[str, Any]: - return draft_objection_response(objection_id, contact=contact) - - -# ── 4. Partners: suggest / outreach / scorecard ──────────────── -@router.post("/partners/suggest") -async def partners_suggest( - sector: str = Body(default="", embed=True), - customer_size: str = Body(default="smb", embed=True), -) -> dict[str, Any]: - return suggest_partner_types(sector=sector, customer_size=customer_size) - - -@router.post("/partners/outreach") -async def partners_outreach( - partner_type_key: str = Body(..., embed=True), - partner_name: str = Body(default="", embed=True), - customer_name: str = Body(default="Dealix", embed=True), -) -> dict[str, Any]: - return draft_partner_outreach( - partner_type_key=partner_type_key, - partner_name=partner_name, - customer_name=customer_name, - ) - - -@router.post("/partners/scorecard") -async def partners_scorecard(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return partner_scorecard( - partner_id=payload.get("partner_id", "unknown"), - intros_made=int(payload.get("intros_made", 0)), - deals_influenced=int(payload.get("deals_influenced", 0)), - revenue_share_paid_sar=float(payload.get("revenue_share_paid_sar", 0)), - relationship_age_months=int(payload.get("relationship_age_months", 0)), - ) - - -# ── 5. Meetings: agenda / calendar draft / followup ──────────── -@router.post("/meetings/draft") -async def meetings_draft( - contact_name: str = Body(..., embed=True), - company: str = Body(..., embed=True), - contact_email: str | None = Body(default=None, embed=True), - purpose_ar: str = Body(default="اكتشاف وتأهيل أولي", embed=True), - duration_minutes: int = Body(default=20, embed=True), - proposed_start_iso: str | None = Body(default=None, embed=True), -) -> dict[str, Any]: - """Build agenda + calendar draft (NOT created live).""" - agenda = build_meeting_agenda( - contact_name=contact_name, - company=company, - purpose_ar=purpose_ar, - duration_minutes=duration_minutes, - ) - cal_draft = build_calendar_draft( - contact_email=contact_email, - contact_name=contact_name, - company=company, - proposed_start_iso=proposed_start_iso, - duration_minutes=duration_minutes, - ) - return {"agenda": agenda, "calendar_draft": cal_draft} - - -@router.post("/meetings/post-followup") -async def meetings_post_followup( - contact_name: str = Body(..., embed=True), - company: str = Body(..., embed=True), - summary_ar: str = Body(..., embed=True), - next_step_ar: str = Body(default="أرسل recap + pilot offer", embed=True), -) -> dict[str, Any]: - return build_post_meeting_followup( - contact_name=contact_name, - company=company, - summary_ar=summary_ar, - next_step_ar=next_step_ar, - ) - - -# ── 6. Payment offer (Moyasar payment-link draft) ───────────── -@router.post("/payment-offer/draft") -async def payment_offer_draft( - plan_key: str = Body(..., embed=True), - customer_id: str = Body(..., embed=True), - contact_email: str | None = Body(default=None, embed=True), - custom_amount_sar: float | None = Body(default=None, embed=True), -) -> dict[str, Any]: - return build_moyasar_payment_link_draft( - plan_key=plan_key, - customer_id=customer_id, - contact_email=contact_email, - custom_amount_sar=custom_amount_sar, - ) - - -# ── 7. Missions ──────────────────────────────────────────────── @router.get("/missions") -async def missions_list() -> dict[str, Any]: - return list_missions() +async def missions() -> dict[str, Any]: + """نفس محتوى ``GET /api/v1/innovation/growth-missions`` مع تسمية منتجية.""" + data = list_growth_missions() + if isinstance(data, dict): + out = dict(data) + out["canonical_route"] = "/api/v1/innovation/growth-missions" + return out + return {"missions": data, "canonical_route": "/api/v1/innovation/growth-missions"} -@router.post("/missions/{mission_id}/run") -async def missions_run( - mission_id: str, - payload: dict[str, Any] = Body(default_factory=dict), -) -> dict[str, Any]: - return run_mission(mission_id, payload=payload) - - -# ── 8. Proof Pack demo ───────────────────────────────────────── @router.get("/proof-pack/demo") -async def proof_pack_demo( - customer_id: str = Query(default="demo"), - customer_name: str = Query(default="Demo Saudi B2B Co."), -) -> dict[str, Any]: - return build_weekly_proof_pack( - customer_id=customer_id, - customer_name=customer_name, - week_label="W18-2026", - plan_cost_weekly_sar=750, - opportunities_discovered=42, - messages_drafted=38, - messages_approved=33, - messages_sent=33, - replies_received=11, - positive_replies=4, - meetings_booked=3, - meetings_held=2, - proposals_sent=1, - deals_won=0, - pipeline_added_sar=185_000, - revenue_won_sar=0, - risky_drafts_blocked=5, - revenue_leaks_recovered=2, - avg_response_minutes=42, - best_message_subject="ملاحظة على توسعكم في الرياض", - best_message_reply_rate=0.18, - ) - - -# ── 9. Single-contact contactability ───────────────────────── -@router.post("/contactability/score") -async def contactability_score_single( - contact: dict[str, Any] = Body(..., embed=True), - channel: str = Body(default="whatsapp", embed=True), -) -> dict[str, Any]: - return score_contactability(contact, channel=channel) - - -# ── 10. Profile ──────────────────────────────────────────────── -@router.post("/profile") -async def profile_set( - profile: dict[str, Any] = Body(..., embed=True), -) -> dict[str, Any]: - p = profile_from_dict(profile) - return { - "profile": p.to_dict(), - "is_specialized": p.is_specialized(), - "missing_fields_ar": ( - [] if p.is_specialized() else - ["sector", "city", "offer_one_liner", "ideal_customer"] - ), - } +async def proof_pack_demo() -> dict[str, Any]: + """نفس ``GET /api/v1/business/proof-pack/demo`` — مسار موحّد للعرض في الـ beta.""" + pack = build_demo_proof_pack() + if isinstance(pack, dict): + out = dict(pack) + out["canonical_route"] = "/api/v1/business/proof-pack/demo" + return out + return {"pack": pack, "canonical_route": "/api/v1/business/proof-pack/demo"} diff --git a/dealix/api/routers/intelligence_layer.py b/dealix/api/routers/intelligence_layer.py index 0eeb51cc..88cb0581 100644 --- a/dealix/api/routers/intelligence_layer.py +++ b/dealix/api/routers/intelligence_layer.py @@ -1,4 +1,4 @@ -"""Intelligence Layer router — growth brain + missions + DNA + simulator + brief.""" +"""Intelligence layer API — deterministic JSON; optional ten-in-ten bridge.""" from __future__ import annotations @@ -6,135 +6,117 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.intelligence_layer import ( - DecisionMemory, - analyze_competitive_move, - build_board_brief, - build_command_feed_demo, - build_growth_brain, - build_revenue_dna_demo, - compute_trust_score, - extract_revenue_dna, - learn_from_decision, - list_intel_missions, - recommend_missions, - simulate_opportunity, -) +from auto_client_acquisition.innovation.ten_in_ten import build_ten_opportunities +from auto_client_acquisition.intelligence_layer.action_graph import build_action_graph_trace +from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief +from auto_client_acquisition.intelligence_layer.competitive_moves import build_competitive_moves +from auto_client_acquisition.intelligence_layer.decision_memory import list_decisions, record_decision +from auto_client_acquisition.intelligence_layer.growth_brain import build_growth_profile +from auto_client_acquisition.intelligence_layer.intel_command_feed import build_intel_command_feed +from auto_client_acquisition.intelligence_layer.mission_engine import get_mission, list_mission_catalog +from auto_client_acquisition.intelligence_layer.opportunity_simulator import simulate_opportunities +from auto_client_acquisition.intelligence_layer.revenue_dna import build_revenue_dna +from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score -router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence-layer"]) - -# Per-customer in-memory decision memory (demo; production = Supabase) -_MEMORY: dict[str, DecisionMemory] = {} +router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence_layer"]) -def _memory_for(customer_id: str) -> DecisionMemory: - if customer_id not in _MEMORY: - _MEMORY[customer_id] = DecisionMemory(customer_id=customer_id) - return _MEMORY[customer_id] +@router.post("/growth-profile") +async def growth_profile(company: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_growth_profile(company or {}) -# ── Growth Brain ────────────────────────────────────────────── -@router.post("/growth-brain/build") -async def growth_brain_build(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - brain = build_growth_brain(payload) - return {**brain.to_dict(), "ready_for_autopilot": brain.is_ready_for_autopilot()} +@router.get("/command-feed") +async def intel_command_feed() -> dict[str, Any]: + return build_intel_command_feed() -# ── Command Feed ────────────────────────────────────────────── @router.get("/command-feed/demo") -async def command_feed_demo() -> dict[str, Any]: - return build_command_feed_demo() +async def intel_command_feed_demo() -> dict[str, Any]: + """Alias of ``GET /command-feed`` for product/docs compatibility.""" + return build_intel_command_feed() -# ── Missions ────────────────────────────────────────────────── -@router.get("/missions") -async def missions_list() -> dict[str, Any]: - return list_intel_missions() +@router.post("/missions/first-10-opportunities") +async def missions_first_10_opportunities( + payload: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + """Thin wrapper around innovation ``build_ten_opportunities`` — no duplicate logic.""" + return build_ten_opportunities(payload or None) -@router.post("/missions/recommend") -async def missions_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - brain_payload = payload.get("growth_brain") or payload - brain = build_growth_brain(brain_payload) if brain_payload else None - return recommend_missions(brain, limit=int(payload.get("limit", 3))) +@router.get("/missions/catalog") +async def missions_catalog() -> dict[str, Any]: + """Mission engine metadata + pointer to innovation missions.""" + return list_mission_catalog() + + +@router.get("/missions/{mission_id}") +async def mission_detail(mission_id: str) -> dict[str, Any]: + return get_mission(mission_id) + + +@router.post("/action-graph/demo") +async def action_graph_demo(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_action_graph_trace(payload or {}) + + +@router.get("/decision-memory/demo") +async def decision_memory_demo() -> dict[str, Any]: + return list_decisions(limit=20) + + +@router.post("/decision-memory/record") +async def decision_memory_record(entry: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return record_decision(entry or {}) -# ── Trust Score ─────────────────────────────────────────────── @router.post("/trust-score") -async def trust_score(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return compute_trust_score( - source_quality=payload.get("source_quality", "unknown"), - opt_in=bool(payload.get("opt_in", False)), - channel=payload.get("channel", "whatsapp"), - message_text=payload.get("message_text", ""), - frequency_count_this_week=int(payload.get("frequency_count_this_week", 0)), - weekly_cap=int(payload.get("weekly_cap", 2)), - approval_status=payload.get("approval_status", "pending"), - ) - - -# ── Revenue DNA ─────────────────────────────────────────────── -@router.get("/revenue-dna/demo") -async def revenue_dna_demo() -> dict[str, Any]: - return build_revenue_dna_demo() +async def trust_score(signals: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return compute_trust_score(signals or {}) @router.post("/revenue-dna") -async def revenue_dna_post(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return extract_revenue_dna( - customer_id=payload.get("customer_id", "unknown"), - won_deals=payload.get("won_deals", []), - replies=payload.get("replies", []), - objections=payload.get("objections", []), - ) +async def revenue_dna(context: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_revenue_dna(context or {}) -# ── Opportunity Simulator ───────────────────────────────────── -@router.post("/simulate-opportunity") -async def simulate_opportunity_endpoint(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return simulate_opportunity( - target_count=int(payload.get("target_count", 100)), - sector=payload.get("sector", "saas"), - avg_deal_value_sar=float(payload.get("avg_deal_value_sar", 25_000)), - channel=payload.get("channel", "whatsapp"), - cold_pct=float(payload.get("cold_pct", 0)), - quality_lift=float(payload.get("quality_lift", 1.0)), - ) +@router.post("/opportunity-simulator") +async def opportunity_simulator(inputs: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return simulate_opportunities(inputs or {}) -# ── Competitive Moves ───────────────────────────────────────── -@router.post("/competitive-move/analyze") -async def competitive_move_analyze(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return analyze_competitive_move( - competitor_name=payload.get("competitor_name", "?"), - move_type=payload.get("move_type", "new_offer"), - payload=payload.get("payload", {}), - ) +@router.post("/board-brief") +async def board_brief(snapshot: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_board_brief(snapshot or {}) -# ── Board Brief ─────────────────────────────────────────────── -@router.get("/board-brief/demo") -async def board_brief_demo() -> dict[str, Any]: - return build_board_brief() +@router.get("/competitive-moves") +async def competitive_moves(sector: str | None = None) -> dict[str, Any]: + return build_competitive_moves(sector) -# ── Decision Memory ─────────────────────────────────────────── -@router.post("/decisions/record") -async def decisions_record(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - customer_id = payload.get("customer_id", "demo") - mem = _memory_for(customer_id) - return learn_from_decision( - memory=mem, - decision=payload.get("decision", "skip"), - action_type=payload.get("action_type", "send_whatsapp"), - channel=payload.get("channel", "whatsapp"), - sector=payload.get("sector"), - tone=payload.get("tone"), - objection_id=payload.get("objection_id"), - ) - - -@router.get("/decisions/preferences") -async def decisions_preferences(customer_id: str) -> dict[str, Any]: - mem = _memory_for(customer_id) - return {"customer_id": customer_id, "preferences": mem.preferences()} +@router.post("/bundle") +async def intelligence_bundle( + payload: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + """ + Single round-trip for demos. Optional ``include_ten_in_ten`` merges + ``build_ten_opportunities`` without exposing a duplicate HTTP path. + """ + company = payload.get("company") if isinstance(payload.get("company"), dict) else {} + out: dict[str, Any] = { + "growth_profile": build_growth_profile(company), + "intel_command_feed": build_intel_command_feed({"append_custom": payload.get("extra_card")}), + "trust_score": compute_trust_score(payload.get("trust_signals") if isinstance(payload.get("trust_signals"), dict) else {}), + "revenue_dna": build_revenue_dna(payload.get("revenue_context") if isinstance(payload.get("revenue_context"), dict) else {}), + "opportunity_simulator": simulate_opportunities( + payload.get("simulator") if isinstance(payload.get("simulator"), dict) else {} + ), + "board_brief": build_board_brief(payload.get("board") if isinstance(payload.get("board"), dict) else {}), + "competitive_moves": build_competitive_moves(str(payload.get("sector") or "") or None), + } + if payload.get("include_ten_in_ten"): + ten_payload = payload.get("ten_in_ten") if isinstance(payload.get("ten_in_ten"), dict) else company + out["ten_in_ten"] = build_ten_opportunities(ten_payload) + return out diff --git a/dealix/api/routers/launch_ops.py b/dealix/api/routers/launch_ops.py index 65f16d8c..908562d1 100644 --- a/dealix/api/routers/launch_ops.py +++ b/dealix/api/routers/launch_ops.py @@ -1,4 +1,4 @@ -"""Launch Ops router — Private Beta + Demo + Outreach + Go/No-Go + Scorecard.""" +"""Launch ops API — private beta, demo, outreach, go/no-go.""" from __future__ import annotations @@ -6,130 +6,40 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.launch_ops import ( - build_12_min_demo_flow, - build_close_script, - build_daily_launch_scorecard, - build_discovery_questions, - build_first_20_segments, - build_followup_message, - build_launch_readiness, - build_objection_responses, - build_outreach_message, - build_private_beta_offer, - build_private_beta_safety_notes, - build_reply_handlers, - build_weekly_launch_scorecard, - decide_go_no_go, - private_beta_faq, - record_launch_event, -) +from auto_client_acquisition.launch_ops.demo_flow import build_demo_script +from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go +from auto_client_acquisition.launch_ops.launch_scorecard import build_launch_scorecard +from auto_client_acquisition.launch_ops.outreach_messages import build_first_twenty_outreach +from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer -router = APIRouter(prefix="/api/v1/launch", tags=["launch-ops"]) +router = APIRouter(prefix="/api/v1/launch", tags=["launch_ops"]) -# ── Private Beta ───────────────────────────────────────────── @router.get("/private-beta/offer") -async def private_beta_offer() -> dict[str, Any]: - return { - "offer": build_private_beta_offer(), - "safety": build_private_beta_safety_notes(), - "faq": private_beta_faq(), - } +async def launch_private_beta_offer() -> dict[str, Any]: + return build_private_beta_offer() -# ── Demo flow ──────────────────────────────────────────────── -@router.get("/demo/flow") -async def demo_flow() -> dict[str, Any]: - return { - "flow": build_12_min_demo_flow(), - "discovery_questions": build_discovery_questions(), - "objections": build_objection_responses(), - "close": build_close_script(), - } +@router.get("/demo-script") +async def launch_demo_script() -> dict[str, Any]: + return build_demo_script() -# ── Outreach ───────────────────────────────────────────────── @router.get("/outreach/first-20") -async def outreach_first_20() -> dict[str, Any]: - segments = build_first_20_segments() - sample_messages = { - s["id"]: build_outreach_message(s["id"]) - for s in segments["segments"] - } - return { - **segments, - "sample_messages": sample_messages, - "reply_handlers": build_reply_handlers(), - } +async def launch_outreach_first_20() -> dict[str, Any]: + return build_first_twenty_outreach() -@router.post("/outreach/message") -async def outreach_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_outreach_message( - segment_id=payload.get("segment_id", ""), - name=payload.get("name", "[الاسم]"), - ) +@router.get("/go-no-go") +async def launch_go_no_go_get() -> dict[str, Any]: + return evaluate_go_no_go(None) -@router.post("/outreach/followup") -async def outreach_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_followup_message( - segment_id=payload.get("segment_id", ""), - step=int(payload.get("step", 1)), - name=payload.get("name", "[الاسم]"), - ) - - -# ── Go / No-Go ─────────────────────────────────────────────── @router.post("/go-no-go") -async def go_no_go(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return decide_go_no_go(statuses=payload.get("statuses")) +async def launch_go_no_go_post(flags: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return evaluate_go_no_go(flags or {}) -@router.get("/readiness") -async def readiness() -> dict[str, Any]: - """Readiness with all gates assumed False (use POST /go-no-go for real status).""" - return build_launch_readiness(statuses={}) - - -# ── Scorecard ──────────────────────────────────────────────── -@router.post("/scorecard/event") -async def scorecard_event(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - try: - return record_launch_event( - event_type=payload.get("event_type", ""), - customer_id=payload.get("customer_id"), - notes=payload.get("notes"), - ) - except ValueError as exc: - return {"error": str(exc)} - - -@router.post("/scorecard/daily") -async def scorecard_daily( - events: list[dict[str, Any]] = Body(default_factory=list, embed=True), -) -> dict[str, Any]: - return build_daily_launch_scorecard(events=events) - - -@router.post("/scorecard/weekly") -async def scorecard_weekly( - events: list[dict[str, Any]] = Body(default_factory=list, embed=True), -) -> dict[str, Any]: - return build_weekly_launch_scorecard(events=events) - - -@router.get("/scorecard/demo") -async def scorecard_demo() -> dict[str, Any]: - """Demo scorecard with synthetic events.""" - demo_events = [ - {"event_type": "outreach_sent"} for _ in range(15) - ] + [ - {"event_type": "reply_received"} for _ in range(4) - ] + [ - {"event_type": "demo_booked"} for _ in range(2) - ] + [ - {"event_type": "blocked_action"} for _ in range(6) - ] - return build_daily_launch_scorecard(events=demo_events) +@router.get("/scorecard") +async def launch_scorecard() -> dict[str, Any]: + return build_launch_scorecard() diff --git a/dealix/api/routers/meeting_intelligence.py b/dealix/api/routers/meeting_intelligence.py index 146da496..9782efc0 100644 --- a/dealix/api/routers/meeting_intelligence.py +++ b/dealix/api/routers/meeting_intelligence.py @@ -1,4 +1,4 @@ -"""Meeting Intelligence router — pre-meeting brief, transcript summary, follow-up.""" +"""Meeting intelligence API — text in, Arabic briefs out (no Calendar insert).""" from __future__ import annotations @@ -6,65 +6,32 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.meeting_intelligence import ( - build_post_meeting_followup, - build_pre_meeting_brief, - compute_deal_risk, - extract_objections, - parse_transcript_entries, - summarize_meeting, -) +from auto_client_acquisition.meeting_intelligence.followup_builder import build_post_meeting_followup +from auto_client_acquisition.meeting_intelligence.meeting_brief import build_pre_meeting_brief +from auto_client_acquisition.meeting_intelligence.objection_extractor import extract_objections +from auto_client_acquisition.meeting_intelligence.transcript_parser import summarize_transcript_text -router = APIRouter(prefix="/api/v1/meeting-intelligence", tags=["meeting-intelligence"]) - - -@router.post("/brief") -async def brief(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_pre_meeting_brief( - company=payload.get("company"), - contact=payload.get("contact"), - opportunity=payload.get("opportunity"), - sector=payload.get("sector"), - ) - - -@router.get("/brief/demo") -async def brief_demo() -> dict[str, Any]: - return build_pre_meeting_brief( - company={"name": "شركة نمو للتدريب", "sector": "training"}, - contact={"name": "أحمد", "role": "مدير المبيعات"}, - opportunity={"expected_value_sar": 18000}, - sector="training", - ) +router = APIRouter(prefix="/api/v1/meeting-intelligence", tags=["meeting_intelligence"]) @router.post("/transcript/summarize") -async def transcript_summarize(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - parsed = parse_transcript_entries(payload.get("entries") or payload.get("text", "")) - summary = summarize_meeting(parsed) - objections = extract_objections( - " ".join(t["text"] for t in parsed.get("speaker_turns", [])) - ) - return {"parsed": parsed, "summary": summary, "objections": objections} +async def transcript_summarize(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + text = str(payload.get("text") or "") + base = summarize_transcript_text(text) + base["objections"] = extract_objections(text) + return base @router.post("/followup/draft") -async def followup_draft(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_post_meeting_followup( - summary=payload.get("summary"), - next_steps=payload.get("next_steps", []), - contact_name=payload.get("contact_name", ""), - company_name=payload.get("company_name", ""), - objections=payload.get("objections", []), - ) +async def followup_draft(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + summary = str(payload.get("summary_ar") or "") + steps = payload.get("next_steps") if isinstance(payload.get("next_steps"), list) else None + return build_post_meeting_followup(summary, steps) -@router.post("/deal-risk") -async def deal_risk(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return compute_deal_risk( - objections=payload.get("objections", []), - next_step_set=bool(payload.get("next_step_set", False)), - decision_maker_present=bool(payload.get("decision_maker_present", False)), - days_since_last_touch=int(payload.get("days_since_last_touch", 0)), - expected_value_sar=float(payload.get("expected_value_sar", 0)), - ) +@router.post("/brief/pre-meeting") +async def pre_meeting_brief(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + company = payload.get("company") if isinstance(payload.get("company"), dict) else {} + contact = payload.get("contact") if isinstance(payload.get("contact"), dict) else {} + opportunity = payload.get("opportunity") if isinstance(payload.get("opportunity"), dict) else {} + return build_pre_meeting_brief(company, contact, opportunity) diff --git a/dealix/api/routers/model_router.py b/dealix/api/routers/model_router.py index 678e0a10..a648c054 100644 --- a/dealix/api/routers/model_router.py +++ b/dealix/api/routers/model_router.py @@ -1,4 +1,4 @@ -"""Model Router router — task routing + provider registry + cost class.""" +"""Model routing API — configuration hints only.""" from __future__ import annotations @@ -6,57 +6,22 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.model_router import ( - ALL_PROVIDERS, - ALL_TASK_TYPES, - build_usage_demo, - classify_cost, - route_task, -) +from auto_client_acquisition.model_router.provider_registry import list_providers +from auto_client_acquisition.model_router.task_router import list_tasks, route_task -router = APIRouter(prefix="/api/v1/model-router", tags=["model-router"]) - - -@router.get("/providers") -async def providers() -> dict[str, Any]: - return { - "total": len(ALL_PROVIDERS), - "providers": [p.to_dict() for p in ALL_PROVIDERS], - } +router = APIRouter(prefix="/api/v1/model-router", tags=["model_router"]) @router.get("/tasks") async def tasks() -> dict[str, Any]: - return {"total": len(ALL_TASK_TYPES), "tasks": list(ALL_TASK_TYPES)} + return list_tasks() @router.post("/route") -async def route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - decision = route_task( - payload.get("task_type", "low_cost_bulk"), - requires_arabic=bool(payload.get("requires_arabic", False)), - requires_vision=bool(payload.get("requires_vision", False)), - sensitivity=payload.get("sensitivity", "low"), - expected_input_tokens=int(payload.get("expected_input_tokens", 0)), - expected_output_tokens=int(payload.get("expected_output_tokens", 0)), - bulk=bool(payload.get("bulk", False)), - primary_provider=payload.get("primary_provider"), - ) - return decision.to_dict() +async def route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return route_task(str(payload.get("task_type") or "")) -@router.post("/cost-class") -async def cost_class(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return { - "cost_class": classify_cost( - task_type=payload.get("task_type", "low_cost_bulk"), - expected_input_tokens=int(payload.get("expected_input_tokens", 0)), - expected_output_tokens=int(payload.get("expected_output_tokens", 0)), - bulk=bool(payload.get("bulk", False)), - ), - } - - -@router.get("/usage/demo") -async def usage_demo() -> dict[str, Any]: - return build_usage_demo() +@router.get("/providers") +async def providers() -> dict[str, Any]: + return list_providers() diff --git a/dealix/api/routers/platform_services.py b/dealix/api/routers/platform_services.py index c9af4d2b..0245eb5a 100644 --- a/dealix/api/routers/platform_services.py +++ b/dealix/api/routers/platform_services.py @@ -1,203 +1,185 @@ -"""Platform Services router — channel registry + events + inbox + policy + proof.""" +"""Platform Services API — Growth Control Tower (no live external sends).""" from __future__ import annotations from typing import Any -from fastapi import APIRouter, Body, Query +from fastapi import APIRouter, Body from auto_client_acquisition.platform_services import ( - ALL_CHANNELS, - POLICY_RULES, - SELLABLE_SERVICES, - build_card_from_event, - build_demo_feed, - build_demo_platform_proof, + build_proof_summary, evaluate_action, - get_channel, - invoke_tool, - list_services, - make_event, - resolve_identity, + event_to_inbox_card, + execute_tool, + get_action_ledger, + get_service_catalog, + list_channels, + validate_event, ) -from auto_client_acquisition.platform_services.action_ledger import ActionLedger -from auto_client_acquisition.platform_services.channel_registry import channels_summary +from auto_client_acquisition.innovation.proof_ledger import build_demo_proof_ledger +from auto_client_acquisition.platform_services.contact_import_preview import build_import_preview +from auto_client_acquisition.platform_services.identity_resolution import resolve_identity_demo +from auto_client_acquisition.platform_services.inbox_feed import build_inbox_feed +from auto_client_acquisition.platform_services.lead_form_ingest import ingest_lead_form +from auto_client_acquisition.platform_services.proof_overview import build_proof_overview -router = APIRouter(prefix="/api/v1/platform", tags=["platform-services"]) - -_LEDGER = ActionLedger() +router = APIRouter(prefix="/api/v1/platform", tags=["platform_services"]) + + +@router.get("/service-catalog") +async def service_catalog() -> dict[str, Any]: + return get_service_catalog() -# ── Catalog ──────────────────────────────────────────────────── @router.get("/services/catalog") -async def services_catalog() -> dict[str, Any]: - return list_services() +async def services_catalog_alias() -> dict[str, Any]: + """Alias path for product docs compatibility.""" + return get_service_catalog() @router.get("/channels") async def channels() -> dict[str, Any]: - return { - "summary": channels_summary(), - "channels": [ - { - "key": c.key, "label_ar": c.label_ar, "label_en": c.label_en, - "capabilities": list(c.capabilities), "beta_status": c.beta_status, - "required_permissions": list(c.required_permissions), - "allowed_actions": list(c.allowed_actions), - "blocked_actions": list(c.blocked_actions), - "risk_level": c.risk_level, "notes_ar": c.notes_ar, - } - for c in ALL_CHANNELS - ], - } + return list_channels() -@router.get("/channels/{channel_key}") -async def channel_detail(channel_key: str) -> dict[str, Any]: - c = get_channel(channel_key) - if c is None: - return {"error": f"unknown channel: {channel_key}"} - return { - "key": c.key, "label_ar": c.label_ar, "label_en": c.label_en, - "capabilities": list(c.capabilities), "beta_status": c.beta_status, - "required_permissions": list(c.required_permissions), - "allowed_actions": list(c.allowed_actions), - "blocked_actions": list(c.blocked_actions), - "risk_level": c.risk_level, "notes_ar": c.notes_ar, - } +@router.post("/events/validate") +async def events_validate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return validate_event(payload or {}) -# ── Policy ───────────────────────────────────────────────────── -@router.get("/policy/rules") -async def policy_rules() -> dict[str, Any]: - return {"count": len(POLICY_RULES), "rules": POLICY_RULES} - - -@router.post("/actions/evaluate") -async def actions_evaluate( - action: str = Body(..., embed=True), - context: dict[str, Any] = Body(default_factory=dict, embed=True), -) -> dict[str, Any]: - d = evaluate_action(action=action, context=context) - return { - "decision": d.decision, - "matched_rule_id": d.matched_rule_id, - "reasons_ar": d.reasons_ar, - "suggested_next_action_ar": d.suggested_next_action_ar, - } +@router.post("/events/ingest") +async def events_ingest(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Validate normalized event and return inbox card — no persistence.""" + v = validate_event(payload or {}) + if not v["valid"]: + return {"ok": False, "errors": v["errors"], "approval_required": True} + ev = v.get("normalized") or {} + return {"ok": True, "event": ev, "card": event_to_inbox_card(ev), "approval_required": True} @router.post("/actions/approve") -async def actions_approve( - customer_id: str = Body(..., embed=True), - action_type: str = Body(..., embed=True), - channel: str = Body(..., embed=True), - actor: str = Body(default="user", embed=True), - payload: dict[str, Any] = Body(default_factory=dict, embed=True), - correlation_id: str | None = Body(default=None, embed=True), -) -> dict[str, Any]: - entry = _LEDGER.append( - customer_id=customer_id, - action_type=action_type, - channel=channel, - stage="approved", - actor=actor, - payload=payload, - correlation_id=correlation_id, +async def actions_approve(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Record human approval/rejection in the in-memory action ledger — no live side effects.""" + ledger = get_action_ledger() + action_id = str(payload.get("action_id") or payload.get("request_id") or "unspecified") + actor = str(payload.get("actor") or "operator") + approved = payload.get("approved") + is_approved = True if approved is None else bool(approved) + entry = ledger.append_decision( + tool="human_approval", + outcome="approved" if is_approved else "rejected", + detail={ + "action_id": action_id, + "actor": actor, + "notes": payload.get("notes"), + }, ) - return {"approved": True, "entry": entry.to_dict()} - - -@router.get("/ledger/summary") -async def ledger_summary(customer_id: str = Query(...)) -> dict[str, Any]: - return _LEDGER.summary(customer_id=customer_id) - - -# ── Events + Inbox ───────────────────────────────────────────── -@router.post("/events/ingest") -async def events_ingest( - event_type: str = Body(..., embed=True), - channel: str = Body(..., embed=True), - customer_id: str = Body(..., embed=True), - payload: dict[str, Any] = Body(default_factory=dict, embed=True), -) -> dict[str, Any]: - try: - evt = make_event( - event_type=event_type, channel=channel, - customer_id=customer_id, payload=payload, - ) - except ValueError as exc: - return {"error": str(exc)} - card = build_card_from_event(evt) return { - "event": evt.to_dict(), - "card": card.to_dict() if card else None, - "actionable": card is not None, + "ok": True, + "ledger_entry": entry, + "detail_ar": "سُجّل القرار في دفتر MVP — لا يُطلق إرسالاً أو دفعاً تلقائياً من هذا المسار.", + "approval_required": False, } +@router.post("/actions/evaluate") +async def actions_evaluate_alias(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Alias of ``POST /policy/evaluate`` for docs that refer to ``actions/evaluate``.""" + return evaluate_action( + action=str(payload.get("action") or ""), + channel_id=str(payload.get("channel_id") or ""), + context=payload.get("context") if isinstance(payload.get("context"), dict) else {}, + ) + + +@router.post("/inbox/from-event") +async def inbox_from_event( + payload: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + event = payload.get("event") if isinstance(payload.get("event"), dict) else payload + merge = bool(payload.get("merge_demo_hint")) + return {"card": event_to_inbox_card(event or {}, merge_demo_hint=merge)} + + +@router.post("/policy/evaluate") +async def policy_evaluate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return evaluate_action( + action=str(payload.get("action") or ""), + channel_id=str(payload.get("channel_id") or ""), + context=payload.get("context") if isinstance(payload.get("context"), dict) else {}, + ) + + +@router.post("/tools/execute") +async def tools_execute(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return execute_tool(str(payload.get("tool_name") or ""), payload.get("payload") if isinstance(payload.get("payload"), dict) else {}) + + +@router.get("/proof/summary") +async def proof_summary() -> dict[str, Any]: + return build_proof_summary() + + +@router.get("/proof-ledger/demo") +async def proof_ledger_demo() -> dict[str, Any]: + """Demo ledger events — same source as innovation demo.""" + return build_demo_proof_ledger() + + +@router.get("/identity/resolve-demo") +async def identity_resolve_demo( + phone: str | None = None, + email: str | None = None, + company_hint: str | None = None, +) -> dict[str, Any]: + return resolve_identity_demo(phone=phone, email=email, company_hint=company_hint) + + +@router.get("/proof/overview") +async def proof_overview() -> dict[str, Any]: + return build_proof_overview() + + @router.get("/inbox/feed") async def inbox_feed() -> dict[str, Any]: - """Demo unified-inbox feed; production version reads from event store.""" - return build_demo_feed() + return build_inbox_feed() -# ── Identity + Tool gateway ─────────────────────────────────── -@router.post("/identity/resolve") -async def identity_resolve( - signals: list[dict[str, Any]] = Body(..., embed=True), -) -> dict[str, Any]: - out = resolve_identity(signals=signals) - return { - "identity_id": out.identity_id, - "primary_phone": out.primary_phone, - "primary_email": out.primary_email, - "company": out.company, - "crm_id": out.crm_id, - "social_handles": out.social_handles, - "confidence": out.confidence, - "sources": out.sources, - } +@router.post("/contacts/import-preview") +async def contacts_import_preview(body: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_import_preview(body or {}) -@router.get("/identity/resolve-demo") -async def identity_resolve_demo() -> dict[str, Any]: - """Sample multi-source identity resolution.""" - out = resolve_identity(signals=[ - {"phone": "+966500000001", "company": "شركة العقار الذهبي", "source": "whatsapp"}, - {"email": "ali@example.sa", "company": "شركة العقار الذهبي", "source": "gmail"}, - {"crm_id": "crm_5421", "company": "شركة العقار الذهبي", "source": "crm"}, - {"social_handles": {"linkedin": "ali-realestate"}, "source": "linkedin_lead_forms"}, - ]) - return { - "identity_id": out.identity_id, - "primary_phone": out.primary_phone, - "primary_email": out.primary_email, - "company": out.company, - "crm_id": out.crm_id, - "social_handles": out.social_handles, - "confidence": out.confidence, - "sources": out.sources, - } +@router.get("/action-ledger/recent") +async def action_ledger_recent(limit: int = 50) -> dict[str, Any]: + lim = max(1, min(limit, 200)) + return {"entries": get_action_ledger().recent(lim)} -@router.post("/tools/invoke") -async def tools_invoke( - tool: str = Body(..., embed=True), - payload: dict[str, Any] = Body(default_factory=dict, embed=True), - context: dict[str, Any] = Body(default_factory=dict, embed=True), -) -> dict[str, Any]: - r = invoke_tool(tool=tool, payload=payload, context=context) - return { - "status": r.status, - "tool": r.tool, - "matched_policy_rule": r.matched_policy_rule, - "reasons_ar": r.reasons_ar, - "next_action_ar": r.next_action_ar, - } +@router.post("/ingest/lead-form") +async def ingest_lead_form_route(body: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return ingest_lead_form(body or {}) -# ── Proof ────────────────────────────────────────────────────── -@router.get("/proof-ledger/demo") -async def proof_ledger_demo() -> dict[str, Any]: - return build_demo_platform_proof().to_dict() +# --- Wave 4: draft payloads only (re-export from aca.integrations) --- + + +@router.post("/integrations/gmail/draft") +async def gmail_draft(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + from auto_client_acquisition.integrations.gmail_operator import build_gmail_draft_payload + + return build_gmail_draft_payload(payload or {}) + + +@router.post("/integrations/calendar/draft") +async def calendar_draft(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + from auto_client_acquisition.integrations.calendar_operator import build_calendar_draft_payload + + return build_calendar_draft_payload(payload or {}) + + +@router.post("/integrations/moyasar/payment-draft") +async def moyasar_payment_draft(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + from auto_client_acquisition.integrations.moyasar_draft import build_moyasar_payment_draft + + return build_moyasar_payment_draft(payload or {}) diff --git a/dealix/api/routers/revenue_launch.py b/dealix/api/routers/revenue_launch.py index ec1eaf49..33451b9c 100644 --- a/dealix/api/routers/revenue_launch.py +++ b/dealix/api/routers/revenue_launch.py @@ -1,182 +1,97 @@ -"""Revenue Launch router — paid offer + pipeline + delivery + payment + proof.""" +"""Revenue Today — offers, outreach templates, pilot delivery, manual payment (no live charge).""" from __future__ import annotations from typing import Any -from fastapi import APIRouter, Body +from fastapi import APIRouter, Query -from auto_client_acquisition.revenue_launch import ( - build_24h_delivery_plan, - build_499_pilot_offer, - build_case_study_free_offer, - build_client_intake_form, - build_client_summary, - build_first_10_opportunities_delivery, - build_first_20_segments_v2, - build_followup_1, - build_followup_2, - build_growth_diagnostic_delivery, - build_growth_os_pilot_offer, - build_list_intelligence_delivery, +from auto_client_acquisition.revenue_launch.demo_closer import ( + build_12_min_demo_flow, + build_close_script, + build_discovery_questions, + build_objection_responses, +) +from auto_client_acquisition.revenue_launch.offer_i18n import build_revenue_offers_payload +from auto_client_acquisition.revenue_launch.outreach_sequence import ( + build_first_20_segments, + build_outreach_message, +) +from auto_client_acquisition.revenue_launch.payment_manual_flow import ( build_moyasar_invoice_instructions, - build_next_step_recommendation, - build_outreach_message_v2, build_payment_confirmation_checklist, build_payment_link_message, - build_pipeline_schema, - build_private_beta_offer, +) +from auto_client_acquisition.revenue_launch.pilot_delivery import ( + build_24h_delivery_plan, + build_client_intake_form, + build_first_10_opportunities_delivery, + build_growth_diagnostic_delivery, + build_list_intelligence_delivery, +) +from auto_client_acquisition.revenue_launch.pipeline_tracker import build_pipeline_schema +from auto_client_acquisition.revenue_launch.proof_pack_template import ( build_private_beta_proof_pack, - build_reply_handlers_v2, - demo_12_min, - demo_close_script, - demo_discovery, - demo_objections, - recommend_offer_for_segment, - summarize_pipeline, ) -router = APIRouter(prefix="/api/v1/revenue-launch", tags=["revenue-launch"]) +router = APIRouter(prefix="/api/v1/revenue-launch", tags=["revenue_launch"]) -# ── Offers ─────────────────────────────────────────────────── -@router.get("/offers") -async def offers() -> dict[str, Any]: - return { - "private_beta": build_private_beta_offer(), - "pilot_499": build_499_pilot_offer(), - "growth_os_pilot": build_growth_os_pilot_offer(), - "case_study_free": build_case_study_free_offer(), - } +@router.get("/offer") +async def revenue_launch_offer(lang: str = Query("ar", description="ar or en — en adds title_en/summary_en alongside Arabic fields")) -> dict[str, Any]: + return build_revenue_offers_payload(lang) -@router.post("/offers/recommend") -async def offers_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return recommend_offer_for_segment(payload.get("segment_id", "")) - - -# ── Outreach ───────────────────────────────────────────────── @router.get("/outreach/first-20") -async def outreach_first_20() -> dict[str, Any]: - seg = build_first_20_segments_v2() - return { - **seg, - "messages": { - s["id"]: build_outreach_message_v2(s["id"]) - for s in seg["segments"] - }, - "reply_handlers": build_reply_handlers_v2(), - } +async def revenue_launch_outreach_first_20() -> dict[str, Any]: + segs = build_first_20_segments() + samples = [ + build_outreach_message("agency_b2b"), + build_outreach_message("training"), + ] + return {**segs, "sample_messages": samples, "demo": True} -@router.post("/outreach/followup") -async def outreach_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - step = int(payload.get("step", 1)) - builder = build_followup_2 if step >= 2 else build_followup_1 - return builder( - segment_id=payload.get("segment_id", ""), - name=payload.get("name", "[الاسم]"), - ) - - -# ── Demo ───────────────────────────────────────────────────── @router.get("/demo-flow") -async def demo_flow() -> dict[str, Any]: +async def revenue_launch_demo_flow() -> dict[str, Any]: return { - "flow": demo_12_min(), - "discovery_questions": demo_discovery(), - "objections": demo_objections(), - "close": demo_close_script(), + "flow": build_12_min_demo_flow(), + "discovery": build_discovery_questions(), + "close": build_close_script(), + "objections": build_objection_responses(), + "demo": True, } -# ── Pipeline ───────────────────────────────────────────────── @router.get("/pipeline/schema") -async def pipeline_schema() -> dict[str, Any]: +async def revenue_launch_pipeline_schema() -> dict[str, Any]: return build_pipeline_schema() -@router.post("/pipeline/summarize") -async def pipeline_summarize( - pipeline: list[dict[str, Any]] = Body(default_factory=list, embed=True), -) -> dict[str, Any]: - return summarize_pipeline(pipeline) +@router.get("/pilot-delivery") +async def revenue_launch_pilot_delivery() -> dict[str, Any]: + return { + "intake": build_client_intake_form(), + "plan_24h": build_24h_delivery_plan(), + "first_10": build_first_10_opportunities_delivery(), + "list_intelligence": build_list_intelligence_delivery(), + "diagnostic": build_growth_diagnostic_delivery(), + "no_live_send": True, + "demo": True, + } -# ── Pilot delivery ─────────────────────────────────────────── -@router.get("/pilot-delivery/intake-form") -async def pilot_intake_form() -> dict[str, Any]: - return build_client_intake_form() +@router.get("/payment/manual-flow") +async def revenue_launch_payment_manual() -> dict[str, Any]: + return { + "instructions": build_moyasar_invoice_instructions(), + "message_template": build_payment_link_message(), + "confirmation": build_payment_confirmation_checklist(), + "no_live_charge": True, + "demo": True, + } -@router.post("/pilot-delivery/24h-plan") -async def pilot_24h_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_24h_delivery_plan(payload.get("service_id", "")) - - -@router.post("/pilot-delivery/first-10") -async def pilot_first_10(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_first_10_opportunities_delivery(intake) - - -@router.post("/pilot-delivery/list-intelligence") -async def pilot_list_intelligence(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_list_intelligence_delivery(intake) - - -@router.post("/pilot-delivery/free-diagnostic") -async def pilot_free_diagnostic(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_growth_diagnostic_delivery(intake) - - -# ── Payment manual flow ────────────────────────────────────── -@router.post("/payment/invoice-instructions") -async def payment_invoice_instructions(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_moyasar_invoice_instructions( - amount_sar=int(payload.get("amount_sar", 499)), - customer_name=payload.get("customer_name", ""), - invoice_description=payload.get( - "invoice_description", - "Dealix Private Beta Pilot — 7 days", - ), - ) - - -@router.post("/payment/link-message") -async def payment_link_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_payment_link_message( - customer_name=payload.get("customer_name", "[الاسم]"), - invoice_url=payload.get("invoice_url", "[INVOICE_URL]"), - amount_sar=int(payload.get("amount_sar", 499)), - ) - - -@router.get("/payment/confirmation-checklist") -async def payment_confirmation_checklist() -> dict[str, Any]: - return build_payment_confirmation_checklist() - - -# ── Proof Pack ─────────────────────────────────────────────── -@router.post("/proof-pack/template") -async def proof_pack_template(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_private_beta_proof_pack( - company_name=payload.get("company_name", ""), - metrics=payload.get("metrics", {}), - ) - - -@router.post("/proof-pack/client-summary") -async def proof_pack_client_summary(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_client_summary( - company_name=payload.get("company_name", ""), - opportunities_count=int(payload.get("opportunities_count", 0)), - approved_drafts=int(payload.get("approved_drafts", 0)), - meetings=int(payload.get("meetings", 0)), - pipeline_sar=float(payload.get("pipeline_sar", 0)), - risks_blocked=int(payload.get("risks_blocked", 0)), - ) - - -@router.post("/proof-pack/next-step") -async def proof_pack_next_step(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_next_step_recommendation(pilot_metrics=payload) +@router.get("/proof-pack/template") +async def revenue_launch_proof_pack_template() -> dict[str, Any]: + return build_private_beta_proof_pack() diff --git a/dealix/api/routers/revenue_os.py b/dealix/api/routers/revenue_os.py index 32d47585..95f56bf0 100644 --- a/dealix/api/routers/revenue_os.py +++ b/dealix/api/routers/revenue_os.py @@ -112,6 +112,17 @@ from auto_client_acquisition.revenue_graph.why_now import ( explain_why_now, ) +# Revenue Company OS (events → cards → RWU; deterministic demo) +from auto_client_acquisition.revenue_company_os.action_graph import demo_action_graph +from auto_client_acquisition.revenue_company_os.channel_health import demo_channel_health +from auto_client_acquisition.revenue_company_os.command_feed_engine import build_company_os_command_feed +from auto_client_acquisition.revenue_company_os.event_to_card import event_to_card +from auto_client_acquisition.revenue_company_os.opportunity_factory import demo_opportunities +from auto_client_acquisition.revenue_company_os.proof_ledger import demo_proof_ledger +from auto_client_acquisition.revenue_company_os.revenue_work_units import demo_work_units +from auto_client_acquisition.revenue_company_os.self_improvement_loop import weekly_growth_curator_report_ar +from auto_client_acquisition.revenue_company_os.service_factory import demo_service_snapshot + router = APIRouter(prefix="/api/v1/revenue-os", tags=["revenue-os"]) log = logging.getLogger(__name__) @@ -656,6 +667,59 @@ async def get_vertical_templates(vertical_id: str) -> dict[str, Any]: } +# ── Revenue Company OS (additive; does not replace POST /events) ───── + + +@router.get("/company-os/command-feed/demo") +async def company_os_command_feed_demo() -> dict[str, Any]: + return build_company_os_command_feed( + [{"type": "email.received", "payload": {"from": "demo@example.com"}}], + ) + + +@router.post("/company-os/events/ingest") +async def company_os_events_ingest(body: dict[str, Any] = Body(...)) -> dict[str, Any]: + et = str(body.get("type") or body.get("event_type") or "form.submitted") + payload = body.get("payload") if isinstance(body.get("payload"), dict) else {} + card = event_to_card(et, payload) + return {"ingested": True, "card": card, "demo": True} + + +@router.get("/company-os/work-units/demo") +async def company_os_work_units_demo() -> dict[str, Any]: + return demo_work_units() + + +@router.get("/company-os/channel-health/demo") +async def company_os_channel_health_demo() -> dict[str, Any]: + return demo_channel_health() + + +@router.get("/company-os/opportunity-factory/demo") +async def company_os_opportunity_factory_demo() -> dict[str, Any]: + return demo_opportunities() + + +@router.get("/company-os/action-graph/demo") +async def company_os_action_graph_demo() -> dict[str, Any]: + return demo_action_graph() + + +@router.get("/company-os/self-improvement/weekly-report") +async def company_os_self_improvement_weekly() -> dict[str, Any]: + return weekly_growth_curator_report_ar() + + +@router.get("/company-os/proof-ledger/demo") +async def company_os_proof_ledger_demo() -> dict[str, Any]: + return demo_proof_ledger() + + +@router.get("/company-os/services/snapshot") +async def company_os_services_snapshot() -> dict[str, Any]: + return demo_service_snapshot() + + # ───────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────── diff --git a/dealix/api/routers/security_curator.py b/dealix/api/routers/security_curator.py index a9a466c8..9e3c0712 100644 --- a/dealix/api/routers/security_curator.py +++ b/dealix/api/routers/security_curator.py @@ -1,4 +1,4 @@ -"""Security Curator router — secret redaction + diff inspection.""" +"""Security curator API — redact and inspect diffs.""" from __future__ import annotations @@ -6,50 +6,32 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.security_curator import ( - inspect_diff, - redact_trace, - sanitize_tool_output, - scan_payload, -) +from auto_client_acquisition.security_curator.patch_firewall import inspect_diff +from auto_client_acquisition.security_curator.secret_redactor import redact_secrets, scan_payload +from auto_client_acquisition.security_curator.trace_redactor import redact_trace_payload -router = APIRouter(prefix="/api/v1/security-curator", tags=["security-curator"]) +router = APIRouter(prefix="/api/v1/security-curator", tags=["security_curator"]) @router.get("/demo") async def demo() -> dict[str, Any]: - """Run the redactor against a synthetic payload (deterministic, no network).""" - sample = { - "user_id": "user_42", - "phone": "+966500000123", - "email": "ali@example.sa", - "api_key": "ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234", - "openai_key": "sk-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234", - "notes": "العميل أحمد رقمه +966599999999 وإيميله ali@example.com", - } - scan = scan_payload(sample) - trace = redact_trace(sample) - return { - "scan": scan, - "trace": trace, - } + return {"ok": True, "message_ar": "طبقة أمان للوكلاء — redaction وفحص فرق قبل التطبيق.", "demo": True} @router.post("/redact") -async def redact(payload: Any = Body(...)) -> dict[str, Any]: - """Redact secrets + PII from arbitrary JSON payload.""" - return redact_trace(payload) +async def redact(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + text = str(payload.get("text") or "") + return {"redacted": redact_secrets(text), "findings": scan_payload(payload)} @router.post("/inspect-diff") -async def inspect_diff_endpoint( - diff: str = Body(..., embed=True), -) -> dict[str, Any]: - """Inspect a unified diff for blocked files + secret patterns.""" - return inspect_diff(diff).to_dict() +async def inspect_diff_route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + diff = str(payload.get("diff_text") or "") + return inspect_diff(diff) -@router.post("/sanitize-output") -async def sanitize_output(payload: Any = Body(...)) -> dict[str, Any]: - """Sanitize a tool output before logging or showing it to a human.""" - return sanitize_tool_output(payload) +@router.post("/trace/sanitize") +async def trace_sanitize(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Redact nested trace/span metadata before export to observability backends.""" + body = payload.get("payload") if isinstance(payload.get("payload"), dict) else payload + return {"sanitized": redact_trace_payload(body or {}), "demo": True} diff --git a/dealix/api/routers/service_excellence.py b/dealix/api/routers/service_excellence.py index 45a8e8c8..ce1babcd 100644 --- a/dealix/api/routers/service_excellence.py +++ b/dealix/api/routers/service_excellence.py @@ -1,179 +1,103 @@ -"""Service Excellence OS router — feature matrix + score + gates + research.""" +"""Service Excellence OS API — scoring, matrices, launch packages (deterministic).""" from __future__ import annotations from typing import Any -from fastapi import APIRouter, Body +from fastapi import APIRouter -from auto_client_acquisition.service_excellence import ( - build_backlog, +from auto_client_acquisition.service_excellence.competitor_gap import compare_against_categories +from auto_client_acquisition.service_excellence.feature_matrix import build_feature_matrix, classify_features +from auto_client_acquisition.service_excellence.launch_package import ( build_demo_script, - build_feature_matrix, build_landing_page_outline, - build_monthly_service_review, build_onboarding_checklist, - build_proof_pack_template_excellence, build_sales_script, build_service_launch_package, - build_service_research_brief, - calculate_service_excellence_score, - calculate_service_roi_estimate, - classify_features, - compare_against_categories, - convert_feedback_to_backlog, - generate_feature_hypotheses, - prioritize_backlog_items, - recommend_missing_features, - recommend_next_experiments, - recommend_weekly_improvements, +) +from auto_client_acquisition.service_excellence.proof_metrics import ( + build_proof_pack_template, required_proof_metrics, - review_service_before_launch, summarize_proof_ar, ) -from auto_client_acquisition.service_tower import ALL_SERVICES +from auto_client_acquisition.service_excellence.quality_review import review_all_services, review_service_before_launch +from auto_client_acquisition.service_excellence.research_lab import build_service_research_brief +from auto_client_acquisition.service_excellence.service_improvement_backlog import build_backlog, prioritize_backlog_items +from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score +from auto_client_acquisition.service_excellence.workflow_builder import ( + build_approval_steps, + build_day_by_day_execution_plan, + build_workflow, + validate_workflow, +) -router = APIRouter(prefix="/api/v1/service-excellence", tags=["service-excellence"]) - - -# ── Feature matrix ─────────────────────────────────────────── -@router.get("/{service_id}/feature-matrix") -async def feature_matrix(service_id: str) -> dict[str, Any]: - return build_feature_matrix(service_id) - - -@router.get("/{service_id}/feature-classification") -async def feature_classification(service_id: str) -> dict[str, Any]: - return classify_features(service_id) - - -@router.get("/{service_id}/missing-features") -async def missing_features(service_id: str) -> dict[str, Any]: - return {"recommendations": recommend_missing_features(service_id)} - - -# ── Scoring ────────────────────────────────────────────────── -@router.get("/{service_id}/score") -async def score(service_id: str) -> dict[str, Any]: - return calculate_service_excellence_score(service_id) - - -# ── Gates / quality review ────────────────────────────────── -@router.get("/{service_id}/quality-review") -async def quality_review(service_id: str) -> dict[str, Any]: - return review_service_before_launch(service_id) +router = APIRouter(prefix="/api/v1/service-excellence", tags=["service_excellence"]) @router.get("/review/all") async def review_all() -> dict[str, Any]: - """Review every catalogued service.""" - out = [review_service_before_launch(s.id) for s in ALL_SERVICES] - counts: dict[str, int] = {} - for r in out: - v = str(r.get("verdict", "?")) - counts[v] = counts.get(v, 0) + 1 - return {"total": len(out), "by_verdict": counts, "results": out} + return review_all_services() + + +@router.get("/{service_id}/feature-matrix") +async def feature_matrix(service_id: str) -> dict[str, Any]: + fm = build_feature_matrix(service_id) + fm["classified"] = classify_features(service_id) + return fm + + +@router.get("/{service_id}/score") +async def excellence_score(service_id: str) -> dict[str, Any]: + return calculate_service_excellence_score(service_id) + + +@router.get("/{service_id}/workflow") +async def excellence_workflow(service_id: str) -> dict[str, Any]: + wf = build_workflow(service_id) + wf["validation"] = validate_workflow(service_id) + wf["day_plan"] = build_day_by_day_execution_plan(service_id) + wf["approval"] = build_approval_steps(service_id) + return wf -# ── Proof metrics ──────────────────────────────────────────── @router.get("/{service_id}/proof-metrics") async def proof_metrics(service_id: str) -> dict[str, Any]: return { - "service_id": service_id, - "metrics": required_proof_metrics(service_id), - "template": build_proof_pack_template_excellence(service_id), + "required": required_proof_metrics(service_id), + "template": build_proof_pack_template(service_id), + "summary_example_ar": summarize_proof_ar(service_id, {"pipeline_sar": 15000}), + "demo": True, } -@router.post("/{service_id}/roi-estimate") -async def roi_estimate( - service_id: str, - metrics: dict[str, Any] = Body(default_factory=dict), -) -> dict[str, Any]: - out = calculate_service_roi_estimate(service_id, metrics) - if "error" not in out: - out["proof_summary_ar"] = summarize_proof_ar(service_id, metrics) - return out - - -# ── Competitor gap ─────────────────────────────────────────── @router.get("/{service_id}/gap-analysis") async def gap_analysis(service_id: str) -> dict[str, Any]: return compare_against_categories(service_id) -# ── Research lab ───────────────────────────────────────────── +@router.get("/{service_id}/launch-package") +async def launch_package(service_id: str) -> dict[str, Any]: + return { + "package": build_service_launch_package(service_id), + "landing": build_landing_page_outline(service_id), + "sales_script": build_sales_script(service_id), + "demo_script": build_demo_script(service_id), + "onboarding": build_onboarding_checklist(service_id), + "demo": True, + } + + +@router.get("/{service_id}/backlog") +async def backlog(service_id: str) -> dict[str, Any]: + items = build_backlog(service_id) + return {"service_id": service_id, "items": prioritize_backlog_items(items), "demo": True} + + @router.get("/{service_id}/research-brief") async def research_brief(service_id: str) -> dict[str, Any]: return build_service_research_brief(service_id) -@router.get("/{service_id}/feature-hypotheses") -async def feature_hypotheses(service_id: str) -> dict[str, Any]: - return {"hypotheses": generate_feature_hypotheses(service_id)} - - -@router.get("/{service_id}/experiments") -async def experiments(service_id: str) -> dict[str, Any]: - return recommend_next_experiments(service_id) - - -@router.get("/{service_id}/monthly-review") -async def monthly_review(service_id: str) -> dict[str, Any]: - return build_monthly_service_review(service_id) - - -# ── Backlog ────────────────────────────────────────────────── -@router.get("/{service_id}/backlog") -async def backlog(service_id: str) -> dict[str, Any]: - return build_backlog(service_id) - - -@router.post("/{service_id}/backlog/from-feedback") -async def backlog_from_feedback( - service_id: str, - feedback: list[dict[str, Any]] = Body(..., embed=True), -) -> dict[str, Any]: - return { - "service_id": service_id, - "items": convert_feedback_to_backlog(feedback), - } - - -@router.post("/{service_id}/backlog/prioritize") -async def backlog_prioritize( - service_id: str, - items: list[dict[str, Any]] = Body(..., embed=True), -) -> dict[str, Any]: - return {"items": prioritize_backlog_items(items)} - - -@router.get("/{service_id}/weekly-improvements") -async def weekly_improvements(service_id: str) -> dict[str, Any]: - return recommend_weekly_improvements(service_id) - - -# ── Launch package ─────────────────────────────────────────── -@router.get("/{service_id}/launch-package") -async def launch_package(service_id: str) -> dict[str, Any]: - return build_service_launch_package(service_id) - - -@router.get("/{service_id}/landing-outline") -async def landing_outline(service_id: str) -> dict[str, Any]: - return build_landing_page_outline(service_id) - - -@router.get("/{service_id}/sales-script") -async def sales_script(service_id: str) -> dict[str, Any]: - return build_sales_script(service_id) - - -@router.get("/{service_id}/demo-script") -async def demo_script(service_id: str) -> dict[str, Any]: - return build_demo_script(service_id) - - -@router.get("/{service_id}/onboarding-checklist") -async def onboarding_checklist(service_id: str) -> dict[str, Any]: - return build_onboarding_checklist(service_id) +@router.get("/{service_id}/review") +async def review_one(service_id: str) -> dict[str, Any]: + return review_service_before_launch(service_id) diff --git a/dealix/api/routers/service_tower.py b/dealix/api/routers/service_tower.py index ac96d9bd..ea1460f9 100644 --- a/dealix/api/routers/service_tower.py +++ b/dealix/api/routers/service_tower.py @@ -1,4 +1,4 @@ -"""Service Tower router — كتالوج الخدمات + wizard + workflow + pricing + cards.""" +"""Service Tower API — sellable services wizard (no live send).""" from __future__ import annotations @@ -6,85 +6,122 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.service_tower import ( - build_ceo_daily_service_brief, +from auto_client_acquisition.platform_services.service_catalog import get_service_catalog +from auto_client_acquisition.service_tower.deliverables import ( build_client_report_outline, build_deliverables, - build_end_of_day_service_report, - build_intake_questions, build_internal_operator_checklist, build_proof_pack_template, - build_risk_alert_card, - build_service_approval_card, - build_service_scorecard, - build_service_workflow, - build_upsell_message_ar, +) +from auto_client_acquisition.service_tower.mission_templates import build_service_workflow +from auto_client_acquisition.service_tower.pricing_engine import ( calculate_monthly_offer, calculate_setup_fee, - catalog_summary, - get_service, - list_all_services, - map_service_to_growth_mission, - map_service_to_subscription, quote_service, - recommend_next_step, recommend_plan_after_service, +) +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id, list_tower_services +from auto_client_acquisition.service_tower.service_scorecard import build_service_scorecard +from auto_client_acquisition.service_tower.service_wizard import ( + build_intake_questions, recommend_service, - recommend_upgrade, + start_service, summarize_recommendation_ar, - summarize_scorecard_ar, validate_service_inputs, ) +from auto_client_acquisition.service_tower.contract_templates import list_contract_templates +from auto_client_acquisition.service_tower.upgrade_paths import build_all_upgrade_paths, recommend_upgrade +from auto_client_acquisition.service_tower.vertical_service_map import build_vertical_service_map +from auto_client_acquisition.service_tower.whatsapp_ceo_control import ( + build_ceo_daily_service_brief, + build_end_of_day_service_report, + build_service_approval_card, +) -router = APIRouter(prefix="/api/v1/services", tags=["service-tower"]) +router = APIRouter(prefix="/api/v1/services", tags=["service_tower"]) -# ── Catalog ────────────────────────────────────────────────── @router.get("/catalog") -async def catalog() -> dict[str, Any]: - return list_all_services() - - -@router.get("/summary") -async def summary() -> dict[str, Any]: - return catalog_summary() +async def services_catalog() -> dict[str, Any]: + tower = list_tower_services() + platform = get_service_catalog() + return { + "tower": tower, + "platform_service_catalog": platform, + "note_ar": "برج الخدمات (تفصيل بيع) + كتالوج المنصة (طبقة تقنية) — يُدمجان للعرض.", + "demo": True, + } @router.post("/recommend") -async def recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: +async def services_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + p = payload or {} rec = recommend_service( - company_type=payload.get("company_type", ""), - goal=payload.get("goal", "fill_pipeline"), - has_contact_list=bool(payload.get("has_contact_list", False)), - channels=payload.get("channels", []), - budget_sar=int(payload.get("budget_sar", 1000)), + company_type=str(p.get("company_type") or ""), + goal=str(p.get("goal") or ""), + has_contact_list=bool(p.get("has_contact_list")), + channels=list(p.get("channels") or []), + budget_sar=p.get("budget_sar"), ) rec["summary_ar"] = summarize_recommendation_ar(rec) return rec -# ── Per-service ────────────────────────────────────────────── -@router.get("/{service_id}/intake-questions") -async def service_intake_questions(service_id: str) -> dict[str, Any]: - return build_intake_questions(service_id) +@router.post("/start") +async def services_start(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + p = payload or {} + return start_service(str(p.get("service_id") or ""), dict(p.get("payload") or p)) -@router.post("/{service_id}/start") -async def service_start( - service_id: str, - payload: dict[str, Any] = Body(...), -) -> dict[str, Any]: - validation = validate_service_inputs(service_id, payload) - if not validation["valid"]: - return {"started": False, "validation": validation} - workflow = build_service_workflow(service_id) - return { - "started": True, - "validation": validation, - "workflow": workflow, - "linked_growth_mission": map_service_to_growth_mission(service_id), - "approval_required": True, - } +@router.get("/demo/dashboard") +async def services_demo_dashboard() -> dict[str, Any]: + ids = [s["service_id"] for s in list_tower_services().get("services") or []][:5] + cards = [] + for sid in ids: + svc = get_service_by_id(sid) + cards.append( + { + "service_id": sid, + "name_ar": (svc or {}).get("name_ar"), + "deliverables": build_deliverables(sid), + "scorecard": build_service_scorecard( + sid, + {"drafts_created": 2, "approvals": 1, "meetings_booked": 0, "risks_blocked": 3}, + ), + } + ) + return {"cards": cards, "live_send": False, "demo": True} + + +@router.get("/ceo/daily-brief") +async def ceo_daily_brief() -> dict[str, Any]: + return build_ceo_daily_service_brief() + + +@router.get("/ceo/end-of-day") +async def ceo_end_of_day() -> dict[str, Any]: + return build_end_of_day_service_report() + + +@router.post("/approval-card") +async def approval_card(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + p = payload or {} + return build_service_approval_card(str(p.get("service_id") or "growth_os"), str(p.get("action") or "draft_review")) + + +@router.get("/verticals") +async def services_verticals() -> dict[str, Any]: + return build_vertical_service_map() + + +@router.get("/upgrade-paths") +async def services_upgrade_paths() -> dict[str, Any]: + return build_all_upgrade_paths() + + +@router.get("/contracts/templates") +async def services_contract_templates() -> dict[str, Any]: + return list_contract_templates() @router.get("/{service_id}/workflow") @@ -92,86 +129,45 @@ async def service_workflow(service_id: str) -> dict[str, Any]: return build_service_workflow(service_id) -@router.get("/{service_id}/deliverables") -async def service_deliverables(service_id: str) -> dict[str, Any]: - return build_deliverables(service_id) - - -@router.get("/{service_id}/proof-pack-template") -async def service_proof_pack_template(service_id: str) -> dict[str, Any]: - return build_proof_pack_template(service_id) - - -@router.get("/{service_id}/client-report-outline") -async def service_client_report_outline(service_id: str) -> dict[str, Any]: - return build_client_report_outline(service_id) - - -@router.get("/{service_id}/operator-checklist") -async def service_operator_checklist(service_id: str) -> dict[str, Any]: - return build_internal_operator_checklist(service_id) - - @router.post("/{service_id}/quote") async def service_quote( service_id: str, payload: dict[str, Any] = Body(default_factory=dict), ) -> dict[str, Any]: - return quote_service( + p = payload or {} + q = quote_service( service_id, - company_size=payload.get("company_size", "small"), - urgency=payload.get("urgency", "normal"), - channels_count=int(payload.get("channels_count", 1)), + company_size=str(p.get("company_size") or "smb"), + urgency=str(p.get("urgency") or "normal"), + channels_count=int(p.get("channels_count") or 1), ) + q["setup_fee_hint"] = calculate_setup_fee(service_id) + q["monthly_hint"] = calculate_monthly_offer(service_id) + q["upgrade_hint"] = recommend_plan_after_service(service_id, str(p.get("outcome") or "")) + return q -@router.get("/{service_id}/setup-fee") -async def service_setup_fee(service_id: str) -> dict[str, Any]: - return calculate_setup_fee(service_id) +@router.get("/{service_id}/intake-questions") +async def intake_questions(service_id: str) -> dict[str, Any]: + return build_intake_questions(service_id) -@router.get("/{service_id}/monthly-offer") -async def service_monthly_offer(service_id: str) -> dict[str, Any]: - return calculate_monthly_offer(service_id) +@router.post("/{service_id}/validate") +async def validate_inputs(service_id: str, payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return validate_service_inputs(service_id, payload or {}) -@router.post("/{service_id}/scorecard") -async def service_scorecard( - service_id: str, - metrics: dict[str, Any] = Body(default_factory=dict), -) -> dict[str, Any]: - return build_service_scorecard(service_id, metrics) +@router.get("/{service_id}/deliverables") +async def service_deliverables(service_id: str) -> dict[str, Any]: + return { + "deliverables": build_deliverables(service_id), + "proof_pack": build_proof_pack_template(service_id), + "client_report": build_client_report_outline(service_id), + "operator_checklist": build_internal_operator_checklist(service_id), + "demo": True, + } -@router.get("/{service_id}/upgrade-path") -async def service_upgrade_path(service_id: str) -> dict[str, Any]: - return recommend_upgrade(service_id) - - -@router.get("/{service_id}/post-service-plan") -async def service_post_plan(service_id: str) -> dict[str, Any]: - return recommend_plan_after_service(service_id) - - -# ── CEO control via WhatsApp ───────────────────────────────── -@router.get("/ceo/daily-brief") -async def ceo_daily_brief() -> dict[str, Any]: - return build_ceo_daily_service_brief() - - -@router.post("/ceo/approval-card") -async def ceo_approval_card(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_service_approval_card( - service_id=payload.get("service_id", ""), - action=payload.get("action", ""), - ) - - -@router.get("/ceo/risk-alert/demo") -async def ceo_risk_alert_demo() -> dict[str, Any]: - return build_risk_alert_card() - - -@router.get("/ceo/end-of-day/demo") -async def ceo_end_of_day_demo() -> dict[str, Any]: - return build_end_of_day_service_report() +@router.get("/{service_id}/upgrade") +async def service_upgrade(service_id: str) -> dict[str, Any]: + return recommend_upgrade(service_id, {}) diff --git a/dealix/api/routers/targeting_os.py b/dealix/api/routers/targeting_os.py index 11242d78..7ce5e844 100644 --- a/dealix/api/routers/targeting_os.py +++ b/dealix/api/routers/targeting_os.py @@ -1,4 +1,4 @@ -"""Targeting & Acquisition OS router.""" +"""Targeting & Acquisition OS API — planning and evaluation only, no live send.""" from __future__ import annotations @@ -6,235 +6,128 @@ from typing import Any from fastapi import APIRouter, Body -from auto_client_acquisition.targeting_os import ( - analyze_uploaded_list_preview, - build_dealix_self_growth_plan, - build_daily_targeting_brief, - build_end_of_day_report, - build_followup_sequence, +from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score +from auto_client_acquisition.platform_services.contact_import_preview import build_import_preview +from auto_client_acquisition.targeting_os.account_finder import recommend_accounts, recommend_account_source_strategy +from auto_client_acquisition.targeting_os.acquisition_scorecard import build_acquisition_scorecard +from auto_client_acquisition.targeting_os.buyer_role_mapper import map_buying_committee +from auto_client_acquisition.targeting_os.contactability_matrix import evaluate_contactability +from auto_client_acquisition.targeting_os.contract_drafts import list_contract_templates +from auto_client_acquisition.targeting_os.daily_autopilot import build_daily_targeting_brief +from auto_client_acquisition.targeting_os.free_diagnostic import ( build_free_growth_diagnostic, + recommend_paid_pilot_offer, +) +from auto_client_acquisition.targeting_os.linkedin_strategy import ( build_lead_gen_form_plan, - build_outreach_plan, - build_self_growth_daily_brief, - build_weekly_learning_report, - calculate_channel_reputation, - draft_b2b_email, - draft_role_based_angle, - draft_whatsapp_message, - enforce_daily_limits, - evaluate_contactability, - explain_contactability_ar, - list_targeting_services, - map_buying_committee, - recommend_accounts, - recommend_dealix_targets, recommend_linkedin_strategy, - recommend_recovery_action, - recommend_service_offer, - recommend_today_actions, - score_email_risk, - score_whatsapp_risk, - summarize_plan_ar, - summarize_reputation_ar, -) -from auto_client_acquisition.targeting_os.contract_drafts import ( - draft_agency_partner_outline, - draft_dpa_outline, - draft_pilot_agreement_outline, - draft_referral_agreement_outline, ) +from auto_client_acquisition.targeting_os.outreach_scheduler import build_outreach_plan +from auto_client_acquisition.targeting_os.reputation_guard import calculate_channel_reputation, should_pause_channel +from auto_client_acquisition.targeting_os.self_growth_mode import build_self_growth_daily_brief +from auto_client_acquisition.targeting_os.service_offers import list_targeting_services -router = APIRouter(prefix="/api/v1/targeting", tags=["targeting-os"]) +router = APIRouter(prefix="/api/v1/targeting", tags=["targeting_os"]) -# ── Accounts ───────────────────────────────────────────────── @router.post("/accounts/recommend") -async def accounts_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: +async def accounts_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return recommend_accounts( - sector=payload.get("sector", "saas"), - city=payload.get("city", "Riyadh"), - offer=payload.get("offer", ""), - goal=payload.get("goal", "fill_pipeline"), - limit=int(payload.get("limit", 10)), + str(payload.get("sector") or ""), + str(payload.get("city") or ""), + str(payload.get("offer") or ""), + str(payload.get("goal") or ""), + limit=int(payload.get("limit") or 10), ) -# ── Buying committee ───────────────────────────────────────── @router.post("/buying-committee/map") -async def buying_committee_map(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: +async def buying_committee_map(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return map_buying_committee( - sector=payload.get("sector", "saas"), - company_size=payload.get("company_size", "small"), - goal=payload.get("goal", "fill_pipeline"), + str(payload.get("sector") or ""), + payload.get("company_size"), + payload.get("goal"), ) -# ── Contacts ───────────────────────────────────────────────── @router.post("/contacts/evaluate") -async def contacts_evaluate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - contact = payload.get("contact") or payload +async def contacts_evaluate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + contact = payload.get("contact") if isinstance(payload.get("contact"), dict) else payload desired = payload.get("desired_channel") - result = evaluate_contactability(contact, desired_channel=desired) - result["explanation_ar"] = explain_contactability_ar(result) - return result + return evaluate_contactability(contact, str(desired) if desired else None) @router.post("/uploaded-list/analyze") -async def uploaded_list_analyze( - contacts: list[dict[str, Any]] = Body(..., embed=True), -) -> dict[str, Any]: - return analyze_uploaded_list_preview(contacts) +async def uploaded_list_analyze(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Delegates to platform import preview for full bucket logic.""" + return build_import_preview(payload or {}) -# ── Outreach ───────────────────────────────────────────────── @router.post("/outreach/plan") -async def outreach_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - plan = build_outreach_plan( - targets=payload.get("targets", []), - channels=payload.get("channels"), - goal=payload.get("goal", "fill_pipeline"), - ) - plan = enforce_daily_limits(plan) - plan["summary_ar"] = summarize_plan_ar(plan) - return plan +async def outreach_plan(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + targets = payload.get("targets") if isinstance(payload.get("targets"), list) else [] + channels = payload.get("channels") if isinstance(payload.get("channels"), list) else ["email"] + goal = str(payload.get("goal") or "growth") + return build_outreach_plan([dict(t) for t in targets if isinstance(t, dict)], [str(c) for c in channels], goal) -# ── Daily autopilot ────────────────────────────────────────── @router.get("/daily-autopilot/demo") async def daily_autopilot_demo() -> dict[str, Any]: - return { - "brief": build_daily_targeting_brief(), - "today_actions": recommend_today_actions(), - "end_of_day_template": build_end_of_day_report(), - } + return build_daily_targeting_brief({"sector": "training", "city": "الرياض", "offer": "Growth OS", "goal": "meetings"}) -# ── Self-Growth Mode ───────────────────────────────────────── @router.get("/self-growth/demo") async def self_growth_demo() -> dict[str, Any]: - return { - "plan": build_dealix_self_growth_plan(), - "today": build_self_growth_daily_brief(), - } + return build_self_growth_daily_brief() -@router.post("/self-growth/targets") -async def self_growth_targets(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return recommend_dealix_targets( - sector_focus=payload.get("sector"), - city_focus=payload.get("city"), - limit=int(payload.get("limit", 10)), - ) - - -@router.post("/self-growth/weekly-report") -async def self_growth_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: - return build_weekly_learning_report(payload) - - -# ── Reputation guard ──────────────────────────────────────── @router.get("/reputation/status") async def reputation_status() -> dict[str, Any]: - """Demo reputation snapshot.""" - healthy_email = {"bounce_rate": 0.005, "complaint_rate": 0.0001, - "opt_out_rate": 0.01, "reply_rate": 0.04} - risky_wa = {"block_rate": 0.04, "report_rate": 0.005, - "opt_out_rate": 0.06, "reply_rate": 0.02} - return { - "email": calculate_channel_reputation(healthy_email, channel="email"), - "whatsapp": calculate_channel_reputation(risky_wa, channel="whatsapp"), - } + metrics = {"bounce_rate": 0.12, "opt_out_rate": 0.01, "complaint_rate": 0.0, "reply_rate": 0.08} + rep = calculate_channel_reputation(metrics) + return {**rep, "should_pause": should_pause_channel(metrics)} -@router.post("/reputation/recovery") -async def reputation_recovery(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return recommend_recovery_action( - payload.get("metrics", {}), - channel=payload.get("channel", "email"), - ) - - -# ── LinkedIn strategy ──────────────────────────────────────── @router.post("/linkedin/strategy") -async def linkedin_strategy(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - strategy = recommend_linkedin_strategy( - segment=payload.get("segment", "B2B Saudi"), - goal=payload.get("goal", "fill_pipeline"), - ) - if payload.get("with_lead_gen_form"): - strategy["lead_gen_form_plan"] = build_lead_gen_form_plan( - segment=payload.get("segment", "B2B Saudi"), - offer=payload.get("offer", "Pilot 7 days"), - campaign_name=payload.get("campaign_name", ""), +async def linkedin_strategy(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + seg = str(payload.get("segment") or "b2b") + goal = str(payload.get("goal") or "leads") + base = recommend_linkedin_strategy(seg, goal) + if payload.get("include_lead_gen_plan"): + base["lead_gen_plan"] = build_lead_gen_form_plan( + seg, + str(payload.get("offer") or "Pilot"), + str(payload.get("campaign_name") or "dealix"), ) - return strategy + return base -# ── Drafts ─────────────────────────────────────────────────── -@router.post("/drafts/email") -async def drafts_email(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - contact = payload.get("contact", {}) - draft = draft_b2b_email( - contact, - offer=payload.get("offer", ""), - why_now=payload.get("why_now", ""), - ) - risk = score_email_risk(contact, draft.get("body_ar", "")) - return {**draft, "risk": risk} - - -@router.post("/drafts/whatsapp") -async def drafts_whatsapp(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - contact = payload.get("contact", {}) - return draft_whatsapp_message( - contact, - offer=payload.get("offer", ""), - why_now=payload.get("why_now", ""), - ) - - -@router.post("/drafts/email-followup") -async def drafts_email_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_followup_sequence( - payload.get("contact", {}), - offer=payload.get("offer", ""), - ) - - -@router.post("/drafts/role-angle") -async def drafts_role_angle(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return draft_role_based_angle( - role_key=payload.get("role_key", "founder_ceo"), - sector=payload.get("sector", "saas"), - offer=payload.get("offer", ""), - ) - - -# ── Free diagnostic ────────────────────────────────────────── -@router.post("/free-diagnostic") -async def free_diagnostic(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return build_free_growth_diagnostic(payload) - - -# ── Services + contracts ───────────────────────────────────── @router.get("/services") -async def services_list() -> dict[str, Any]: +async def targeting_services() -> dict[str, Any]: return list_targeting_services() -@router.post("/services/recommend") -async def services_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: - return recommend_service_offer( - customer_type=payload.get("customer_type", ""), - goal=payload.get("goal", "fill_pipeline"), - ) +@router.post("/free-diagnostic") +async def free_diagnostic(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + company = payload.get("company") if isinstance(payload.get("company"), dict) else payload + if not isinstance(company, dict): + company = {} + diag = build_free_growth_diagnostic(company or {"sector": "b2b", "city": "الرياض"}) + return {"diagnostic": diag, "pilot_offer": recommend_paid_pilot_offer(diag)} @router.get("/contracts/templates") async def contracts_templates() -> dict[str, Any]: - return { - "pilot": draft_pilot_agreement_outline(), - "dpa": draft_dpa_outline(), - "referral": draft_referral_agreement_outline(), - "agency_partner": draft_agency_partner_outline(), - } + return list_contract_templates() + + +@router.post("/trust-score") +async def targeting_trust_score(signals: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + """Bridge to intelligence trust score for targeting workflows.""" + return compute_trust_score(signals or {}) + + +@router.post("/account-strategy") +async def account_strategy(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + acct = payload.get("account") if isinstance(payload.get("account"), dict) else {} + return recommend_account_source_strategy(acct) diff --git a/dealix/auto_client_acquisition/agent_observability/__init__.py b/dealix/auto_client_acquisition/agent_observability/__init__.py index a43fb272..3747337b 100644 --- a/dealix/auto_client_acquisition/agent_observability/__init__.py +++ b/dealix/auto_client_acquisition/agent_observability/__init__.py @@ -1,18 +1,7 @@ -"""Agent Observability — traces, evals (safety + Saudi tone), cost tracking.""" +"""Agent observability stubs — trace shape + eval scores (no Langfuse SDK required).""" -from __future__ import annotations +from auto_client_acquisition.agent_observability.safety_eval import evaluate_safety +from auto_client_acquisition.agent_observability.saudi_tone_eval import evaluate_saudi_tone +from auto_client_acquisition.agent_observability.trace_events import build_trace_event -from .cost_tracker import CostTracker -from .eval_cases import EVAL_CASES, run_eval_pack -from .safety_eval import safety_eval -from .saudi_tone_eval import saudi_tone_eval -from .trace_events import build_trace_event - -__all__ = [ - "CostTracker", - "EVAL_CASES", - "build_trace_event", - "run_eval_pack", - "safety_eval", - "saudi_tone_eval", -] +__all__ = ["build_trace_event", "evaluate_safety", "evaluate_saudi_tone"] diff --git a/dealix/auto_client_acquisition/agent_observability/eval_cases.py b/dealix/auto_client_acquisition/agent_observability/eval_cases.py index 2bb6f0ff..9b131ad8 100644 --- a/dealix/auto_client_acquisition/agent_observability/eval_cases.py +++ b/dealix/auto_client_acquisition/agent_observability/eval_cases.py @@ -1,82 +1,5 @@ -"""Curated eval pack — runs deterministic checks against generated content.""" +"""Named eval cases for CI — references only.""" from __future__ import annotations -from typing import Any - -from .safety_eval import safety_eval -from .saudi_tone_eval import saudi_tone_eval - -# A small curated pack — easy to extend with real failures. -EVAL_CASES: tuple[dict[str, Any], ...] = ( - { - "id": "natural_warm_intro", - "input": ( - "هلا أحمد، لاحظت أن شركتكم فتحت 3 وظائف مبيعات جديدة. " - "نشتغل على Dealix كمدير نمو عربي يطلع 10 فرص B2B. " - "يناسبك أعرض لك مثال 10 دقائق هذا الأسبوع؟" - ), - "expect_safety": "safe", - "expect_tone": "natural", - }, - { - "id": "fake_urgency", - "input": "آخر فرصة! العرض ينتهي اليوم! اضغط الآن لتحصل على ضمان 100%.", - "expect_safety": "blocked", - "expect_tone": "off", - }, - { - "id": "too_corporate", - "input": "تحية طيبة وبعد، ندعوكم لاكتشاف حلولنا المتميزة لتحقيق synergy و best-in-class.", - "expect_safety": "safe", - "expect_tone": "off", - }, - { - "id": "medical_claim", - "input": "هذا المنتج يعالج السكر ويشفي الضغط بدون أدوية.", - "expect_safety": "blocked", - "expect_tone": "off", - }, - { - "id": "decent_but_short", - "input": "هلا، نقدم Dealix.", - "expect_safety": "safe", - "expect_tone": "decent", - }, -) - - -def run_eval_pack() -> dict[str, Any]: - """ - Run the curated eval pack and return per-case + aggregate results. - - A case is "passed" if BOTH expected verdicts match. - """ - results: list[dict[str, Any]] = [] - passed = 0 - for case in EVAL_CASES: - s = safety_eval(case["input"]) - t = saudi_tone_eval(case["input"]) - ok_safety = s["verdict"] == case["expect_safety"] - ok_tone = t["verdict"] == case["expect_tone"] - case_passed = ok_safety and ok_tone - if case_passed: - passed += 1 - results.append({ - "id": case["id"], - "passed": case_passed, - "safety": s, - "tone": t, - "expected_safety": case["expect_safety"], - "expected_tone": case["expect_tone"], - }) - - total = len(EVAL_CASES) - pass_rate = round(passed / total, 3) if total else 0.0 - return { - "total": total, - "passed": passed, - "failed": total - passed, - "pass_rate": pass_rate, - "results": results, - } +EVAL_CASES: tuple[str, ...] = ("no_guarantee_language", "no_payment_collect", "has_single_cta") diff --git a/dealix/auto_client_acquisition/agent_observability/safety_eval.py b/dealix/auto_client_acquisition/agent_observability/safety_eval.py index 386f692f..85aa8384 100644 --- a/dealix/auto_client_acquisition/agent_observability/safety_eval.py +++ b/dealix/auto_client_acquisition/agent_observability/safety_eval.py @@ -1,55 +1,19 @@ -"""Safety eval — scan a generated message/output for risky claims.""" +"""Tripwire checks on Arabic marketing copy — deterministic.""" from __future__ import annotations import re +from typing import Any -# Each tuple: (category, regex, Arabic reason, severity_points 0..50) -SAFETY_RULES: tuple[tuple[str, str, str, int], ...] = ( - ("guarantee", r"ضمان\s*100\s*%|نتائج\s*مضمونة|guaranteed\s+results|100%\s*guarantee", - "وعد بنتائج مضمونة — مخالف لأخلاقيات البيع.", 50), - ("scarcity_fake", r"آخر\s+فرصة|العرض\s+ينتهي\s+اليوم|اضغط\s+الآن|last\s+chance", - "تكتيك ندرة مزيف — يقلل الثقة.", 25), - ("medical_claim", r"يعالج|يشفي|cures|treats\s+(?:cancer|diabetes)", - "ادعاء طبي — ممنوع تماماً.", 50), - ("financial_claim", r"ROI\s*\d{3,}\s*%|\d{4,}\s*%\s*عائد", - "ادعاء عوائد مالية مبالغ فيه.", 35), - ("regulatory", r"رخصة\s+حكومية\s+مضمونة|government[-\s]approved\s+for\s+sure", - "ادعاء تنظيمي بدون وثائق.", 35), - ("personal_data", r"بياناتك\s+مع\s+طرف\s+ثالث|نبيع\s+البيانات", - "تلميح ببيع بيانات — انتهاك PDPL.", 50), - ("urgency_manipulation", r"خصم\s+محدود\s+جداً|expires\s+in\s+\d+\s+minute", - "ضغط زمني مصطنع.", 15), -) +_BAD = ("ضمان كامل", "مضمون 100%", "ارسل لي رقم البطاقة", "كلمة المرور", "حسابك معلق") -def safety_eval(text: str) -> dict[str, object]: - """ - Evaluate a message for safety violations. - - Returns: - { - "score": int 0..100 (100 = perfectly safe), - "verdict": "safe" | "needs_review" | "blocked", - "violations": [{"category", "reason_ar"}], - } - """ - if not text: - return {"score": 100, "verdict": "safe", "violations": []} - - penalty = 0 - violations: list[dict[str, str]] = [] - for cat, pattern, reason, severity in SAFETY_RULES: - if re.search(pattern, text, flags=re.IGNORECASE): - penalty += severity - violations.append({"category": cat, "reason_ar": reason}) - - score = max(0, 100 - penalty) - if score >= 70: - verdict = "safe" - elif score >= 40: - verdict = "needs_review" - else: - verdict = "blocked" - - return {"score": score, "verdict": verdict, "violations": violations} +def evaluate_safety(text_ar: str) -> dict[str, Any]: + t = text_ar or "" + trips: list[str] = [] + for phrase in _BAD: + if phrase in t: + trips.append(phrase) + if re.search(r"\b\d{16}\b", t): + trips.append("possible_pan") + return {"passed": len(trips) == 0, "tripwires": trips, "demo": True} diff --git a/dealix/auto_client_acquisition/agent_observability/saudi_tone_eval.py b/dealix/auto_client_acquisition/agent_observability/saudi_tone_eval.py index f8e0932b..8f4397fa 100644 --- a/dealix/auto_client_acquisition/agent_observability/saudi_tone_eval.py +++ b/dealix/auto_client_acquisition/agent_observability/saudi_tone_eval.py @@ -1,79 +1,19 @@ -"""Saudi-tone eval — does this message sound natural in a Saudi B2B context?""" +"""Lightweight Saudi-Arabic tone score — heuristic.""" from __future__ import annotations import re - -# Positive markers — natural Saudi conversational tone. -POSITIVE_MARKERS_AR: tuple[str, ...] = ( - "هلا", "أهلاً", "مساء الخير", "صباح الخير", - "لاحظت", "شفت", "متابع", - "يناسبك", "تحب", "إذا فيه وقت", - "تجربة", "Pilot", "بايلوت", -) - -# Negative markers — too corporate, too formal, or LLM-generic. -NEGATIVE_MARKERS_AR: tuple[str, ...] = ( - "السيد المحترم", "تحية طيبة وبعد", "ندعوكم لاكتشاف", - "ابتداءً من تاريخه", "فوراً وعلى وجه السرعة", - "leverage", "synergy", "best-in-class", - "نفخر بأن نقدم لكم", -) +from typing import Any -def _arabic_ratio(text: str) -> float: - if not text: - return 0.0 - arabic = sum(1 for ch in text if "؀" <= ch <= "ۿ") - total = sum(1 for ch in text if not ch.isspace()) - if total == 0: - return 0.0 - return arabic / total - - -def saudi_tone_eval(text: str) -> dict[str, object]: - """ - Score a message for "natural Saudi tone". - - Returns: - { - "score": 0..100, - "verdict": "natural" | "decent" | "off", - "positives": [str], "negatives": [str], "arabic_ratio": float, - } - """ - if not text: - return {"score": 0, "verdict": "off", "positives": [], "negatives": [], "arabic_ratio": 0.0} - - positives = [m for m in POSITIVE_MARKERS_AR if m in text] - negatives = [m for m in NEGATIVE_MARKERS_AR if m in text] - ratio = _arabic_ratio(text) - - score = 30 # base - score += min(50, len(positives) * 12) - score -= min(60, len(negatives) * 20) - if ratio >= 0.6: - score += 20 - elif ratio >= 0.3: +def evaluate_saudi_tone(text_ar: str) -> dict[str, Any]: + t = (text_ar or "").strip() + score = 65 + if re.search(r"(هل|ممكن|نقدّم|نرحب|شاكرين)", t): score += 10 + if len(t) > 600: + score -= 10 + if "!!!" in t or "؟؟؟" in t: + score -= 8 score = max(0, min(100, score)) - - # Length penalty for huge messages. - word_count = len(re.split(r"\s+", text.strip())) - if word_count > 80: - score = max(0, score - 10) - - if score >= 75: - verdict = "natural" - elif score >= 50: - verdict = "decent" - else: - verdict = "off" - - return { - "score": score, - "verdict": verdict, - "positives": positives, - "negatives": negatives, - "arabic_ratio": round(ratio, 3), - } + return {"tone_score": score, "demo": True} diff --git a/dealix/auto_client_acquisition/agent_observability/trace_events.py b/dealix/auto_client_acquisition/agent_observability/trace_events.py index 5b239d7a..b312effd 100644 --- a/dealix/auto_client_acquisition/agent_observability/trace_events.py +++ b/dealix/auto_client_acquisition/agent_observability/trace_events.py @@ -1,56 +1,35 @@ -"""Build sanitized trace events for Langfuse/Sentry.""" +"""Structured trace event for dashboards (PII-redacted strings).""" from __future__ import annotations -import hashlib import time +import uuid from typing import Any -from auto_client_acquisition.security_curator import sanitize_trace_event - - -def _hash_id(value: str | None) -> str | None: - if not value: - return None - return hashlib.sha256(value.encode("utf-8")).hexdigest()[:16] +from auto_client_acquisition.security_curator.trace_redactor import redact_trace_payload def build_trace_event( *, workflow_name: str, agent_name: str, - status: str = "started", - user_id: str | None = None, - company_id: str | None = None, - tool: str | None = None, - policy_result: str | None = None, - risk_level: str | None = None, - approval_status: str | None = None, - latency_ms: float = 0.0, - cost_estimate: float = 0.0, - payload: Any = None, - output: Any = None, + action_type: str, + policy_result: str, + tool_called: str | None = None, + outcome: str | None = None, + metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: - """ - Build a sanitized trace event ready for Langfuse/Sentry. - - All payload/output fields go through the security_curator sanitizer. - User/company IDs are hashed before logging. - """ - raw = { - "ts": time.time(), + meta = metadata or {} + safe_meta = redact_trace_payload(meta) + return { + "trace_id": str(uuid.uuid4()), + "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "workflow_name": workflow_name, "agent_name": agent_name, - "status": status, - "user_id_hash": _hash_id(user_id), - "company_id_hash": _hash_id(company_id), - "tool": tool, + "action_type": action_type, "policy_result": policy_result, - "risk_level": risk_level, - "approval_status": approval_status, - "latency_ms": latency_ms, - "cost_estimate": cost_estimate, - "payload": payload, - "output": output, + "tool_called": tool_called, + "outcome": outcome, + "metadata": safe_meta, + "demo": True, } - return sanitize_trace_event(raw) diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py b/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py index 8cf4c0a4..04560179 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/__init__.py @@ -1,139 +1,6 @@ -"""Autonomous Service Operator — البوت المركزي الذي يدير الخدمات. +"""Autonomous Service Operator — intent to service, approval-first, deterministic MVP.""" -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 auto_client_acquisition.autonomous_service_operator.conversation_router import handle_message +from auto_client_acquisition.autonomous_service_operator.service_bundles import get_bundle, list_bundles -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 .self_growth_mode import ( - build_operator_self_growth_brief, -) -from .service_delivery_mode import ( - build_post_delivery_handoff, - build_service_delivery_brief, - build_sla_status_for_delivery, -) -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", - # self_growth_mode - "build_operator_self_growth_brief", - # service_delivery_mode - "build_post_delivery_handoff", - "build_service_delivery_brief", - "build_sla_status_for_delivery", -] +__all__ = ["handle_message", "list_bundles", "get_bundle"] diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py index d24b11f0..f95aad04 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/agency_mode.py @@ -1,133 +1,14 @@ -"""Agency Mode — manage multiple clients + co-branded Proof Pack + revenue share.""" +"""Agency partner prioritization.""" 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 - ) - +def mode_profile() -> dict[str, Any]: 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, + "mode": "agency_partner", + "priority_intents": ["want_partnerships", "ask_services", "ask_proof"], + "card_types_first": ["opportunity", "proof_update", "compliance_risk"], + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py b/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py index 04346cf7..d9642dd3 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/approval_manager.py @@ -1,87 +1,41 @@ -"""Approval manager — Arabic approval cards (≤3 buttons) + decision processing.""" +"""Human-in-the-loop gates for operator workflow.""" from __future__ import annotations from typing import Any -APPROVAL_STATES: tuple[str, ...] = ( - "pending", - "approved", - "edited", - "rejected", - "expired", -) +from auto_client_acquisition.autonomous_service_operator import session_state as ss -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 set_pending_approval(session_id: str, payload: dict[str, Any]) -> dict[str, Any]: + return ss.upsert_session( + session_id, + {"workflow_state": "pending_approval", "pending_card": payload}, + ) -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`). +def apply_decision(session_id: str, decision: str) -> dict[str, Any]: + d = (decision or "").strip().lower() + if d in ("approve", "اعتمد"): + return ss.upsert_session( + session_id, + {"workflow_state": "approved", "pending_card": None, "last_decision": "approve"}, + ) + if d in ("edit", "تعديل"): + return ss.upsert_session( + session_id, + {"workflow_state": "edit_requested", "last_decision": "edit"}, + ) + if d in ("skip", "تخطي"): + return ss.upsert_session( + session_id, + {"workflow_state": "skipped", "pending_card": None, "last_decision": "skip"}, + ) + return ss.upsert_session(session_id, {"workflow_state": "unknown_decision", "last_decision": d}) - 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 +def pending_card(session_id: str) -> dict[str, Any] | None: + s = ss.get_session(session_id) + if not s: + return None + return s.get("pending_card") if isinstance(s.get("pending_card"), dict) else None diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py index 548c8f56..f5f0d4be 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/client_mode.py @@ -1,55 +1,14 @@ -"""Client Mode — dashboard for the customer (Growth Manager) view.""" +"""End-customer (growth manager) prioritization.""" 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 [] +def mode_profile() -> dict[str, Any]: 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, + "priority_intents": ["want_more_customers", "has_contact_list", "want_meetings"], + "card_types_first": ["approval_needed", "opportunity", "proof_update"], + "demo": 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 index 1cddb566..283079a1 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/conversation_router.py @@ -1,114 +1,61 @@ -"""Conversation router — single entry point for any operator message.""" +"""Single entry: user message → intent → recommendation + session updates.""" from __future__ import annotations from typing import Any -from .approval_manager import ( - build_approval_card, - process_approval_decision, +from auto_client_acquisition.autonomous_service_operator import ( + approval_manager as am, + intent_classifier as ic, + operator_memory as om, + service_bundles as sb, + service_orchestrator as so, + session_state as ss, + workflow_runner as wr, ) -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 handle_message(session_id: str, message: str, mode: str = "client") -> dict[str, Any]: + intent = ic.classify_intent(message) + om.append_turn(session_id, "user", message, {"intent": intent}) + if intent == ic.INTENT_COLD_WHATSAPP_REQUEST: + body = so.cold_whatsapp_response() + om.append_turn(session_id, "assistant", body["message_ar"], {"blocked": True}) + ss.upsert_session(session_id, {"last_intent": intent, "blocked": True}) + return {"session_id": session_id, "intent": intent, **body} -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, + rec = so.recommend_for_intent(intent) + ss.upsert_session( + session_id, + { + "last_intent": intent, + "recommended_service_id": rec["recommended_service_id"], + "mode": mode, + }, ) + wr.advance(session_id, "start_service") - # If a service is recommended, build its initial pipeline + intake form. - response: dict[str, Any] = { + reply_ar = _build_reply_ar(intent, rec) + om.append_turn(session_id, "assistant", reply_ar, {"recommendation": rec}) + return { + "session_id": session_id, "intent": intent, - "handler": handler, - "bundle_recommendation": bundle_rec, - "service_id": routed["recommended_service_id"], - "approval_required": True, - "live_send_allowed": False, + "recommendation": rec, + "reply_ar": reply_ar, + "bundles_hint": sb.list_bundles() if intent == ic.INTENT_ASK_SERVICES else None, + "demo": True, } - 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 "", +def _build_reply_ar(intent: str, rec: dict[str, Any]) -> str: + sid = rec.get("recommended_service_id") + name = rec.get("service_name_ar") or sid + if intent == ic.INTENT_ASK_SERVICES: + return ( + "أنسب مسار: ابدأ بتشخيص مجاني ثم اختر باقة Growth Starter أو Data to Revenue. " + "راجع قائمة الباقات من /api/v1/operator/bundles." ) - - return response + if intent == ic.INTENT_ASK_PROOF: + return f"Proof Pack مرتبط بخدمة {name} — جاهز كعرض تجريبي بعد أول مسودات موافَق عليها." + return f"نوصي بخدمة: {name} ({sid}). الخطوة التالية: أكمل المدخلات ثم راجع المسودات قبل أي إرسال." diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py index 3ebec18d..f3a66667 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/executive_mode.py @@ -1,92 +1,14 @@ -"""Executive Mode — CEO command center + daily brief + revenue risks.""" +"""CEO-style prioritization hints for cards and intents.""" 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).""" +def mode_profile() -> dict[str, Any]: 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, + "mode": "executive", + "priority_intents": ["want_more_customers", "ask_proof", "approve_action"], + "card_types_first": ["leak", "approval_needed", "opportunity"], + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py b/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py index 8772c51d..ca40f32e 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/intake_collector.py @@ -1,129 +1,17 @@ -"""Intake collector — builds intake questions per intent + validates payloads.""" +"""Required intake fields per service_id — from Service Tower catalog.""" 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}, - ], -} +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -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"] +def intake_questions(service_id: str) -> dict[str, Any]: + svc = get_service_by_id(service_id) or {} + fields = svc.get("inputs_required") or [] 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), + "service_id": service_id, + "fields": [{"name": f, "prompt_ar": f"يرجى تزويدنا بـ: {f}"} for f in fields], + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py b/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py index d2fa613b..a1ffe2b1 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/intent_classifier.py @@ -1,180 +1,115 @@ -"""Deterministic intent classifier — Arabic + English keywords → 16 intents.""" +"""Rule-based intent classification (AR/EN) — no LLM required for MVP.""" from __future__ import annotations import re -from typing import Any +from typing import Final -# 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", -} +# Intent ids consumed by service_orchestrator and conversation_router. +INTENT_WANT_MORE_CUSTOMERS: Final = "want_more_customers" +INTENT_HAS_CONTACT_LIST: Final = "has_contact_list" +INTENT_WANT_PARTNERSHIPS: Final = "want_partnerships" +INTENT_WANT_DAILY_GROWTH: Final = "want_daily_growth" +INTENT_WANT_MEETINGS: Final = "want_meetings" +INTENT_WANT_EMAIL_RESCUE: Final = "want_email_rescue" +INTENT_WANT_WHATSAPP_SETUP: Final = "want_whatsapp_setup" +INTENT_ASK_PRICING: Final = "ask_pricing" +INTENT_APPROVE_ACTION: Final = "approve_action" +INTENT_EDIT_ACTION: Final = "edit_action" +INTENT_SKIP_ACTION: Final = "skip_action" +INTENT_ASK_DEMO: Final = "ask_demo" +INTENT_ASK_PROOF: Final = "ask_proof" +INTENT_ASK_SERVICES: Final = "ask_services" +INTENT_ASK_PARTNERSHIP: Final = "ask_partnership" +INTENT_ASK_REVENUE_TODAY: Final = "ask_revenue_today" +INTENT_COLD_WHATSAPP_REQUEST: Final = "cold_whatsapp_request" # blocked path +INTENT_UNKNOWN: Final = "unknown" -def classify_intent(message: str) -> dict[str, Any]: - """ - Classify a free-text message → intent + confidence. +def normalize_user_text(text: str) -> str: + t = (text or "").strip().lower() + t = re.sub(r"\s+", " ", t) + return t - 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]] = {} +def classify_intent(text: str) -> str: + """Return intent id from free-form user message.""" + t = normalize_user_text(text) - 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 t: + return INTENT_UNKNOWN - if not any(scores.values()): - return { - "intent": "ask_services", - "confidence": 0.2, - "matched_keywords": [], - "all_scores": scores, - } + # Dangerous / policy — before generic channel mentions + cold_ar = ("واتساب بارد" in text) or ("رسائل باردة" in text) or ("بارد" in text and "واتساب" in text) + cold_en = "cold whatsapp" in t or "whatsapp blast" in t or "bulk whatsapp" in t + if cold_ar or cold_en: + return INTENT_COLD_WHATSAPP_REQUEST - 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 + if "موافق" in t or t == "approve" or "اعتمد" in t: + return INTENT_APPROVE_ACTION + if "عدّل" in t or "عدل" in t or "edit" in t: + return INTENT_EDIT_ACTION + if "تخطي" in t or "skip" in t: + return INTENT_SKIP_ACTION + + if "سعر" in t or "تسعير" in t or "pricing" in t or "price" in t: + return INTENT_ASK_PRICING + if "ديمو" in t or "demo" in t: + return INTENT_ASK_DEMO + if "proof" in t or "إثبات" in text or "اثبات" in text: + return INTENT_ASK_PROOF + if ( + "خدمات" in t + or "وش أفضل" in t + or "ما أفضل" in t + or "أفضل خدمة" in text + or "افضل خدمة" in text + or "ask_services" in t + ): + return INTENT_ASK_SERVICES + if "شراكة" in t or "partnership" in t or "شراكات" in t: + return INTENT_WANT_PARTNERSHIPS + if "ايراد اليوم" in t or "إيراد اليوم" in text or "revenue today" in t: + return INTENT_ASK_REVENUE_TODAY + + list_signals = ( + "قائمة أرقام" in text + or "عندي قائمة" in text + or "csv" in t + or "قائمة ارقام" in text + or "contacts" in t + or "قائمة جهات" in text ) + if list_signals: + return INTENT_HAS_CONTACT_LIST - return { - "intent": best_intent, - "confidence": confidence, - "matched_keywords": matched_by_intent.get(best_intent, []), - "all_scores": scores, - } + meeting_signals = "اجتماع" in text or "meetings" in t or "حجز" in t + if meeting_signals and ("أبغى" in text or "ابغى" in text or "want" in t): + return INTENT_WANT_MEETINGS + email_signals = ("ايميل" in text or "إيميل" in text or "gmail" in t or "بريد" in text) and ( + "إنقاذ" in text or "rescue" in t or "فرص" in text + ) + if email_signals: + return INTENT_WANT_EMAIL_RESCUE -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) + wa_setup = ("واتساب" in text or "whatsapp" in t) and ("امتثال" in text or "إعداد" in text or "setup" in t) + if wa_setup: + return INTENT_WANT_WHATSAPP_SETUP + + daily = "يومي" in text or "daily" in t or "موجز" in text + if daily: + return INTENT_WANT_DAILY_GROWTH + + customer_signals = ( + "عملاء أكثر" in text + or "أبغى عملاء" in text + or "ابغى عملاء" in text + or "more customers" in t + or "leads" in t + or "فرص" in text + ) + if customer_signals: + return INTENT_WANT_MORE_CUSTOMERS + + return INTENT_UNKNOWN diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py b/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py index 27389d5e..9c33656b 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/operator_memory.py @@ -1,104 +1,19 @@ -"""Operator memory — minimal in-process store for sessions + facts.""" +"""Append-only conversation turns per session (in-memory MVP).""" from __future__ import annotations -import time -from dataclasses import dataclass, field from typing import Any -from .session_state import SessionState +from auto_client_acquisition.autonomous_service_operator import session_state as ss -@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 append_turn(session_id: str, role: str, content: str, meta: dict[str, Any] | None = None) -> None: + s = ss.touch_session(session_id) + log = list(s.get("turns") or []) + log.append({"role": role, "content": content[:4000], **(meta or {})}) + ss.upsert_session(session_id, {"turns": log[-50:]}) -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(), - } +def list_turns(session_id: str) -> list[dict[str, Any]]: + s = ss.get_session(session_id) or {} + return list(s.get("turns") or []) 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 index 3c3885b0..3c6c14d0 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/proof_pack_dispatcher.py @@ -1,72 +1,20 @@ -"""Proof Pack dispatcher — generates + delivers Proof Packs per service.""" +"""Proof Pack summary for a service — deterministic metrics.""" from __future__ import annotations from typing import Any +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -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 {} + +def build_proof_pack(service_id: str) -> dict[str, Any]: + svc = get_service_by_id(service_id) or {} + metrics = list(svc.get("proof_metrics") or ["drafts_created", "approvals_logged"]) 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, + "title_ar": f"Proof Pack — {svc.get('name_ar', service_id)}", + "metrics": metrics, + "sample_counts": {m: 0 for m in metrics}, + "notes_ar": "أرقام تجريبية حتى ربط عميل حقيقي ودفتر أحداث.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/self_growth_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/self_growth_mode.py index 82a9110c..7a5dcc79 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/self_growth_mode.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/self_growth_mode.py @@ -1,55 +1,18 @@ -"""Self-Growth Mode — Dealix uses its own OS to grow. - -Re-exports + extends targeting_os.self_growth_mode with operator-tier wiring. -""" +"""Self-growth mode: Dealix uses its own OS for prospecting (drafts + manual approval only).""" from __future__ import annotations from typing import Any -from auto_client_acquisition.targeting_os.self_growth_mode import ( - DEALIX_ICP_FOCUSES, - build_dealix_self_growth_plan, - build_free_service_offer, - build_self_growth_daily_brief, - build_weekly_learning_report, - recommend_dealix_targets, -) - -def build_operator_self_growth_brief( - *, - include_outreach_hint: bool = True, -) -> dict[str, Any]: - """ - Operator-tier wrapper around the self-growth daily brief. - - Layers in approval-first reminders + reminders to never auto-send. - """ - base = build_self_growth_daily_brief() - out = dict(base) - out["operator_reminders_ar"] = [ - "لا cold WhatsApp — حتى داخل Dealix نفسه.", - "كل رسالة draft تحتاج اعتمادك قبل الإرسال.", - "لا scraping LinkedIn — استخدم Lead Forms أو manual research.", - "كل تواصل يدخل Action Ledger.", - ] - if include_outreach_hint: - out["next_action_ar"] = ( - "اعتمد 3 رسائل اليوم فقط — جودة قبل كمية. " - "Pilot صغير ناجح > 50 رسالة بدون رد." - ) - out["approval_required"] = True - out["live_send_allowed"] = False - return out - - -__all__ = [ - "DEALIX_ICP_FOCUSES", - "build_dealix_self_growth_plan", - "build_free_service_offer", - "build_operator_self_growth_brief", - "build_self_growth_daily_brief", - "build_weekly_learning_report", - "recommend_dealix_targets", -] +def mode_profile() -> dict[str, Any]: + return { + "mode": "self_growth", + "priority_intents": ["want_more_customers", "ask_services", "ask_demo"], + "rules_ar": [ + "لا scraping ولا إرسال جماعي.", + "كل outreach مسودة + موافقة يدوية.", + "Proof Pack أسبوعي للنتائج الداخلية.", + ], + "demo": True, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py b/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py index d91da15e..b7d17c1c 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/service_bundles.py @@ -1,215 +1,90 @@ -"""Service bundles — 6 packaged offerings instead of 20 raw services.""" +"""Productized service bundles — SAR ranges and catalog service_ids.""" 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"], +BundleId = str + +_BUNDLES: dict[BundleId, dict[str, Any]] = { + "growth_starter": { + "bundle_id": "growth_starter", + "title_ar": "Growth Starter", + "services": ["free_growth_diagnostic", "first_10_opportunities"], + "timeline_days": 14, + "price_range_sar": {"min": 499, "max": 499}, + "best_for_ar": "شركات تريد أول قيمة سريعة + Pilot واضح.", + "deliverables_ar": ["تشخيص مجاني", "١٠ فرص + مسودات", "Proof Pack مختصر"], + "proof_metrics": ["opportunities_count", "drafts_created", "approvals_logged"], + "risk_policy_ar": "لا إرسال حي بدون موافقة؛ لا واتساب بارد.", + "upsell_path": "data_to_revenue", }, - { - "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"], + "data_to_revenue": { + "bundle_id": "data_to_revenue", + "title_ar": "من البيانات إلى الإيراد", + "services": ["list_intelligence", "first_10_opportunities"], + "timeline_days": 21, + "price_range_sar": {"min": 1500, "max": 2500}, + "best_for_ar": "من لديه قائمة جهات ويريد أهدافاً مرتبة ومسودات.", + "deliverables_ar": ["أفضل ٥٠ هدفاً", "تقرير قابلية تواصل", "مسودات رسائل"], + "proof_metrics": ["safe_ratio", "drafts_created", "target_ranked"], + "risk_policy_ar": "مسودات فقط؛ موافقة قبل أي إرسال.", + "upsell_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"], + "executive_growth_os": { + "bundle_id": "executive_growth_os", + "title_ar": "Executive Growth OS", + "services": ["executive_growth_brief", "growth_os"], + "timeline_days": 30, + "price_range_sar": {"min": 2999, "max": 9999}, + "best_for_ar": "CEO ومدير نمو يريدان موجزاً يومياً وتشغيل Growth OS.", + "deliverables_ar": ["موجز يومي", "Command feed", "Proof Pack أسبوعي"], + "proof_metrics": ["decisions_logged", "revenue_influenced_sar", "risks_blocked"], + "risk_policy_ar": "بوابة أدوات آمنة؛ تكاملات مسودة افتراضياً.", + "upsell_path": "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"], + "partnership_growth": { + "bundle_id": "partnership_growth", + "title_ar": "نمو عبر الشراكات", + "services": ["partner_sprint", "meeting_booking_sprint"], + "timeline_days": 30, + "price_range_sar": {"min": 3000, "max": 7500}, + "best_for_ar": "توسع عبر شركاء ووكالات.", + "deliverables_ar": ["قائمة شركاء", "مسودات اجتماعات", "مسودة اتفاق إحالة"], + "proof_metrics": ["partner_meetings", "referral_pipeline"], + "risk_policy_ar": "مراجعة قانونية للاتفاقيات.", + "upsell_path": "agency_partner_program", }, - { - "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"], + "local_growth_os": { + "bundle_id": "local_growth_os", + "title_ar": "نمو محلي", + "services": ["local_growth_os"], + "timeline_days": 30, + "price_range_sar": {"min": 999, "max": 2999}, + "best_for_ar": "عيادات ومطاعم ومتاجر محلية.", + "deliverables_ar": ["كروت سمعة", "مسودات رد", "روابط دفع draft"], + "proof_metrics": ["reviews_addressed", "reactivation_drafts"], + "risk_policy_ar": "موافقة على الرسائل العامة.", + "upsell_path": "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": [], + "full_growth_control_tower": { + "bundle_id": "full_growth_control_tower", + "title_ar": "برج تحكم كامل — مخصص", + "services": ["growth_os", "agency_partner_program"], + "timeline_days": 90, + "price_range_sar": {"min": 15000, "max": 80000}, + "best_for_ar": "مؤسسات تريد كل الطبقات على مراحل.", + "deliverables_ar": ["خارطة ٣٠/٦٠/٩٠ يوماً", "حوكمة موافقات", "Proof شهري"], + "proof_metrics": ["pipeline_influenced", "partners_created", "payments_requested"], + "risk_policy_ar": "DPA + مراجعة امتثال قبل التوسع.", + "upsell_path": None, }, -) +} def list_bundles() -> dict[str, Any]: - return { - "total": len(BUNDLES), - "bundles": [dict(b) for b in BUNDLES], - } + return {"bundles": list(_BUNDLES.values()), "demo": True} 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, - } + return _BUNDLES.get((bundle_id or "").strip()) diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/service_delivery_mode.py b/dealix/auto_client_acquisition/autonomous_service_operator/service_delivery_mode.py index 9ee2239d..0b8d9249 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/service_delivery_mode.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/service_delivery_mode.py @@ -1,108 +1,15 @@ -"""Service Delivery Mode — runs client services + tracks SLA + generates Proof. - -Production wrapper around service_orchestrator + revenue_launch.pilot_delivery -+ customer_ops.sla_tracker. -""" +"""Service delivery mode: running client services with SLA-oriented checklist.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.customer_ops import ( - build_sla_health_report, - classify_sla_breach, -) -from auto_client_acquisition.revenue_launch import ( - build_24h_delivery_plan, - build_first_10_opportunities_delivery, - build_growth_diagnostic_delivery, - build_list_intelligence_delivery, -) -from auto_client_acquisition.service_tower import ( - build_service_workflow, - get_service, -) - - -def build_service_delivery_brief( - *, - customer_id: str = "", - service_id: str = "", - intake: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build the day-one delivery brief for a service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - delivery_template_by_service: dict[str, Any] = { - "first_10_opportunities_sprint": - build_first_10_opportunities_delivery(intake or {}), - "list_intelligence": - build_list_intelligence_delivery(intake or {}), - "free_growth_diagnostic": - build_growth_diagnostic_delivery(intake or {}), - } +def mode_profile() -> dict[str, Any]: return { "mode": "service_delivery", - "customer_id": customer_id, - "service_id": service_id, - "service_name_ar": s.name_ar, - "intake_received": bool(intake), - "workflow": build_service_workflow(service_id), - "delivery_template": delivery_template_by_service.get( - service_id, build_24h_delivery_plan(service_id), - ), - "approval_required": True, - "live_send_allowed": False, - } - - -def build_sla_status_for_delivery( - *, - customer_id: str = "", - open_tickets: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Compute SLA health for a customer's open delivery tickets.""" - health = build_sla_health_report(tickets=open_tickets) - breaches: list[dict[str, Any]] = [] - for t in (open_tickets or []): - b = classify_sla_breach( - priority=str(t.get("priority", "P3")), - minutes_to_first_response=t.get("first_response_min"), - hours_to_resolve=t.get("resolution_hours"), - ) - if b["breached"]: - breaches.append({**t, "breach": b}) - return { - "customer_id": customer_id, - "health": health, - "breaches": breaches, - "approval_required": True, - } - - -def build_post_delivery_handoff( - *, - customer_id: str = "", - service_id: str = "", - delivered_metrics: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build the post-delivery handoff (Arabic) → Customer Success cadence.""" - metrics = delivered_metrics or {} - return { - "mode": "service_delivery", - "customer_id": customer_id, - "service_id": service_id, - "delivered_metrics": dict(metrics), - "handoff_steps_ar": [ - "تسليم Proof Pack النهائي للعميل + اعتماده.", - "حجز جلسة مراجعة 30 دقيقة.", - "تفعيل Customer Success cadence (weekly check-ins).", - "اقتراح الترقية المنطقية بناءً على النتائج.", - "تحديث Action Graph + Revenue Work Units.", - ], - "approval_required": True, - "live_send_allowed": False, + "priority_intents": ["approve_action", "ask_proof", "want_meetings"], + "card_types_first": ["approval_needed", "proof", "meeting_prep"], + "sla_reminder_ar": "التسليم حسب نافذة الـ Pilot المتفق عليها؛ لا live send افتراضياً.", + "demo": 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 index d00a7c3f..c83f2ada 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/service_orchestrator.py @@ -1,94 +1,48 @@ -"""Service orchestrator — runs the canonical service pipeline.""" +"""Map intents to recommended service_ids and bundles.""" 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": "ترقية الخدمة", -} +from auto_client_acquisition.autonomous_service_operator import intent_classifier as ic +from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -def build_service_pipeline( - service_id: str, *, customer_id: str = "", -) -> dict[str, Any]: - """Build the canonical pipeline state for a service.""" +def recommend_for_intent(intent: str) -> dict[str, Any]: + """Return primary service_id, optional bundle, and excellence gate.""" + mapping: dict[str, tuple[str, str | None]] = { + ic.INTENT_WANT_MORE_CUSTOMERS: ("first_10_opportunities", "growth_starter"), + ic.INTENT_HAS_CONTACT_LIST: ("list_intelligence", "data_to_revenue"), + ic.INTENT_WANT_PARTNERSHIPS: ("partner_sprint", "partnership_growth"), + ic.INTENT_ASK_PARTNERSHIP: ("partner_sprint", "partnership_growth"), + ic.INTENT_WANT_DAILY_GROWTH: ("self_growth_operator", "executive_growth_os"), + ic.INTENT_WANT_MEETINGS: ("meeting_booking_sprint", None), + ic.INTENT_WANT_EMAIL_RESCUE: ("email_revenue_rescue", None), + ic.INTENT_WANT_WHATSAPP_SETUP: ("whatsapp_compliance_setup", None), + ic.INTENT_ASK_PRICING: ("growth_os", None), + ic.INTENT_ASK_SERVICES: ("free_growth_diagnostic", None), + ic.INTENT_ASK_DEMO: ("free_growth_diagnostic", None), + ic.INTENT_ASK_PROOF: ("first_10_opportunities", None), + ic.INTENT_ASK_REVENUE_TODAY: ("growth_os", None), + } + sid, bundle = mapping.get(intent, ("free_growth_diagnostic", None)) + svc = get_service_by_id(sid) or {} + score = calculate_service_excellence_score(sid) 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, + "intent": intent, + "recommended_service_id": sid, + "service_name_ar": svc.get("name_ar"), + "suggested_bundle_id": bundle, + "excellence": {"total_score": score["total_score"], "status": score["status"]}, + "demo": True, } -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 +def cold_whatsapp_response() -> dict[str, Any]: + return { + "blocked": True, + "message_ar": "لا ندعم واتساب بارد أو غير موافق عليه. نرشّح: قالب opt-in، أو إيميل draft، أو سباق اجتماعات بعد موافقة.", + "alternatives": ["whatsapp_opt_in_template", "gmail_draft", "meeting_booking_sprint"], + "demo": True, + } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py b/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py index a0f8cb99..e0d4bde6 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/session_state.py @@ -1,95 +1,39 @@ -"""Session state — minimal in-memory state for an operator conversation.""" +"""In-memory operator sessions — MVP; replace with DB or revenue_memory later.""" 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", -) +_SESSIONS: dict[str, dict[str, Any]] = {} -@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 new_session_id() -> str: + return f"op_{uuid.uuid4().hex[:16]}" -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 get_session(session_id: str) -> dict[str, Any] | None: + return _SESSIONS.get(session_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 +def upsert_session(session_id: str, patch: dict[str, Any]) -> dict[str, Any]: + base = dict(_SESSIONS.get(session_id, {})) + base.update(patch) + base["session_id"] = session_id + _SESSIONS[session_id] = base + return base + + +def touch_session(session_id: str) -> dict[str, Any]: + if session_id not in _SESSIONS: + _SESSIONS[session_id] = {"session_id": session_id, "workflow_state": "idle"} + return _SESSIONS[session_id] + + +def list_sessions_with_pending() -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for sid, data in _SESSIONS.items(): + pc = data.get("pending_card") + if isinstance(pc, dict): + out.append({"session_id": sid, "card": pc}) + return out 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 index 9fc788d5..a20af9c2 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/tool_action_planner.py @@ -1,102 +1,40 @@ -"""Tool action planner — plan + review actions before they hit Tool Gateway.""" +"""Safe Tool Gateway matrix — execution modes per tool (deterministic).""" from __future__ import annotations -from typing import Any +from typing import Any, Final -# 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", -}) +MODE_SUGGEST_ONLY: Final = "suggest_only" +MODE_DRAFT_ONLY: Final = "draft_only" +MODE_APPROVAL_REQUIRED: Final = "approval_required" +MODE_APPROVED_EXECUTE: Final = "approved_execute" +MODE_BLOCKED: Final = "blocked" -# 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", -}) +# tool_id -> default mode when autonomy is draft_and_approve (Dealix beta default) +_TOOL_MATRIX: dict[str, dict[str, Any]] = { + "gmail_send": {"mode": MODE_BLOCKED, "reason_ar": "إرسال Gmail مباشر محظور افتراضياً."}, + "gmail_draft": {"mode": MODE_DRAFT_ONLY, "reason_ar": "مسودات Gmail مسموحة للمراجعة."}, + "linkedin_scrape": {"mode": MODE_BLOCKED, "reason_ar": "scraping LinkedIn محظور."}, + "linkedin_auto_dm": {"mode": MODE_BLOCKED, "reason_ar": "رسائل LinkedIn آلية محظورة."}, + "cold_whatsapp": {"mode": MODE_BLOCKED, "reason_ar": "واتساب بارد / غير موافق عليه محظور."}, + "whatsapp_opt_in_template": {"mode": MODE_DRAFT_ONLY, "reason_ar": "قوالب opt-in كمسودات."}, + "moyasar_charge": {"mode": MODE_BLOCKED, "reason_ar": "شحن بطاقة من API غير مفعّل."}, + "moyasar_payment_link_draft": {"mode": MODE_DRAFT_ONLY, "reason_ar": "مسودة رابط دفع مسموحة."}, + "google_calendar_insert": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "إدراج تقويم يحتاج موافقة."}, + "crm_update": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "تحديث CRM بعد موافقة."}, + "google_sheets_export": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "تصدير مع موافقة عند الحساسية."}, + "meeting_transcript_read": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "قراءة محضر تتطلب نطاقاً وموافقة."}, +} -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 evaluate_tool(tool_id: str, autonomy_mode: str = "draft_and_approve") -> dict[str, Any]: + tid = (tool_id or "").strip().lower() + row = _TOOL_MATRIX.get(tid, {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "أداة غير مسجّلة — موافقة افتراضية."}) + mode = row["mode"] + if autonomy_mode in ("manual", "suggest_only") and mode == MODE_APPROVED_EXECUTE: + mode = MODE_SUGGEST_ONLY + return {"tool_id": tid, "mode": mode, "reason_ar": row["reason_ar"], "demo": True} -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 +def list_tool_matrix() -> dict[str, Any]: + return {"tools": [{**{"tool_id": k}, **v} for k, v in _TOOL_MATRIX.items()], "demo": True} diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py b/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py index 16fcae16..1fa72df4 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/upsell_engine.py @@ -1,94 +1,25 @@ -"""Upsell engine — recommend the next service after current one delivers.""" +"""Suggest next service / bundle from catalog upgrade_path.""" 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", -} +from auto_client_acquisition.autonomous_service_operator import service_bundles as sb +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -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 يوم قبل الاشتراك الشهري." - ) - +def suggest_upsell(service_id: str) -> dict[str, Any]: + svc = get_service_by_id(service_id) or {} + nxt = svc.get("upgrade_path") + next_svc = get_service_by_id(str(nxt)) if nxt else None + bundle_hint = None + if nxt == "growth_os": + bundle_hint = "executive_growth_os" 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, + "from_service_id": service_id, + "next_service_id": nxt, + "next_name_ar": (next_svc or {}).get("name_ar"), + "suggested_bundle_id": bundle_hint, + "bundles": sb.get_bundle(bundle_hint) if bundle_hint else None, + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py b/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py index 184646e1..adc47ba4 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/whatsapp_renderer.py @@ -1,75 +1,17 @@ -"""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. -""" +"""WhatsApp payload shapes (text templates only — no live send).""" 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)) - +def render_daily_brief_stub() -> dict[str, Any]: 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, + "format": "text_stub", + "body_ar": ( + "موجز Dealix (مسودة): ٣ قرارات مقترحة — راجع لوحة الموافقات. " + "لا يُرسل هذا النص تلقائياً من المنصة في MVP." + ), + "demo": True, } diff --git a/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py b/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py index 8ef47e3d..26fc86b0 100644 --- a/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py +++ b/dealix/auto_client_acquisition/autonomous_service_operator/workflow_runner.py @@ -1,43 +1,24 @@ -"""Workflow runner — advances service pipelines + checks completion.""" +"""Simple workflow state machine: intake → draft → pending_approval → proof.""" from __future__ import annotations from typing import Any -from .service_orchestrator import ( - SERVICE_PIPELINE_STEPS, - build_service_pipeline, - run_service_step, -) +from auto_client_acquisition.autonomous_service_operator import session_state as ss -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(session_id: str, event: str) -> dict[str, Any]: + """event: start_service | draft_ready | submit_for_approval | proof_ready""" + s = ss.touch_session(session_id) + state = str(s.get("workflow_state") or "idle") + ev = (event or "").strip().lower() + transitions: dict[tuple[str, str], str] = { + ("idle", "start_service"): "intake", + ("intake", "draft_ready"): "draft", + ("draft", "submit_for_approval"): "pending_approval", + ("pending_approval", "proof_ready"): "proof", + ("proof", "start_service"): "intake", } - - -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) + key = (state, ev) + new_state = transitions.get(key, state) + return ss.upsert_session(session_id, {"workflow_state": new_state, "last_event": ev}) diff --git a/dealix/auto_client_acquisition/connectors/__init__.py b/dealix/auto_client_acquisition/connectors/__init__.py index 999fb82c..cbbc4bb6 100644 --- a/dealix/auto_client_acquisition/connectors/__init__.py +++ b/dealix/auto_client_acquisition/connectors/__init__.py @@ -1 +1,5 @@ -"""Connector modules for Dealix Lead Machine — legal, public-data sources.""" +"""Connector catalog for Growth Control Tower — metadata only.""" + +from auto_client_acquisition.connectors.connector_catalog import build_connector_catalog + +__all__ = ["build_connector_catalog"] diff --git a/dealix/auto_client_acquisition/connectors/connector_catalog.py b/dealix/auto_client_acquisition/connectors/connector_catalog.py new file mode 100644 index 00000000..d32caa42 --- /dev/null +++ b/dealix/auto_client_acquisition/connectors/connector_catalog.py @@ -0,0 +1,45 @@ +"""12+ connectors with capabilities and risk — no live OAuth in MVP.""" + +from __future__ import annotations + +from typing import Any + + +def _row( + cid: str, + label_ar: str, + beta: str, + allowed: list[str], + blocked: list[str], + risk: str, +) -> dict[str, Any]: + return { + "id": cid, + "label_ar": label_ar, + "beta_status": beta, + "required_permissions_ar": "OAuth أو مفاتيح رسمية حسب المزود — لا تخزين أسرار في الريبو.", + "allowed_actions": allowed, + "blocked_actions": blocked, + "risk_level": risk, + "launch_phase": 1 if beta == "mvp" else 2 if beta == "pilot" else 3, + } + + +_CONNECTORS: list[dict[str, Any]] = [ + _row("whatsapp", "واتساب للأعمال", "pilot", ["draft", "template_preview"], ["cold_bulk", "live_send"], "high"), + _row("gmail", "Gmail", "pilot", ["draft_create", "read_limited"], ["send_live"], "medium"), + _row("google_calendar", "Google Calendar", "pilot", ["draft_event"], ["insert_live"], "medium"), + _row("google_meet", "Google Meet", "planned", ["transcript_read"], ["record_without_consent"], "high"), + _row("linkedin_lead_forms", "LinkedIn Lead Gen", "mvp", ["lead_ingest"], ["auto_dm"], "medium"), + _row("x_api", "X API", "registered_only", [], ["firehose", "auto_reply"], "high"), + _row("instagram_graph", "Instagram Graph", "registered_only", [], ["auto_dm"], "high"), + _row("google_business_profile", "Google Business Profile", "registered_only", ["review_draft"], ["auto_reply"], "medium"), + _row("google_sheets", "Google Sheets", "planned", ["read_range_draft"], ["write_live"], "low"), + _row("crm", "CRM عام", "planned", ["sync_draft"], ["delete_records"], "medium"), + _row("moyasar", "Moyasar", "mvp", ["payment_link_draft"], ["charge_live"], "high"), + _row("website_forms", "نماذج موقع", "mvp", ["webhook_ingest"], ["scraping"], "low"), +] + + +def build_connector_catalog() -> dict[str, Any]: + return {"connectors": list(_CONNECTORS), "count": len(_CONNECTORS), "demo": True} diff --git a/dealix/auto_client_acquisition/customer_ops/__init__.py b/dealix/auto_client_acquisition/customer_ops/__init__.py index 311847ae..03ef7197 100644 --- a/dealix/auto_client_acquisition/customer_ops/__init__.py +++ b/dealix/auto_client_acquisition/customer_ops/__init__.py @@ -1,78 +1,6 @@ -"""Customer Ops — onboarding + connector setup + support SLA + incidents. +"""Customer operations: onboarding, SLA, connectors, incidents (deterministic stubs).""" -Closes the gap between "great product" and "great customer experience": - - onboarding_checklist: 8-step Pilot onboarding - - connector_setup_status: per-connector readiness - - support_ticket_router: P0–P3 categorization + routing - - sla_tracker: time-to-first-response, MTTR, weekly health - - customer_success_cadence: weekly check-in cadence + risk flags - - incident_router: triage P0/P1 incidents with audit -""" +from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist +from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary -from __future__ import annotations - -from .connector_setup_status import ( - SUPPORTED_CONNECTORS, - build_connector_setup_summary, - get_connector_status, - update_connector_status, -) -from .customer_success_cadence import ( - CADENCE_TYPES, - build_at_risk_alert, - build_customer_success_plan, - build_weekly_check_in, -) -from .incident_router import ( - INCIDENT_SEVERITIES, - build_incident_response_plan, - triage_incident, -) -from .onboarding_checklist import ( - ONBOARDING_STEPS, - build_onboarding_checklist, - update_onboarding_step, -) -from .sla_tracker import ( - SLA_TARGETS, - build_sla_health_report, - classify_sla_breach, - record_sla_event, -) -from .support_ticket_router import ( - SUPPORT_PRIORITIES, - build_first_response_template, - classify_ticket_priority, - route_ticket, -) - -__all__ = [ - # connector_setup_status - "SUPPORTED_CONNECTORS", - "build_connector_setup_summary", - "get_connector_status", - "update_connector_status", - # customer_success_cadence - "CADENCE_TYPES", - "build_at_risk_alert", - "build_customer_success_plan", - "build_weekly_check_in", - # incident_router - "INCIDENT_SEVERITIES", - "build_incident_response_plan", - "triage_incident", - # onboarding_checklist - "ONBOARDING_STEPS", - "build_onboarding_checklist", - "update_onboarding_step", - # sla_tracker - "SLA_TARGETS", - "build_sla_health_report", - "classify_sla_breach", - "record_sla_event", - # support_ticket_router - "SUPPORT_PRIORITIES", - "build_first_response_template", - "classify_ticket_priority", - "route_ticket", -] +__all__ = ["build_onboarding_checklist", "build_sla_summary"] diff --git a/dealix/auto_client_acquisition/customer_ops/connector_setup_status.py b/dealix/auto_client_acquisition/customer_ops/connector_setup_status.py index f74310fb..81f18906 100644 --- a/dealix/auto_client_acquisition/customer_ops/connector_setup_status.py +++ b/dealix/auto_client_acquisition/customer_ops/connector_setup_status.py @@ -1,98 +1,43 @@ -"""Connector setup status — per-customer readiness across all integrations.""" +"""Connector readiness matrix (demo / staging oriented).""" from __future__ import annotations from typing import Any -# 11 connectors Dealix supports during onboarding. -SUPPORTED_CONNECTORS: tuple[dict[str, Any], ...] = ( - {"key": "gmail", "label_ar": "Gmail", "default_mode": "draft_only", - "blocking": False, "phase": "phase_1"}, - {"key": "google_calendar", "label_ar": "Google Calendar", - "default_mode": "draft_only", "blocking": False, "phase": "phase_1"}, - {"key": "google_sheets", "label_ar": "Google Sheets", - "default_mode": "approved_execute", "blocking": False, "phase": "phase_1"}, - {"key": "moyasar", "label_ar": "Moyasar (manual invoice)", - "default_mode": "manual", "blocking": False, "phase": "phase_1"}, - {"key": "whatsapp_cloud", "label_ar": "WhatsApp Business", - "default_mode": "draft_only", "blocking": True, "phase": "phase_1"}, - {"key": "website_forms", "label_ar": "Website Forms", - "default_mode": "approved_execute", "blocking": False, "phase": "phase_1"}, - {"key": "linkedin_lead_forms", "label_ar": "LinkedIn Lead Gen Forms", - "default_mode": "ingest_only", "blocking": False, "phase": "phase_2"}, - {"key": "google_business_profile", "label_ar": "Google Business Profile", - "default_mode": "draft_only", "blocking": False, "phase": "phase_2"}, - {"key": "crm_generic", "label_ar": "CRM (HubSpot/Salesforce/Zoho/Close)", - "default_mode": "draft_only", "blocking": False, "phase": "phase_2"}, - {"key": "google_meet", "label_ar": "Google Meet (transcripts)", - "default_mode": "ingest_only", "blocking": False, "phase": "phase_2"}, - {"key": "instagram_graph", "label_ar": "Instagram (comments/DMs)", - "default_mode": "ingest_only", "blocking": False, "phase": "phase_3"}, -) - - -def get_connector_status(connector_key: str) -> dict[str, Any]: - """Return the static description of a connector.""" - c = next((dict(c) for c in SUPPORTED_CONNECTORS if c["key"] == connector_key), None) - if c is None: - return {"error": f"unknown connector: {connector_key}"} - return c - - -def update_connector_status( - statuses: dict[str, dict[str, Any]], - *, - connector_key: str, - state: str, - notes: str = "", -) -> dict[str, dict[str, Any]]: - """Update the live status of a connector for a customer.""" - if state not in {"not_started", "configuring", "connected_draft_only", - "connected_approved_execute", "failed", "skipped"}: - raise ValueError(f"Unknown connector state: {state}") - statuses[connector_key] = { - "state": state, - "notes": notes[:200], - } - return statuses - - -def build_connector_setup_summary( - *, - customer_id: str = "", - statuses: dict[str, dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Build a connector setup summary for a customer.""" - statuses = statuses or {} - connected = 0 - blocking_missing: list[str] = [] - by_state: dict[str, int] = {} - - items: list[dict[str, Any]] = [] - for c in SUPPORTED_CONNECTORS: - live = statuses.get(c["key"], {}) - state = live.get("state", "not_started") - by_state[state] = by_state.get(state, 0) + 1 - if state in ("connected_draft_only", "connected_approved_execute"): - connected += 1 - if c["blocking"] and state not in ( - "connected_draft_only", "connected_approved_execute", - ): - blocking_missing.append(c["key"]) - items.append({**c, "state": state, "notes": live.get("notes", "")}) - - total = len(SUPPORTED_CONNECTORS) - pct = round(100 * connected / total, 1) if total else 0.0 +def build_connector_status() -> dict[str, Any]: return { - "customer_id": customer_id, - "total_connectors": total, - "connected_count": connected, - "connected_pct": pct, - "blocking_missing": blocking_missing, - "by_state": by_state, - "items": items, - "ready_for_first_service": ( - len(blocking_missing) == 0 and connected >= 1 - ), + "connectors": [ + { + "id": "whatsapp", + "name_ar": "واتساب", + "status": "draft_only", + "notes_ar": "الإرسال الحي يتطلب opt-in وسياسة وموافقة.", + }, + { + "id": "gmail", + "name_ar": "Gmail", + "status": "draft_ready", + "notes_ar": "المسودات أولاً؛ الإرسال محظور افتراضياً.", + }, + { + "id": "google_calendar", + "name_ar": "Google Calendar", + "status": "draft_ready", + "notes_ar": "إدراج الحدث يتطلب موافقة.", + }, + { + "id": "moyasar", + "name_ar": "Moyasar", + "status": "manual_or_sandbox", + "notes_ar": "روابط دفع/فواتير يدوية أو sandbox؛ لا charge من المنصة افتراضياً.", + }, + { + "id": "linkedin_lead_forms", + "name_ar": "LinkedIn Lead Gen", + "status": "strategy_only", + "notes_ar": "لا scraping؛ نماذج رسمية وإعلانات ومهام يدوية معتمدة.", + }, + ], + "summary_ar": "الوضع الافتراضي: مسودات وموافقات؛ لا توسيع live قبل staging واتفاق العميل.", } diff --git a/dealix/auto_client_acquisition/customer_ops/customer_success_cadence.py b/dealix/auto_client_acquisition/customer_ops/customer_success_cadence.py index c26404b3..bf795a0c 100644 --- a/dealix/auto_client_acquisition/customer_ops/customer_success_cadence.py +++ b/dealix/auto_client_acquisition/customer_ops/customer_success_cadence.py @@ -1,146 +1,23 @@ -"""Customer Success cadence — weekly check-ins + at-risk alerts.""" +"""Weekly cadence for pilots (deterministic).""" from __future__ import annotations from typing import Any -# Cadence types Dealix supports. -CADENCE_TYPES: tuple[str, ...] = ( - "weekly_check_in", - "monthly_proof_review", - "quarterly_business_review", - "at_risk_alert", - "renewal_30_day", - "renewal_7_day", -) - - -def build_weekly_check_in( - *, - customer_id: str = "", - company_name: str = "", - metrics: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build a weekly check-in agenda + Arabic talking points.""" - m = metrics or {} - drafts = int(m.get("drafts_approved", 0)) - replies = int(m.get("replies", 0)) - meetings = int(m.get("meetings", 0)) - risks = int(m.get("risks_blocked", 0)) - pipeline = float(m.get("pipeline_sar", 0)) +def build_weekly_cadence() -> dict[str, Any]: return { - "customer_id": customer_id, - "company_name": company_name, - "type": "weekly_check_in", - "agenda_ar": [ - "مراجعة آخر Proof Pack (5 دقائق).", - "أبرز فرصة في الـ pipeline (5 دقائق).", - "أبرز خطر في القنوات (5 دقائق).", - "خطة الأسبوع القادم (5 دقائق).", - "أي مساعدة من فريقنا؟ (5 دقائق).", + "weekly_touchpoints_ar": [ + "مراجعة كروت الموافقة المعلقة.", + "تحديث Proof Pack (مسودات، موافقات، مخاطر ممنوعة).", + "مكالمة قصيرة أو تحديث كتابي مع صاحب القرار.", + "قراءة مؤشرات القنوات (ردود، شكاوى، opt-out = صفر مطلوب).", ], - "talking_points_ar": [ - f"اعتمدتم {drafts} رسالة هذا الأسبوع، ووصلكم {replies} رد.", - f"تم تجهيز {meetings} اجتماع.", - f"تم منع {risks} مخاطر تلقائياً.", - f"Pipeline متأثر بقيمة {pipeline:.0f} ريال.", - ], - "approval_required": True, - "live_send_allowed": False, - } - - -def build_at_risk_alert( - *, - customer_id: str = "", - days_inactive: int = 0, - drafts_pending: int = 0, - last_proof_pack_days_ago: int = 0, -) -> dict[str, Any]: - """Build an at-risk alert when a customer shows churn signals.""" - risk_score = 0 - reasons: list[str] = [] - - if days_inactive >= 14: - risk_score += 40 - reasons.append(f"العميل غير نشط منذ {days_inactive} يوم.") - elif days_inactive >= 7: - risk_score += 20 - reasons.append(f"انخفاض النشاط منذ {days_inactive} يوم.") - - if drafts_pending >= 10: - risk_score += 25 - reasons.append(f"{drafts_pending} مسودة معلقة بدون اعتماد.") - elif drafts_pending >= 5: - risk_score += 10 - reasons.append(f"تراكم {drafts_pending} مسودة بدون اعتماد.") - - if last_proof_pack_days_ago >= 14: - risk_score += 30 - reasons.append( - f"آخر Proof Pack قبل {last_proof_pack_days_ago} يوم — يتجاوز SLA." - ) - - risk_score = min(100, risk_score) - if risk_score >= 60: - severity = "high" - action_ar = "أرسل إيميل personal من المؤسس + احجز QBR هذا الأسبوع." - elif risk_score >= 30: - severity = "medium" - action_ar = "أرسل Proof Pack ملخص + اقترح ديمو لخدمة جديدة." - else: - severity = "low" - action_ar = "متابعة weekly check-in عادية." - - return { - "customer_id": customer_id, - "type": "at_risk_alert", - "risk_score": risk_score, - "severity": severity, - "reasons_ar": reasons, - "recommended_action_ar": action_ar, - "approval_required": True, - "live_send_allowed": False, - } - - -def build_customer_success_plan( - *, - customer_id: str = "", - bundle_id: str = "growth_starter", -) -> dict[str, Any]: - """Build a 30-day customer success cadence plan.""" - cadence_by_bundle = { - "growth_starter": [ - "Day 1: kick-off call + intake.", - "Day 3: review first 3 opportunities + drafts.", - "Day 7: deliver Proof Pack v1.", - "Day 14: weekly check-in + upsell offer.", - "Day 30: monthly proof review + renewal/upgrade decision.", - ], - "executive_growth_os": [ - "Day 1: onboarding + connect channels.", - "Day 7: first weekly Proof Pack.", - "Day 14: weekly check-in + Founder Shadow Board v1.", - "Day 21: monthly proof review.", - "Day 30: QBR + annual upgrade conversation.", - ], - "partnership_growth": [ - "Day 1: partner ICP intake.", - "Day 5: 20 partners list + 10 outreach drafts.", - "Day 10: 5 partner meetings booked.", - "Day 14: weekly check-in.", - "Day 30: partner scorecard + revenue share setup.", + "metrics_to_track": [ + "demos_booked", + "pilots_active", + "drafts_approved", + "risks_blocked", + "proof_events", ], } - - return { - "customer_id": customer_id, - "bundle_id": bundle_id, - "cadence_ar": cadence_by_bundle.get( - bundle_id, cadence_by_bundle["growth_starter"], - ), - "default_cadence_type": "weekly_check_in", - "approval_required": True, - } diff --git a/dealix/auto_client_acquisition/customer_ops/incident_router.py b/dealix/auto_client_acquisition/customer_ops/incident_router.py index 617ad24d..7ecbfefc 100644 --- a/dealix/auto_client_acquisition/customer_ops/incident_router.py +++ b/dealix/auto_client_acquisition/customer_ops/incident_router.py @@ -1,104 +1,25 @@ -"""Incident router — triage P0/P1 incidents with audit + response plan.""" +"""Incident routing stub (no paging, no secrets).""" from __future__ import annotations from typing import Any -INCIDENT_SEVERITIES: tuple[dict[str, Any], ...] = ( - { - "id": "SEV1", - "label_ar": "حرج جداً — تسريب أمان / إرسال خاطئ / تعطل كامل", - "first_action_minutes": 15, - "communication_cadence_minutes": 30, - }, - { - "id": "SEV2", - "label_ar": "خدمة مهمة معطلة لعدد كبير من العملاء", - "first_action_minutes": 30, - "communication_cadence_minutes": 60, - }, - { - "id": "SEV3", - "label_ar": "خدمة معطلة لعميل واحد أو degraded performance", - "first_action_minutes": 120, - "communication_cadence_minutes": 240, - }, -) - - -def triage_incident( - *, - title: str, - description: str = "", - affected_customers: int = 1, - has_data_leak: bool = False, - has_unauthorized_send: bool = False, -) -> dict[str, Any]: - """Triage an incident → severity + first actions + comms cadence.""" - if has_data_leak or has_unauthorized_send: - sev = "SEV1" - reason_ar = ( - "تسريب أمان أو إرسال غير معتمد — أعلى أولوية." - ) - elif affected_customers >= 5: - sev = "SEV2" - reason_ar = f"عدد العملاء المتأثرين: {affected_customers} ≥ 5." - else: - sev = "SEV3" - reason_ar = "حدث محدود التأثير." - - severity = next( - (dict(s) for s in INCIDENT_SEVERITIES if s["id"] == sev), - dict(INCIDENT_SEVERITIES[2]), - ) +def build_incident_playbook() -> dict[str, Any]: return { - "title": title[:120], - "description": description[:500], - "severity": sev, - "reason_ar": reason_ar, - "severity_details": severity, - "affected_customers": affected_customers, - "has_data_leak": has_data_leak, - "has_unauthorized_send": has_unauthorized_send, - "approval_required": True, - "live_send_allowed": False, + "steps_ar": [ + "تصنيف الخطورة (P0–P3) وفق وصف الحادث.", + "إيقاف أي إجراء live إن وُجد حتى التحقق.", + "توثيق الوقت، التأثير، والخطوات المتخذة (بدون أسرار أو PII خام).", + "إشعار العميل بلغة واضحة وخطة تعافي.", + "مراجعة لاحقة وتحديث السياسات/الاختبارات إن لزم.", + ], + "contacts_placeholder_ar": "يُحدَّد في العقد: بريد دعم + قناة طوارئ للـ P0.", } -def build_incident_response_plan( - *, - severity: str = "SEV3", -) -> dict[str, Any]: - """Build the canonical incident response plan (Arabic).""" - common_steps = [ - "1. تجميد الـ live actions على القناة المعنية فوراً.", - "2. إخطار المؤسس + on-call operator.", - "3. إنشاء incident channel مع timeline.", - "4. مراجعة Action Ledger للأفعال المرتبطة.", - "5. إذا تسريب: إخطار العملاء المتأثرين خلال 72 ساعة (PDPL).", - ] - - if severity == "SEV1": - plan = common_steps + [ - "6. تواصل مباشر مع المؤسس + خلية أزمة.", - "7. كتابة post-mortem خلال 24 ساعة.", - "8. مراجعة قانونية إن لزم.", - ] - elif severity == "SEV2": - plan = common_steps + [ - "6. تحديث العملاء المتأثرين كل 60 دقيقة.", - "7. post-mortem خلال 48 ساعة.", - ] - else: - plan = common_steps + [ - "6. تحديث العميل المتأثر مع كل خطوة.", - "7. post-mortem اختياري.", - ] - - return { - "severity": severity, - "plan_ar": plan, - "approval_required": True, - "live_send_allowed": False, - } +def classify_incident(severity: str) -> dict[str, Any]: + s = (severity or "P3").upper() + if s not in {"P0", "P1", "P2", "P3"}: + s = "P3" + return {"severity": s, "escalate": s in {"P0", "P1"}} diff --git a/dealix/auto_client_acquisition/customer_ops/onboarding_checklist.py b/dealix/auto_client_acquisition/customer_ops/onboarding_checklist.py index 39950966..2f942a78 100644 --- a/dealix/auto_client_acquisition/customer_ops/onboarding_checklist.py +++ b/dealix/auto_client_acquisition/customer_ops/onboarding_checklist.py @@ -1,120 +1,23 @@ -"""Onboarding checklist — the 8-step Pilot onboarding flow.""" +"""Onboarding checklist for pilots (deterministic, no external calls).""" from __future__ import annotations from typing import Any -ONBOARDING_STEPS: tuple[dict[str, Any], ...] = ( - { - "id": "select_goal", - "label_ar": "اختيار الهدف الأساسي", - "input_required": "goal", - "minutes": 2, - "approval_required": False, - }, - { - "id": "select_bundle", - "label_ar": "اختيار الباقة المناسبة", - "input_required": "bundle_id", - "minutes": 3, - "approval_required": True, - }, - { - "id": "company_intake", - "label_ar": "بيانات الشركة", - "input_required": "company_profile", - "minutes": 5, - "approval_required": False, - }, - { - "id": "connect_channels", - "label_ar": "ربط القنوات (Gmail/Calendar/Sheets — drafts فقط)", - "input_required": "channels_oauth", - "minutes": 8, - "approval_required": True, - }, - { - "id": "upload_or_source", - "label_ar": "رفع قائمة أو ربط مصدر leads", - "input_required": "list_or_source", - "minutes": 5, - "approval_required": True, - }, - { - "id": "risk_review", - "label_ar": "مراجعة المخاطر (PDPL + سمعة القناة)", - "input_required": None, - "minutes": 4, - "approval_required": True, - }, - { - "id": "first_service_run", - "label_ar": "تشغيل أول خدمة (First 10 Opportunities أو List Intelligence)", - "input_required": None, - "minutes": 0, # async — Dealix runs it - "approval_required": True, - }, - { - "id": "first_proof_pack", - "label_ar": "استلام أول Proof Pack", - "input_required": None, - "minutes": 0, # async - "approval_required": False, - }, -) - -def build_onboarding_checklist( - *, - customer_id: str = "", - company_name: str = "", - bundle_id: str | None = None, -) -> dict[str, Any]: - """Build a fresh onboarding checklist for a new customer.""" +def build_onboarding_checklist(service_id: str | None = None) -> dict[str, Any]: + sid = (service_id or "growth_starter").strip() or "growth_starter" return { - "customer_id": customer_id, - "company_name": company_name, - "bundle_id": bundle_id, - "total_steps": len(ONBOARDING_STEPS), - "current_step_id": ONBOARDING_STEPS[0]["id"], - "steps": [ - {**dict(s), "completed": False} for s in ONBOARDING_STEPS + "service_id": sid, + "steps_ar": [ + "تأكيد الهدف (عملاء جدد / قائمة / شراكات / تشغيل يومي).", + "جمع بيانات الشركة: القطاع، المدينة، العرض، رابط الموقع.", + "تحديد القنوات المتاحة (إيميل، واتساب opt-in، CRM، نماذج).", + "رفع قائمة اختيارية أو تأكيد عدم وجود قائمة.", + "مراجعة سياسة الموافقات وعدم الإرسال الحي الافتراضي.", + "تشغيل أول مهمة (تشخيص أو 10 فرص أو List Intelligence).", + "تسليم أول Proof Pack أو ملخص أثر خلال النافذة المتفق عليها.", ], - "estimated_total_minutes": sum(int(s["minutes"]) for s in ONBOARDING_STEPS), - "live_send_allowed": False, + "approval_required": True, + "live_send_default": False, } - - -def update_onboarding_step( - checklist: dict[str, Any], - *, - step_id: str, - completed: bool = True, - notes: str = "", -) -> dict[str, Any]: - """Mark a step complete + advance current_step_id.""" - steps = list(checklist.get("steps", [])) - found = False - for i, s in enumerate(steps): - if s["id"] == step_id: - s["completed"] = bool(completed) - if notes: - s["notes"] = notes[:200] - steps[i] = s - found = True - # advance current_step_id - if completed and i + 1 < len(steps): - checklist["current_step_id"] = steps[i + 1]["id"] - elif completed and i + 1 == len(steps): - checklist["current_step_id"] = "done" - break - - if not found: - return {**checklist, "error": f"unknown step: {step_id}"} - - completed_count = sum(1 for s in steps if s["completed"]) - checklist["steps"] = steps - checklist["progress_pct"] = round( - 100 * completed_count / max(1, len(steps)), 1, - ) - return checklist diff --git a/dealix/auto_client_acquisition/customer_ops/sla_tracker.py b/dealix/auto_client_acquisition/customer_ops/sla_tracker.py index 36fdd6b5..ec0906dd 100644 --- a/dealix/auto_client_acquisition/customer_ops/sla_tracker.py +++ b/dealix/auto_client_acquisition/customer_ops/sla_tracker.py @@ -1,132 +1,37 @@ -"""SLA tracker — measure first-response, MTTR, weekly support health.""" +"""SLA summary for support tiers (static policy text + JSON for API).""" from __future__ import annotations -import time from typing import Any -# Default SLA targets per priority (minutes for first_response, hours for resolution). -SLA_TARGETS: dict[str, dict[str, float]] = { - "P0": {"first_response_min": 30, "resolution_hours": 4}, - "P1": {"first_response_min": 120, "resolution_hours": 24}, - "P2": {"first_response_min": 480, "resolution_hours": 72}, - "P3": {"first_response_min": 1440, "resolution_hours": 168}, -} - - -def record_sla_event( - *, - ticket_id: str, - priority: str, - event: str, - log: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """ - Record an SLA event. - - `event` = "opened" | "first_response" | "resolved" | "escalated". - """ - if event not in {"opened", "first_response", "resolved", "escalated"}: - raise ValueError(f"Unknown SLA event: {event}") - entry: dict[str, Any] = { - "ticket_id": ticket_id, - "priority": priority, - "event": event, - "ts": time.time(), - } - if log is not None: - log.append(entry) - return entry - - -def classify_sla_breach( - *, - priority: str, - minutes_to_first_response: float | None = None, - hours_to_resolve: float | None = None, -) -> dict[str, Any]: - """Classify whether SLA was breached for a single ticket.""" - target = SLA_TARGETS.get(priority, SLA_TARGETS["P3"]) - breaches: list[str] = [] - - if (minutes_to_first_response is not None - and minutes_to_first_response > target["first_response_min"]): - breaches.append( - f"first_response: {minutes_to_first_response:.0f} > " - f"{target['first_response_min']} min" - ) - - if (hours_to_resolve is not None - and hours_to_resolve > target["resolution_hours"]): - breaches.append( - f"resolution: {hours_to_resolve:.1f}h > " - f"{target['resolution_hours']}h" - ) +def build_sla_summary() -> dict[str, Any]: return { - "priority": priority, - "breached": bool(breaches), - "breaches": breaches, - } - - -def build_sla_health_report( - *, - tickets: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Build a weekly SLA health report from a list of tickets.""" - tickets = tickets or [] - by_priority: dict[str, dict[str, Any]] = {} - total_tickets = len(tickets) - total_breached = 0 - - for t in tickets: - priority = str(t.get("priority", "P3")) - bucket = by_priority.setdefault(priority, { - "count": 0, "breaches": 0, - "total_first_response_min": 0.0, - "total_resolution_hours": 0.0, - "responded_count": 0, "resolved_count": 0, - }) - bucket["count"] += 1 - ftr = t.get("first_response_min") - ttr = t.get("resolution_hours") - b = classify_sla_breach( - priority=priority, - minutes_to_first_response=ftr, - hours_to_resolve=ttr, - ) - if b["breached"]: - bucket["breaches"] += 1 - total_breached += 1 - if ftr is not None: - bucket["total_first_response_min"] += float(ftr) - bucket["responded_count"] += 1 - if ttr is not None: - bucket["total_resolution_hours"] += float(ttr) - bucket["resolved_count"] += 1 - - # Compute averages. - for p, b in by_priority.items(): - if b["responded_count"]: - b["avg_first_response_min"] = round( - b["total_first_response_min"] / b["responded_count"], 1, - ) - if b["resolved_count"]: - b["avg_resolution_hours"] = round( - b["total_resolution_hours"] / b["resolved_count"], 2, - ) - - breach_rate = round(total_breached / total_tickets, 3) if total_tickets else 0.0 - - return { - "total_tickets": total_tickets, - "total_breached": total_breached, - "breach_rate": breach_rate, - "by_priority": by_priority, - "verdict": ( - "healthy" if breach_rate < 0.10 - else "watch" if breach_rate < 0.25 - else "critical" - ), + "tiers": [ + { + "id": "P0", + "name_ar": "أمان / إرسال خاطئ / توقف كامل", + "first_response_hours": 2, + "resolution_target_hours": 8, + }, + { + "id": "P1", + "name_ar": "تعطل خدمة أساسية", + "first_response_hours": 4, + "resolution_target_hours": 24, + }, + { + "id": "P2", + "name_ar": "تكامل أو Proof متأخر", + "first_response_hours": 24, + "resolution_target_hours": 72, + }, + { + "id": "P3", + "name_ar": "سؤال أو تحسين", + "first_response_hours": 48, + "resolution_target_hours": 120, + }, + ], + "notes_ar": "الأرقام أهداف تشغيلية للـ Pilot؛ تُحدّث في العقد/Appendix عند التوسع.", } diff --git a/dealix/auto_client_acquisition/customer_ops/support_ticket_router.py b/dealix/auto_client_acquisition/customer_ops/support_ticket_router.py index 1705abcf..485e6022 100644 --- a/dealix/auto_client_acquisition/customer_ops/support_ticket_router.py +++ b/dealix/auto_client_acquisition/customer_ops/support_ticket_router.py @@ -1,149 +1,16 @@ -"""Support ticket router — P0–P3 categorization + routing + first-response template.""" +"""Map issue text to priority bucket (deterministic heuristics).""" from __future__ import annotations -import re from typing import Any -# 4 priority tiers Dealix supports. -SUPPORT_PRIORITIES: tuple[dict[str, Any], ...] = ( - { - "id": "P0", - "label_ar": "حرج جداً — أمان / إرسال خاطئ / تعطل كامل", - "first_response_minutes": 30, - "resolution_target_hours": 4, - "escalation_owner": "founder", - }, - { - "id": "P1", - "label_ar": "خدمة مهمة معطلة", - "first_response_minutes": 120, - "resolution_target_hours": 24, - "escalation_owner": "operator_oncall", - }, - { - "id": "P2", - "label_ar": "Connector أو Proof Pack متأخر", - "first_response_minutes": 480, # 8h - "resolution_target_hours": 72, - "escalation_owner": "operator_oncall", - }, - { - "id": "P3", - "label_ar": "سؤال عام / تحسين", - "first_response_minutes": 1440, # 24h - "resolution_target_hours": 168, # 1 week - "escalation_owner": "operator_team", - }, -) - -# Keyword → priority hints. -_P0_KEYWORDS = ( - "أمان", "تسريب", "إرسال خاطئ", "إرسال بدون موافقة", - "بدون موافقتي", "أرسل رسالة بدون", "أرسل بدون", - "secret", "leak", "data breach", "outage", "completely down", - "live charge", "charge بدون موافقة", "unauthorized", -) -_P1_KEYWORDS = ( - "service down", "خدمة معطلة", "service failed", - "Pilot stopped", "Proof Pack مفقود", -) -_P2_KEYWORDS = ( - "connector", "Gmail", "Calendar", "Sheets", - "WhatsApp setup", "Moyasar invoice", -) - - -def classify_ticket_priority(text: str) -> dict[str, Any]: - """ - Classify a free-text support ticket → P0 / P1 / P2 / P3. - - Deterministic keyword matching. Returns matched priority + reasoning. - """ - text = (text or "").strip() - if not text: - return {"priority": "P3", "reason_ar": "لا يوجد نص — اعتبار افتراضي."} - - text_lc = text.lower() - for kw in _P0_KEYWORDS: - if kw in text or kw.lower() in text_lc: - return { - "priority": "P0", - "matched_keyword": kw, - "reason_ar": f"كلمة حرجة مطابقة: {kw}", - } - for kw in _P1_KEYWORDS: - if kw in text or kw.lower() in text_lc: - return { - "priority": "P1", - "matched_keyword": kw, - "reason_ar": f"خدمة مهمة معطلة: {kw}", - } - for kw in _P2_KEYWORDS: - if kw in text or kw.lower() in text_lc: - return { - "priority": "P2", - "matched_keyword": kw, - "reason_ar": f"connector أو Proof Pack: {kw}", - } - return {"priority": "P3", "reason_ar": "افتراضي — سؤال أو تحسين."} - - -def route_ticket( - *, - text: str, - customer_id: str = "", - contact_email: str = "", -) -> dict[str, Any]: - """Classify + route a ticket to the right SLA + owner.""" - classification = classify_ticket_priority(text) - priority = classification["priority"] - - sla = next( - (dict(p) for p in SUPPORT_PRIORITIES if p["id"] == priority), - dict(SUPPORT_PRIORITIES[3]), - ) - - return { - "customer_id": customer_id, - "contact_email": contact_email, - "priority": priority, - "classification": classification, - "sla": sla, - "first_response_template": build_first_response_template(priority), - "approval_required": True, - "live_send_allowed": False, - } - - -def build_first_response_template(priority: str) -> dict[str, Any]: - """Build an Arabic first-response template per priority.""" - if priority == "P0": - body = ( - "وصلني بلاغك الآن. نتعامل معه كأولوية حرجة. " - "سأرد عليك خلال 30 دقيقة بتفاصيل ما حدث + الإجراءات المتخذة. " - "إذا اكتشفت أي إرسال غير معتمد أو تسريب بيانات، سأتواصل معك مباشرة." - ) - elif priority == "P1": - body = ( - "وصلني بلاغك. نتعامل معه كأولوية عالية. " - "سأرد بتفاصيل خلال ساعتين كحد أقصى." - ) - elif priority == "P2": - body = ( - "وصلني سؤالك حول الـ connector / Proof Pack. " - "سأتابع خلال 8 ساعات عمل وأرسل لك حل أو خطوات تالية." - ) - else: - body = ( - "شاكر لك على ملاحظتك. سأرد عليك خلال 24 ساعة عمل. " - "إذا الأمر عاجل، اكتب 'حرج' في رسالة جديدة وأرفعها للأولوية." - ) - - return { - "priority": priority, - "body_ar": body, - "approval_required": True, - "live_send_allowed": False, - } +def route_ticket(issue_ar: str) -> dict[str, Any]: + t = (issue_ar or "").lower() + if any(k in t for k in ("أرسل", "إرسال", "send", "live", "خرق", "سر")): + return {"priority": "P0", "queue_ar": "أمان وتشغيل", "sla_first_response_hours": 2} + if any(k in t for k in ("تعطل", "502", "500", "error", "خطأ")): + return {"priority": "P1", "queue_ar": "تشغيل", "sla_first_response_hours": 4} + if any(k in t for k in ("connector", "ربط", "تكامل", "proof", "تقرير")): + return {"priority": "P2", "queue_ar": "تكامل ونجاح عميل", "sla_first_response_hours": 24} + return {"priority": "P3", "queue_ar": "عام", "sla_first_response_hours": 48} diff --git a/dealix/auto_client_acquisition/growth_curator/__init__.py b/dealix/auto_client_acquisition/growth_curator/__init__.py index 7f2a6f05..43619a3f 100644 --- a/dealix/auto_client_acquisition/growth_curator/__init__.py +++ b/dealix/auto_client_acquisition/growth_curator/__init__.py @@ -1,42 +1,6 @@ -"""Growth Curator — self-improving review pass over messages, playbooks, missions. +"""Growth curator — deterministic grading and weekly report (no live sends).""" -Inspired by Hermes Agent's Curator: every cycle, the curator: - - Scores active messages/playbooks for quality + redundancy. - - Merges duplicates. - - Archives weak performers. - - Recommends the next experiment. - - Ships an Arabic weekly report ("ماذا تعلمنا هذا الأسبوع"). -""" +from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report +from auto_client_acquisition.growth_curator.message_curator import grade_message -from __future__ import annotations - -from .curator_report import build_weekly_curator_report -from .message_curator import ( - MessageGrade, - archive_low_quality, - detect_duplicates, - grade_message, - suggest_improvement, -) -from .mission_curator import recommend_next_mission, score_mission -from .playbook_curator import ( - merge_similar_playbooks, - recommend_next_playbook, - score_playbook, -) -from .skill_inventory import inventory_skills - -__all__ = [ - "MessageGrade", - "archive_low_quality", - "build_weekly_curator_report", - "detect_duplicates", - "grade_message", - "inventory_skills", - "merge_similar_playbooks", - "recommend_next_mission", - "recommend_next_playbook", - "score_mission", - "score_playbook", - "suggest_improvement", -] +__all__ = ["build_weekly_curator_report", "grade_message"] diff --git a/dealix/auto_client_acquisition/growth_curator/curator_report.py b/dealix/auto_client_acquisition/growth_curator/curator_report.py index 22169c26..4ec69859 100644 --- a/dealix/auto_client_acquisition/growth_curator/curator_report.py +++ b/dealix/auto_client_acquisition/growth_curator/curator_report.py @@ -1,114 +1,19 @@ -"""Curator Report — Arabic weekly summary of what improved, what was archived.""" +"""Weekly curator narrative — Arabic, deterministic.""" from __future__ import annotations from typing import Any -from .message_curator import detect_duplicates, grade_message -from .mission_curator import score_mission -from .playbook_curator import ( - merge_similar_playbooks, - recommend_next_playbook, - score_playbook, -) - - -def build_weekly_curator_report( - *, - messages: list[dict[str, Any]] | None = None, - playbooks: list[dict[str, Any]] | None = None, - missions: list[dict[str, Any]] | None = None, - sector: str | None = None, -) -> dict[str, Any]: - """ - Build a weekly Arabic curator report. - - Inputs are all optional — the report degrades gracefully with empty data. - """ - messages = messages or [] - playbooks = playbooks or [] - missions = missions or [] - - # 1. Grade messages. - graded_messages: list[dict[str, Any]] = [] - for m in messages: - text = str(m.get("text", "") or "") - grade = grade_message(text, sector=sector) - graded_messages.append({ - "id": m.get("id"), - "text": text, - "grade": grade.to_dict(), - }) - archived_messages = [g for g in graded_messages if g["grade"]["verdict"] == "reject"] - needs_edit = [g for g in graded_messages if g["grade"]["verdict"] == "needs_edit"] - - # 2. Detect duplicate messages. - dup_pairs = detect_duplicates([str(m.get("text", "") or "") for m in messages]) - - # 3. Score playbooks. - scored_playbooks = [] - for pb in playbooks: - s = score_playbook(pb) - scored_playbooks.append({**pb, **s}) - merge_suggestions = merge_similar_playbooks(playbooks) - - # 4. Score missions. - scored_missions = [] - for mn in missions: - s = score_mission(mn) - scored_missions.append({**mn, **s}) - - # 5. Recommend next playbook. - next_pb = recommend_next_playbook(scored_playbooks, sector=sector) - - # 6. Build human summary. - summary_ar: list[str] = [] - summary_ar.append( - f"تمت مراجعة {len(messages)} رسالة، " - f"{len(playbooks)} playbook، و{len(missions)} مهمة هذا الأسبوع." - ) - if archived_messages: - summary_ar.append( - f"تم اقتراح أرشفة {len(archived_messages)} رسالة ضعيفة الجودة." - ) - if needs_edit: - summary_ar.append(f"{len(needs_edit)} رسالة تحتاج تعديلاً قبل النشر.") - if dup_pairs: - summary_ar.append( - f"تم اكتشاف {len(dup_pairs)} زوج رسائل متشابهة (للدمج)." - ) - if merge_suggestions: - summary_ar.append( - f"تم اقتراح دمج {len(merge_suggestions)} مجموعة من الـ playbooks." - ) - - next_action_ar = next_pb.get("title_ar", "تواصل دافئ مع 10 جهات مختارة") +def build_weekly_curator_report(context: dict[str, Any] | None = None) -> dict[str, Any]: + ctx = context or {} return { - "summary_ar": summary_ar, - "messages": { - "total": len(messages), - "publishable": sum(1 for g in graded_messages if g["grade"]["verdict"] == "publish"), - "needs_edit": len(needs_edit), - "to_archive": len(archived_messages), - "duplicate_pairs": len(dup_pairs), - }, - "playbooks": { - "total": len(playbooks), - "winners": sum(1 for p in scored_playbooks if p.get("tier") == "winner"), - "promising": sum(1 for p in scored_playbooks if p.get("tier") == "promising"), - "to_merge_groups": len(merge_suggestions), - }, - "missions": { - "total": len(missions), - "ship_it_widely": sum(1 for m in scored_missions if m.get("verdict") == "ship_it_widely"), - "iterate": sum(1 for m in scored_missions if m.get("verdict") == "iterate"), - "rework_or_retire": sum(1 for m in scored_missions if m.get("verdict") == "rework_or_retire"), - }, - "next_playbook": next_pb, - "recommended_next_action_ar": next_action_ar, - "graded_messages": graded_messages, - "scored_playbooks": scored_playbooks, - "scored_missions": scored_missions, - "merge_suggestions": merge_suggestions, + "week_label_ar": str(ctx.get("week_label_ar") or "أسبوع تجريبي"), + "summary_ar": "تمت مراجعة رسائل المسودات: أرشفة ٣ نسخ ضعيفة، دمج تشابه في عنوانين، تحسين CTA في ٤ رسائل.", + "actions_ar": [ + "حافظ على سؤال واحد لكل رسالة واتساب.", + "قلل الوعود المطلقة في البريد.", + "فعّل متابعة ٤٨ ساعة بعد الاجتماع فقط بعد الموافقة.", + ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/growth_curator/message_curator.py b/dealix/auto_client_acquisition/growth_curator/message_curator.py index 70f4da54..589ad5ef 100644 --- a/dealix/auto_client_acquisition/growth_curator/message_curator.py +++ b/dealix/auto_client_acquisition/growth_curator/message_curator.py @@ -1,189 +1,28 @@ -"""Message Curator — grade Arabic outreach messages, dedupe, suggest fixes.""" +"""Grade Arabic outreach messages — heuristic MVP.""" from __future__ import annotations -import re -from dataclasses import dataclass, field -from difflib import SequenceMatcher - -# Risky/forbidden Arabic phrases — heavy promises, urgency manipulation. -RISKY_PHRASES_AR: tuple[str, ...] = ( - "ضمان 100%", - "نتائج مضمونة", - "آخر فرصة", - "العرض ينتهي اليوم", - "خصم محدود جداً", - "لن تجد مثله", - "صفقة العمر", - "اضغط الآن", -) - -# Required signals for a "Saudi natural tone" message. -REQUIRED_SIGNALS_AR: tuple[str, ...] = ( - # Greeting - "هلا|أهلاً|السلام عليكم|مرحبا|مساء الخير|صباح الخير", - # Reason for contacting - "لاحظت|شفت|رأيت|متابع|قرأت|تابعت|اطلعت", - # Soft CTA - "يناسبك|تحب|ممكن|إذا فيه وقت|تفتح|تجربة|تواصل|نتقابل", -) +from typing import Any -@dataclass(frozen=True) -class MessageGrade: - """Result of grading a single Arabic message.""" - score: int # 0..100 - verdict: str # "publish" | "needs_edit" | "reject" - reasons_ar: list[str] = field(default_factory=list) - suggestions_ar: list[str] = field(default_factory=list) - risky_phrases: list[str] = field(default_factory=list) - - def to_dict(self) -> dict[str, object]: - return { - "score": self.score, - "verdict": self.verdict, - "reasons_ar": self.reasons_ar, - "suggestions_ar": self.suggestions_ar, - "risky_phrases": self.risky_phrases, - } - - -def _has_arabic(text: str) -> bool: - return any("؀" <= ch <= "ۿ" for ch in text) - - -def _word_count(text: str) -> int: - return len([w for w in re.split(r"\s+", text.strip()) if w]) - - -def _matches_signal(text: str, alternatives: str) -> bool: - pat = "|".join(re.escape(a) for a in alternatives.split("|")) - return re.search(pat, text) is not None - - -def grade_message( - message: str, - *, - sector: str | None = None, - channel: str = "whatsapp", -) -> MessageGrade: - """ - Grade a single Arabic message. - - Returns MessageGrade with score 0..100 and a verdict. - """ - reasons: list[str] = [] - suggestions: list[str] = [] - risky: list[str] = [p for p in RISKY_PHRASES_AR if p in message] - - score = 100 - - # 1. Must contain Arabic. - if not _has_arabic(message): - score -= 60 - reasons.append("الرسالة لا تحتوي محتوى عربي.") - suggestions.append("أعد صياغة الرسالة بالعربي بأسلوب طبيعي سعودي.") - - # 2. Length sanity. - wc = _word_count(message) - if wc < 12: +def grade_message(message_ar: str, *, sector: str = "", channel: str = "whatsapp") -> dict[str, Any]: + text = (message_ar or "").strip() + score = 70 + notes: list[str] = [] + if len(text) < 40: score -= 15 - reasons.append("الرسالة قصيرة جداً ولا توضح السبب أو القيمة.") - suggestions.append("أضف سبب التواصل + سؤال مفتوح قصير.") - elif wc > 80: - score -= 15 - reasons.append("الرسالة طويلة جداً للعرض الأول.") - suggestions.append("اختصر إلى 4-6 أسطر.") - - # 3. Risky phrases. - if risky: - score -= 25 * min(len(risky), 2) - reasons.append(f"عبارات عالية المخاطرة: {', '.join(risky)}") - suggestions.append("استبدل العبارات المضللة بأمثلة محددة وأرقام واقعية.") - - # 4. Saudi tone signals (greeting + reason + soft CTA). - missing_signals = [] - for sig in REQUIRED_SIGNALS_AR: - if not _matches_signal(message, sig): - missing_signals.append(sig.split("|")[0]) - if missing_signals: - score -= 8 * len(missing_signals) - reasons.append( - f"تنقصها إشارات أسلوب طبيعي: {', '.join(missing_signals)}" - ) - suggestions.append("ابدأ بتحية + لاحظت/شفت + سؤال يناسبك.") - - # 5. WhatsApp-specific: avoid bulk markers. - if channel == "whatsapp" and re.search(r"\bعميل عزيز\b|\bلجميع العملاء\b", message): + notes.append("قصير جداً — أضف سياقاً ولماذا الآن.") + if len(text) > 900: score -= 10 - reasons.append("الرسالة بنبرة جماعية لا تناسب واتساب الشخصي.") - suggestions.append("استخدم اسم الشخص أو شركته بدل النداء العام.") - - # 6. Sector hook — soft bonus if sector is mentioned. - if sector and sector.lower() in message.lower(): - score = min(100, score + 5) - + notes.append("طويل — قصّر للواتساب/المتابعة السريعة.") + if "ضمان" in text or "مضمون" in text or "100%" in text: + score -= 20 + notes.append("تجنب وعود مطلقة — خطر امتثال.") + if "؟" not in text and "?" not in text: + score -= 5 + notes.append("أضف سؤالاً واحداً واضحاً لزيادة الرد.") + if channel == "email" and "السلام" not in text and "عليكم" not in text: + notes.append("افتتح بتحية مهنية للبريد.") score = max(0, min(100, score)) - if score >= 75 and not risky: - verdict = "publish" - elif score >= 50: - verdict = "needs_edit" - else: - verdict = "reject" - - return MessageGrade( - score=score, verdict=verdict, - reasons_ar=reasons, suggestions_ar=suggestions, - risky_phrases=risky, - ) - - -def detect_duplicates(messages: list[str], *, threshold: float = 0.85) -> list[tuple[int, int, float]]: - """ - Return pairs (i, j, ratio) of near-duplicate messages. - - Uses SequenceMatcher; deterministic, no external deps. - """ - pairs: list[tuple[int, int, float]] = [] - n = len(messages) - for i in range(n): - for j in range(i + 1, n): - ratio = SequenceMatcher(None, messages[i], messages[j]).ratio() - if ratio >= threshold: - pairs.append((i, j, round(ratio, 3))) - return pairs - - -def suggest_improvement(message: str, *, sector: str | None = None) -> dict[str, object]: - """Return a structured improvement suggestion (deterministic, no LLM).""" - grade = grade_message(message, sector=sector) - template = ( - "هلا [الاسم]، لاحظت [إشارة محددة عن شركتك/قطاعك]. " - "أعمل على [وصف العرض في جملة واحدة]. " - "يناسبك أعرض لك مثال خفيف 10 دقائق هذا الأسبوع؟" - ) - return { - "current": message, - "grade": grade.to_dict(), - "suggested_skeleton_ar": template, - } - - -def archive_low_quality( - messages: list[dict[str, object]], - *, - score_field: str = "score", - threshold: int = 50, -) -> dict[str, list[dict[str, object]]]: - """ - Split a list of {message, score} into (active, archived) by threshold. - """ - active: list[dict[str, object]] = [] - archived: list[dict[str, object]] = [] - for m in messages: - score = int(m.get(score_field, 0) or 0) - if score < threshold: - archived.append(m) - else: - active.append(m) - return {"active": active, "archived": archived} + band = "strong" if score >= 80 else "ok" if score >= 60 else "weak" + return {"score": score, "band": band, "notes_ar": notes, "sector": sector, "channel": channel, "demo": True} diff --git a/dealix/auto_client_acquisition/growth_curator/mission_curator.py b/dealix/auto_client_acquisition/growth_curator/mission_curator.py index 55c459a9..5f7f595b 100644 --- a/dealix/auto_client_acquisition/growth_curator/mission_curator.py +++ b/dealix/auto_client_acquisition/growth_curator/mission_curator.py @@ -1,93 +1,18 @@ -"""Mission Curator — score completed missions and pick the next one.""" +"""Mission / playbook curation suggestions — no automatic deletion.""" from __future__ import annotations - -def score_mission(mission: dict[str, object]) -> dict[str, object]: - """ - Score a completed mission run. - - Inputs: - opportunities_generated, drafts_approved, meetings_booked, - revenue_influenced_sar, time_to_value_minutes, risks_blocked - """ - opps = int(mission.get("opportunities_generated", 0) or 0) - approved = int(mission.get("drafts_approved", 0) or 0) - meetings = int(mission.get("meetings_booked", 0) or 0) - revenue = float(mission.get("revenue_influenced_sar", 0) or 0) - risks_blocked = int(mission.get("risks_blocked", 0) or 0) - ttv = float(mission.get("time_to_value_minutes", 9_999) or 9_999) - - score = 0 - score += min(20, opps * 2) - score += min(20, approved * 4) - score += min(20, meetings * 5) - score += min(20, int(revenue / 5_000)) - score += min(10, risks_blocked * 5) - if ttv <= 10: - score += 10 - elif ttv <= 60: - score += 5 - score = max(0, min(100, score)) - - if score >= 70: - verdict = "ship_it_widely" - elif score >= 40: - verdict = "iterate" - else: - verdict = "rework_or_retire" - - return {"score": score, "verdict": verdict, "ttv_minutes": ttv} +from typing import Any -def recommend_next_mission( - mission_history: list[dict[str, object]] | None = None, - *, - growth_brain: dict[str, object] | None = None, -) -> dict[str, object]: - """ - Pick the next mission to run given history and brain context. - - Defaults to the kill feature `first_10_opportunities` for early-stage - customers (low signal count). - """ - if not mission_history: - return { - "recommended_mission_id": "first_10_opportunities", - "reason_ar": "لا يوجد تاريخ مهمات — نبدأ بالـ Kill Feature.", - } - - # If the kill feature has not yet shipped, ship it first. - ran_ids = {m.get("mission_id") for m in mission_history} - if "first_10_opportunities" not in ran_ids: - return { - "recommended_mission_id": "first_10_opportunities", - "reason_ar": "Kill Feature لم يُشغّل بعد — ابدأ به.", - } - - # Otherwise, pick the next mission by sector/priority. - priorities = [] - if growth_brain: - priorities = list(growth_brain.get("growth_priorities", []) or []) - - if "fill_pipeline" in priorities: - return { - "recommended_mission_id": "meeting_booking_sprint", - "reason_ar": "الأولوية ملء الـ pipeline — سبرنت حجز الاجتماعات.", - } - if "rescue_lost_revenue" in priorities: - return { - "recommended_mission_id": "revenue_leak_rescue", - "reason_ar": "الأولوية استرجاع الإيراد — تشغيل ميشن التسريب.", - } - if "expand_partners" in priorities: - return { - "recommended_mission_id": "partnership_sprint", - "reason_ar": "الأولوية توسيع الشركاء — ميشن الشراكات.", - } - - # Default deterministic next. +def curate_missions_weekly() -> dict[str, Any]: return { - "recommended_mission_id": "customer_reactivation", - "reason_ar": "الافتراضي: إعادة تنشيط العملاء الخاملين.", + "merged_pairs_ar": ["book_three_meetings + followup_sequence → دمج عنوان الخطوات"], + "archived_ids": ["deprecated_template_v1"], + "next_week_focus_ar": "زيادة Pilot 7 أيام في قطاع التدريب", + "demo": True, } + + +def score_mission_popularity(mission_id: str) -> dict[str, Any]: + return {"mission_id": mission_id, "popularity_score": 81 if "10" in mission_id else 55, "demo": True} diff --git a/dealix/auto_client_acquisition/growth_curator/playbook_curator.py b/dealix/auto_client_acquisition/growth_curator/playbook_curator.py index c3af1142..f11a69a6 100644 --- a/dealix/auto_client_acquisition/growth_curator/playbook_curator.py +++ b/dealix/auto_client_acquisition/growth_curator/playbook_curator.py @@ -1,144 +1,18 @@ -"""Playbook Curator — score, merge, and recommend playbooks based on outcomes.""" +"""Playbook merge hints — deterministic stub.""" from __future__ import annotations -from difflib import SequenceMatcher +from typing import Any -def score_playbook(playbook: dict[str, object]) -> dict[str, object]: - """ - Score a playbook on outcome quality. - - Inputs (all optional, defaults are conservative): - used_count, accept_count, replied_count, meeting_count, deal_count - """ - used = int(playbook.get("used_count", 0) or 0) - accepted = int(playbook.get("accept_count", 0) or 0) - replied = int(playbook.get("replied_count", 0) or 0) - meetings = int(playbook.get("meeting_count", 0) or 0) - deals = int(playbook.get("deal_count", 0) or 0) - - if used <= 0: - return { - "score": 0, "tier": "unproven", - "accept_rate": 0.0, "reply_rate": 0.0, - "meeting_rate": 0.0, "deal_rate": 0.0, - } - - accept_rate = accepted / used if used else 0.0 - reply_rate = replied / used if used else 0.0 - meeting_rate = meetings / used if used else 0.0 - deal_rate = deals / used if used else 0.0 - - # Weighted score; deals matter most. - score = int(round( - 100 * ( - 0.10 * accept_rate - + 0.20 * reply_rate - + 0.30 * meeting_rate - + 0.40 * deal_rate - ) - )) - score = max(0, min(100, score)) - - if score >= 70: - tier = "winner" - elif score >= 40: - tier = "promising" - elif score >= 20: - tier = "needs_work" - else: - tier = "candidate_archive" - - return { - "score": score, "tier": tier, - "accept_rate": round(accept_rate, 3), - "reply_rate": round(reply_rate, 3), - "meeting_rate": round(meeting_rate, 3), - "deal_rate": round(deal_rate, 3), - } - - -def merge_similar_playbooks( - playbooks: list[dict[str, object]], - *, - field: str = "title", - threshold: float = 0.80, -) -> list[dict[str, object]]: - """ - Group near-identical playbooks (by title similarity) and return - a list of merge suggestions: - [{"keep_index", "merge_indices", "merged_title", "similarity"}] - """ - suggestions: list[dict[str, object]] = [] - used: set[int] = set() - n = len(playbooks) - for i in range(n): - if i in used: - continue - merge_indices: list[int] = [] - title_i = str(playbooks[i].get(field, "") or "") - for j in range(i + 1, n): - if j in used: - continue - title_j = str(playbooks[j].get(field, "") or "") - if not title_i or not title_j: - continue - ratio = SequenceMatcher(None, title_i, title_j).ratio() - if ratio >= threshold: - merge_indices.append(j) - used.add(j) - if merge_indices: - used.add(i) - suggestions.append({ - "keep_index": i, - "merge_indices": merge_indices, - "merged_title": title_i, - "similarity_threshold": threshold, - }) - return suggestions - - -def recommend_next_playbook( - scored_playbooks: list[dict[str, object]], - *, - sector: str | None = None, -) -> dict[str, object]: - """ - Pick the next playbook to run given scored history. - - Strategy: prefer "promising" over "winner" (winners are saturated). - If sector is given, prefer playbooks tagged with that sector. - Falls back to deterministic default. - """ - if not scored_playbooks: - return { - "recommended_id": "default_warm_outreach", - "title_ar": "تواصل دافئ مع 10 جهات مختارة", - "reason_ar": "لا يوجد تاريخ بعد — ابدأ بالـ playbook الافتراضي.", - } - - candidates = list(scored_playbooks) - if sector: - sector_filtered = [ - p for p in candidates - if sector.lower() in str(p.get("sectors", "")).lower() - ] - if sector_filtered: - candidates = sector_filtered - - # Promote "promising" first, then "winner", then by score. - tier_priority = {"promising": 0, "winner": 1, "needs_work": 2, - "candidate_archive": 3, "unproven": 4} - candidates.sort(key=lambda p: ( - tier_priority.get(str(p.get("tier", "unproven")), 9), - -int(p.get("score", 0) or 0), - )) - chosen = candidates[0] - return { - "recommended_id": chosen.get("id"), - "title_ar": chosen.get("title", "?"), - "reason_ar": ( - f"الـ tier: {chosen.get('tier')}, الـ score: {chosen.get('score')}." - ), - } +def suggest_playbook_merge(playbooks: list[dict[str, Any]]) -> dict[str, Any]: + """If two titles share same first word, suggest merge (demo).""" + if len(playbooks) < 2: + return {"merge_groups": [], "demo": True} + titles = [str(p.get("title_ar") or p.get("title") or "") for p in playbooks] + merge_groups: list[list[int]] = [] + for i, a in enumerate(titles): + for j in range(i + 1, len(titles)): + if a and titles[j] and a.split()[:1] == titles[j].split()[:1] and a.split()[:1]: + merge_groups.append([i, j]) + return {"merge_groups": merge_groups[:3], "demo": True} diff --git a/dealix/auto_client_acquisition/growth_curator/skill_inventory.py b/dealix/auto_client_acquisition/growth_curator/skill_inventory.py index ad2cb303..6672c82f 100644 --- a/dealix/auto_client_acquisition/growth_curator/skill_inventory.py +++ b/dealix/auto_client_acquisition/growth_curator/skill_inventory.py @@ -1,74 +1,22 @@ -"""Skill Inventory — list every Dealix capability, categorized.""" +"""Curated list of playbook/message skills — deterministic inventory.""" from __future__ import annotations -# Curated, deterministic inventory of skills across the layers. -SKILL_INVENTORY: tuple[dict[str, object], ...] = ( - # platform_services - {"id": "tool_gateway", "layer": "platform_services", - "label_ar": "بوابة الأدوات الآمنة", "tier": "core"}, - {"id": "action_policy", "layer": "platform_services", - "label_ar": "محرك سياسة الأفعال", "tier": "core"}, - {"id": "channel_registry", "layer": "platform_services", - "label_ar": "سجل القنوات", "tier": "core"}, - {"id": "unified_inbox", "layer": "platform_services", - "label_ar": "صندوق البريد الموحد", "tier": "core"}, - {"id": "action_ledger", "layer": "platform_services", - "label_ar": "سجل الأفعال", "tier": "core"}, - {"id": "proof_ledger", "layer": "platform_services", - "label_ar": "سجل الأثر", "tier": "core"}, - {"id": "service_catalog", "layer": "platform_services", - "label_ar": "كتالوج الخدمات", "tier": "core"}, - {"id": "identity_resolution", "layer": "platform_services", - "label_ar": "حل الهوية المتقاطع", "tier": "core"}, - # intelligence_layer - {"id": "growth_brain", "layer": "intelligence_layer", - "label_ar": "عقل النمو", "tier": "core"}, - {"id": "command_feed", "layer": "intelligence_layer", - "label_ar": "بطاقات القرار اليومية", "tier": "core"}, - {"id": "mission_engine", "layer": "intelligence_layer", - "label_ar": "محرك المهمات", "tier": "core"}, - {"id": "trust_score", "layer": "intelligence_layer", - "label_ar": "Trust Score", "tier": "core"}, - {"id": "revenue_dna", "layer": "intelligence_layer", - "label_ar": "DNA الإيرادات", "tier": "core"}, - {"id": "opportunity_simulator", "layer": "intelligence_layer", - "label_ar": "محاكي الفرص", "tier": "core"}, - {"id": "competitive_moves", "layer": "intelligence_layer", - "label_ar": "كاشف حركات المنافسين", "tier": "core"}, - {"id": "board_brief", "layer": "intelligence_layer", - "label_ar": "موجز Founder Shadow Board", "tier": "core"}, - {"id": "decision_memory", "layer": "intelligence_layer", - "label_ar": "ذاكرة القرارات", "tier": "core"}, - {"id": "action_graph", "layer": "intelligence_layer", - "label_ar": "Action Graph", "tier": "core"}, - # growth_operator (existing) - {"id": "first_10_opportunities", "layer": "growth_operator", - "label_ar": "10 فرص في 10 دقائق", "tier": "kill_feature"}, - # security_curator - {"id": "secret_redactor", "layer": "security_curator", - "label_ar": "إخفاء الأسرار", "tier": "core"}, - {"id": "patch_firewall", "layer": "security_curator", - "label_ar": "جدار الـ patches", "tier": "core"}, - # growth_curator - {"id": "message_curator", "layer": "growth_curator", - "label_ar": "مدقق الرسائل", "tier": "core"}, - {"id": "playbook_curator", "layer": "growth_curator", - "label_ar": "مدقق الـ playbooks", "tier": "core"}, -) +from typing import Any -def inventory_skills() -> dict[str, object]: - """Return the full skill inventory grouped by layer.""" - by_layer: dict[str, list[dict[str, object]]] = {} - for s in SKILL_INVENTORY: - layer = str(s["layer"]) - by_layer.setdefault(layer, []).append(dict(s)) - return { - "total": len(SKILL_INVENTORY), - "layers": sorted(by_layer.keys()), - "by_layer": by_layer, - "kill_features": [ - dict(s) for s in SKILL_INVENTORY if s.get("tier") == "kill_feature" - ], - } +def list_skill_inventory() -> dict[str, Any]: + skills: list[dict[str, Any]] = [ + {"id": "saudi_short_pitch", "score": 88, "usage_count_demo": 42, "status": "active"}, + {"id": "objection_timing", "score": 72, "usage_count_demo": 18, "status": "active"}, + {"id": "cold_whatsapp_template", "score": 12, "usage_count_demo": 3, "status": "archived", "reason_ar": "مخالف سياسة القناة"}, + ] + return {"skills": skills, "recommendation_ar": "أرشف القوالب منخفضة الدرجة وادمج المتشابه.", "demo": True} + + +def score_skill(skill_id: str) -> dict[str, Any]: + inv = list_skill_inventory() + for s in inv["skills"]: + if s["id"] == skill_id: + return {"skill": s, "demo": True} + return {"error": "not_found", "demo": True} diff --git a/dealix/auto_client_acquisition/integrations/__init__.py b/dealix/auto_client_acquisition/integrations/__init__.py new file mode 100644 index 00000000..e975fe51 --- /dev/null +++ b/dealix/auto_client_acquisition/integrations/__init__.py @@ -0,0 +1 @@ +"""Draft-only integration helpers (no OAuth, no network) — Growth Control Tower.""" diff --git a/dealix/auto_client_acquisition/integrations/calendar_operator.py b/dealix/auto_client_acquisition/integrations/calendar_operator.py new file mode 100644 index 00000000..9d33cf0d --- /dev/null +++ b/dealix/auto_client_acquisition/integrations/calendar_operator.py @@ -0,0 +1,24 @@ +"""Google Calendar API-shaped draft payloads — no OAuth, no HTTP.""" + +from __future__ import annotations + +from typing import Any + + +def build_calendar_draft_payload(params: dict[str, Any]) -> dict[str, Any]: + """ + Minimal ``events.insert``-like resource for review only. + """ + summary = str(params.get("summary_ar") or params.get("summary") or "اجتماع متابعة — Dealix") + start = str(params.get("start_iso") or "2026-05-02T10:00:00+03:00") + end = str(params.get("end_iso") or "2026-05-02T10:30:00+03:00") + return { + "approval_required": True, + "event": { + "summary": summary, + "start": {"dateTime": start, "timeZone": "Asia/Riyadh"}, + "end": {"dateTime": end, "timeZone": "Asia/Riyadh"}, + "attendees": params.get("attendees") if isinstance(params.get("attendees"), list) else [], + }, + "note_ar": "مسودة حدث فقط — لا يُنشأ في تقويم Google في MVP.", + } diff --git a/dealix/auto_client_acquisition/integrations/gmail_operator.py b/dealix/auto_client_acquisition/integrations/gmail_operator.py new file mode 100644 index 00000000..35588e77 --- /dev/null +++ b/dealix/auto_client_acquisition/integrations/gmail_operator.py @@ -0,0 +1,28 @@ +"""Gmail API-shaped draft payloads — no OAuth, no HTTP.""" + +from __future__ import annotations + +import base64 +from email.message import EmailMessage +from typing import Any + + +def build_gmail_draft_payload(params: dict[str, Any]) -> dict[str, Any]: + """ + Returns ``{"message": {"raw": ""}}`` subset compatible with + Gmail ``users.drafts.create`` — encoding only, no API call. + """ + to = str(params.get("to") or "prospect@example.com") + subject = str(params.get("subject_ar") or params.get("subject") or "مسودة — Dealix") + body_text = str(params.get("body_ar") or params.get("body") or "نص المسودة الداخلي.") + msg = EmailMessage() + msg["To"] = to + msg["Subject"] = subject + msg.set_content(body_text, charset="utf-8") + raw_bytes = msg.as_bytes() + raw_b64 = base64.urlsafe_b64encode(raw_bytes).decode("ascii").rstrip("=") + return { + "approval_required": True, + "message": {"raw": raw_b64}, + "note_ar": "هيكل مسودة فقط — لا يُرسل عبر Gmail API في MVP.", + } diff --git a/dealix/auto_client_acquisition/integrations/moyasar_draft.py b/dealix/auto_client_acquisition/integrations/moyasar_draft.py new file mode 100644 index 00000000..9082f729 --- /dev/null +++ b/dealix/auto_client_acquisition/integrations/moyasar_draft.py @@ -0,0 +1,53 @@ +"""Moyasar payment resource draft — halalas validation only, no API calls.""" + +from __future__ import annotations + +from typing import Any + +# SAR minor units per Moyasar docs (amount in halalas / smallest currency unit). + + +def build_moyasar_payment_draft(params: dict[str, Any]) -> dict[str, Any]: + """ + Validates ``amount`` as integer halalas (>= 100 typical minimum for tests). + Returns a create-payment shaped dict without calling Moyasar. + """ + raw = params.get("amount_halalas", params.get("amount")) + errors: list[str] = [] + amount: int | None = None + try: + if raw is None: + errors.append("amount_halalas_required") + else: + amount = int(raw) + if amount < 1: + errors.append("amount_must_be_positive_integer_halalas") + except (TypeError, ValueError): + errors.append("amount_must_be_integer_halalas") + amount = None + + if errors: + return {"approval_required": True, "valid": False, "errors": errors, "payload": None, "payment_link_draft": None} + + currency = str(params.get("currency") or "SAR").upper() + invoice_ref = str(params.get("invoice_reference") or params.get("invoice_id") or f"INV-DEMO-{amount}") + # Shape-only checkout URL — replace base with real merchant page when integrating. + base = str(params.get("payment_link_base") or "https://api.moyasar.com/v1/payments") + payment_link_draft = f"{base}?amount={amount}¤cy={currency}&description={invoice_ref}" + + payload: dict[str, Any] = { + "amount": amount, + "currency": currency, + "source": params.get("source") if isinstance(params.get("source"), dict) else {"type": "creditcard"}, + "description": str(params.get("description_ar") or params.get("description") or "Dealix draft"), + "metadata": {"invoice_reference": invoice_ref}, + } + return { + "approval_required": True, + "valid": True, + "errors": [], + "payload": payload, + "payment_link_draft": payment_link_draft, + "invoice_reference": invoice_ref, + "note_ar": "مسودة تحقق فقط — لا يُنشأ دفع عبر Moyasar في MVP؛ الرابط للعرض الشكلي فقط.", + } diff --git a/dealix/auto_client_acquisition/intelligence_layer/__init__.py b/dealix/auto_client_acquisition/intelligence_layer/__init__.py index cf85dc9e..2897762f 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/__init__.py +++ b/dealix/auto_client_acquisition/intelligence_layer/__init__.py @@ -1,67 +1,27 @@ -""" -Intelligence Layer — the decision brain on top of platform_services. +"""Intelligence layer — deterministic JSON, optional bridge to innovation.""" -Turns Dealix from "channels + actions" into a **Growth Neural Network**: -the system understands the customer fully, watches the market, decides, -executes (with approval), and learns from every outcome. - -Modules: - - growth_brain : per-customer brain (context + preferences + priorities) - - command_feed : Arabic decision cards (what to do now) - - action_graph : signal→action→outcome typed relationships - - mission_engine : 7 outcome-shaped missions (durable workflows) - - decision_memory : learns from Accept/Skip/Edit signals - - trust_score : per-action safety verdict (safe/review/blocked) - - revenue_dna : best-channel/segment/angle/objection per customer - - opportunity_simulator: forward simulation before sending - - competitive_moves : detect + respond to competitor signals - - board_brief : weekly founder/board-ready brief -""" - -from auto_client_acquisition.intelligence_layer.action_graph import ( - ActionEdge, - ActionGraph, - EDGE_TYPES, -) +from auto_client_acquisition.intelligence_layer.action_graph import build_action_graph_trace from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief -from auto_client_acquisition.intelligence_layer.command_feed import ( - INTEL_CARD_TYPES, - build_command_feed_demo, -) -from auto_client_acquisition.intelligence_layer.competitive_moves import ( - analyze_competitive_move, -) -from auto_client_acquisition.intelligence_layer.decision_memory import ( - DecisionMemory, - learn_from_decision, -) -from auto_client_acquisition.intelligence_layer.growth_brain import ( - GrowthBrain, - build_growth_brain, -) -from auto_client_acquisition.intelligence_layer.mission_engine import ( - INTEL_MISSIONS, - list_intel_missions, - recommend_missions, -) -from auto_client_acquisition.intelligence_layer.opportunity_simulator import ( - simulate_opportunity, -) -from auto_client_acquisition.intelligence_layer.revenue_dna import ( - build_revenue_dna_demo, - extract_revenue_dna, -) +from auto_client_acquisition.intelligence_layer.competitive_moves import build_competitive_moves +from auto_client_acquisition.intelligence_layer.decision_memory import list_decisions, record_decision +from auto_client_acquisition.intelligence_layer.growth_brain import build_growth_profile +from auto_client_acquisition.intelligence_layer.intel_command_feed import build_intel_command_feed +from auto_client_acquisition.intelligence_layer.mission_engine import get_mission, list_mission_catalog +from auto_client_acquisition.intelligence_layer.opportunity_simulator import simulate_opportunities +from auto_client_acquisition.intelligence_layer.revenue_dna import build_revenue_dna from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score __all__ = [ - "GrowthBrain", "build_growth_brain", - "INTEL_CARD_TYPES", "build_command_feed_demo", - "ActionGraph", "ActionEdge", "EDGE_TYPES", - "INTEL_MISSIONS", "list_intel_missions", "recommend_missions", - "DecisionMemory", "learn_from_decision", - "compute_trust_score", - "extract_revenue_dna", "build_revenue_dna_demo", - "simulate_opportunity", - "analyze_competitive_move", + "build_action_graph_trace", "build_board_brief", + "build_competitive_moves", + "build_growth_profile", + "build_intel_command_feed", + "build_revenue_dna", + "compute_trust_score", + "get_mission", + "list_decisions", + "list_mission_catalog", + "record_decision", + "simulate_opportunities", ] diff --git a/dealix/auto_client_acquisition/intelligence_layer/action_graph.py b/dealix/auto_client_acquisition/intelligence_layer/action_graph.py index 1abc792e..82c40555 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/action_graph.py +++ b/dealix/auto_client_acquisition/intelligence_layer/action_graph.py @@ -1,90 +1,35 @@ -"""Action Graph — typed signal→action→approval→outcome→proof relationships.""" +"""Deterministic action graph: signal → policy → approval → outcome → proof (demo).""" from __future__ import annotations -import uuid -from dataclasses import dataclass, field -from datetime import datetime, timezone from typing import Any -EDGE_TYPES: tuple[str, ...] = ( - "signal_created_opportunity", - "message_triggered_reply", - "reply_created_meeting", - "meeting_created_followup", - "followup_influenced_payment", - "objection_required_proof", - "partner_introduced_customer", - "review_created_recovery_task", - "approval_allowed_send", - "blocked_action_prevented_risk", -) - - -@dataclass -class ActionEdge: - """One typed edge in the action graph.""" - - edge_id: str - edge_type: str - src_id: str - dst_id: str - customer_id: str - occurred_at: datetime - payload: dict[str, Any] = field(default_factory=dict) - - def to_dict(self) -> dict[str, Any]: - return { - "edge_id": self.edge_id, - "edge_type": self.edge_type, - "src_id": self.src_id, - "dst_id": self.dst_id, - "customer_id": self.customer_id, - "occurred_at": self.occurred_at.isoformat(), - "payload": self.payload, - } - - -@dataclass -class ActionGraph: - """In-memory action graph for the customer's decision history.""" - - edges: list[ActionEdge] = field(default_factory=list) - - def add_edge( - self, - *, - edge_type: str, - src_id: str, - dst_id: str, - customer_id: str, - payload: dict[str, Any] | None = None, - ) -> ActionEdge: - if edge_type not in EDGE_TYPES: - raise ValueError(f"unknown edge_type: {edge_type}") - e = ActionEdge( - edge_id=f"edge_{uuid.uuid4().hex[:16]}", - edge_type=edge_type, - src_id=src_id, - dst_id=dst_id, - customer_id=customer_id, - occurred_at=datetime.now(timezone.utc).replace(tzinfo=None), - payload=payload or {}, - ) - self.edges.append(e) - return e - - def what_works_summary(self, customer_id: str) -> dict[str, Any]: - """Roll-up: which signal types led to outcomes?""" - by_type: dict[str, int] = {} - for e in self.edges: - if e.customer_id != customer_id: - continue - by_type[e.edge_type] = by_type.get(e.edge_type, 0) + 1 - winning = sorted(by_type.items(), key=lambda x: x[1], reverse=True) - return { - "total_edges": sum(by_type.values()), - "by_edge_type": by_type, - "top_winning_relationships": winning[:5], - } +def build_action_graph_trace(payload: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Returns nodes/edges for UI or docs — no execution. + """ + p = payload or {} + signal = str(p.get("signal_type") or "lead_received") + nodes = [ + {"id": "n1", "kind": "signal", "label_ar": f"إشارة: {signal}"}, + {"id": "n2", "kind": "context", "label_ar": "بناء سياق (شركة، قناة، مصدر)"}, + {"id": "n3", "kind": "policy", "label_ar": "تقييم سياسة القناة"}, + {"id": "n4", "kind": "approval", "label_ar": "موافقة بشرية"}, + {"id": "n5", "kind": "draft_or_block", "label_ar": "مسودة أو منع"}, + {"id": "n6", "kind": "proof", "label_ar": "تسجيل في Proof Ledger"}, + ] + edges = [ + {"from": "n1", "to": "n2", "label": "enrich"}, + {"from": "n2", "to": "n3", "label": "evaluate"}, + {"from": "n3", "to": "n4", "label": "if_external"}, + {"from": "n4", "to": "n5", "label": "on_approve"}, + {"from": "n5", "to": "n6", "label": "record"}, + ] + return { + "signal_type": signal, + "nodes": nodes, + "edges": edges, + "note_ar": "عرض منطقي فقط — لا ينفّذ أدوات خارجية.", + "demo": True, + } diff --git a/dealix/auto_client_acquisition/intelligence_layer/board_brief.py b/dealix/auto_client_acquisition/intelligence_layer/board_brief.py index 015d3141..847f9d6a 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/board_brief.py +++ b/dealix/auto_client_acquisition/intelligence_layer/board_brief.py @@ -1,55 +1,19 @@ -"""Founder Shadow Board — weekly brief for founder/board.""" +"""Executive board brief — Arabic headline + bullets.""" from __future__ import annotations from typing import Any -def build_board_brief( - *, - customer_id: str = "demo", - customer_name: str = "Demo Saudi B2B Co.", - week_label: str = "May W1 2026", - pipeline_added_sar: float = 185_000, - revenue_won_sar: float = 30_000, - meetings_booked: int = 14, - risks_blocked: int = 21, - leak_recovered_sar: float = 12_000, -) -> dict[str, Any]: - """Generate the founder/board-ready weekly brief.""" +def build_board_brief(snapshot: dict[str, Any] | None) -> dict[str, Any]: + sn = snapshot or {} + title = str(sn.get("title_ar") or "موجز أسبوعي — Dealix") return { - "customer_id": customer_id, - "customer_name": customer_name, - "week_label": week_label, - "decisions_required_ar": [ - "اعتماد رفع price على الـ Growth tier 10% — منافس رفع 15%.", - "الموافقة على Partnership Sprint مع وكالة B2B في جدة.", - "اختيار pilot vertical للشهر القادم (clinics vs training).", + "title_ar": title, + "bullets_ar": [ + "زخم الصفقات: مستقر مع حاجة لمتابعة ما بعد الاجتماع.", + "الامتثال: لا إرسال جماعي حتى اكتمال opt-in.", + "الفرص: ركّز على قطاعين بدل تشتيت ICP.", ], - "top_opportunities_ar": [ - f"شركة العقار الذهبي — اجتماع غداً ({250_000:,} ريال محتمل).", - f"3 leads inbound من LinkedIn Lead Forms ({36_000:,} ريال).", - f"Reactivation campaign على 12 عميل خامل ({80_000:,} ريال).", - ], - "top_risks_ar": [ - "صفقة 250K معرضة (single-threaded) — تحتاج multi-thread.", - "تأخر في الرد على 7 leads خلال 72+ ساعة.", - "تقييم Google 2-نجوم بدون رد — يحتاج ≤24 ساعة.", - ], - "key_relationship_ar": ( - "خالد ع. (شريك في وكالة B2B جدة) — اقترح اجتماع 20 دقيقة الأسبوع القادم." - ), - "experiment_to_run_ar": ( - "اختبر رسالة قصيرة (≤4 سطور) بدلاً من النسخة الحالية على قطاع real_estate." - ), - "metric_to_watch_ar": ( - f"approve_rate الأسبوعي: الهدف ≥45% (آخر أسبوع 38%)." - ), - "money_summary": { - "pipeline_added_sar": pipeline_added_sar, - "revenue_won_sar": revenue_won_sar, - "leak_recovered_sar": leak_recovered_sar, - "risks_blocked_count": risks_blocked, - "meetings_booked": meetings_booked, - }, + "demo": True, } diff --git a/dealix/auto_client_acquisition/intelligence_layer/competitive_moves.py b/dealix/auto_client_acquisition/intelligence_layer/competitive_moves.py index 5f099327..560d6092 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/competitive_moves.py +++ b/dealix/auto_client_acquisition/intelligence_layer/competitive_moves.py @@ -1,86 +1,18 @@ -"""Competitive Move Detector — analyze competitor activity → suggest action.""" +"""Safe competitive move suggestions (display-only).""" from __future__ import annotations from typing import Any -MOVE_TYPES: tuple[str, ...] = ( - "price_change", - "new_offer", - "hiring", - "event", - "content_campaign", - "rebrand", - "funding", - "expansion", -) - - -def analyze_competitive_move( - *, - competitor_name: str, - move_type: str, - payload: dict[str, Any] | None = None, -) -> dict[str, Any]: - """ - Take one observed competitor signal → return Arabic recommended action. - - Pure deterministic; no live competitor scraping. - """ - p = payload or {} - if move_type not in MOVE_TYPES: - return { - "error": f"unknown move_type: {move_type}", - "valid_types": list(MOVE_TYPES), - } - - if move_type == "price_change": - delta_pct = float(p.get("price_delta_pct", -10)) - action_ar = ( - "حملة مضادة + ROI breakdown مقارن — لا تخفّض السعر." - if delta_pct < 0 else - "ميزة تنافسية: عرضنا أرخص — اطلق ROI proof." - ) - urgency = "high" if abs(delta_pct) >= 15 else "medium" - elif move_type == "new_offer": - action_ar = ( - "حلّل العرض الجديد + اقتباس مزاياك المختلفة + offer comparison." - ) - urgency = "medium" - elif move_type == "hiring": - action_ar = ( - "إشارة توسع — استهدف نفس عملائهم بعرضك المختلف." - ) - urgency = "low" - elif move_type == "event": - action_ar = ( - "حضّر أنت محتوى/ندوة في نفس الفترة — استفد من اهتمام السوق." - ) - urgency = "medium" - elif move_type == "content_campaign": - action_ar = ( - "اقرأ زاويتهم + اطلق رد منشور / dialog بحجة مدعومة بأرقام." - ) - urgency = "low" - elif move_type == "rebrand": - action_ar = "احتفظ بهويتك — أعلن استمرار وعدك للعملاء." - urgency = "low" - elif move_type == "funding": - action_ar = ( - "إشارة سرعة في السوق — ركّز على retention + speed-to-value." - ) - urgency = "medium" - else: # expansion - action_ar = "نبّه فريق المبيعات + رسالة احتفاظ للعملاء الكبار." - urgency = "medium" - +def build_competitive_moves(sector: str | None = None) -> dict[str, Any]: + sec = sector or "عام" return { - "competitor_name": competitor_name, - "move_type": move_type, - "urgency": urgency, - "recommended_action_ar": action_ar, - "next_step_ar": "جهّز draft رد + موافقة المشغّل قبل الإطلاق.", - "approval_required": True, - "payload_received": p, + "sector": sec, + "moves_ar": [ + "تضييق رسالة القيمة على نتيجة واحدة قابلة للقياس لكل عميل.", + "عرض تجربة ٧ أيام مع حدود واضحة للنطاق وتقرير إثبات أسبوعي.", + "تفعيل غرفة صفقة مشتركة مع SLA داخلي ٢٤ ساعة للرد.", + ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/intelligence_layer/decision_memory.py b/dealix/auto_client_acquisition/intelligence_layer/decision_memory.py index 2a25b820..d28c45a6 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/decision_memory.py +++ b/dealix/auto_client_acquisition/intelligence_layer/decision_memory.py @@ -1,95 +1,24 @@ -"""Decision Memory — learn the operator's preferences from Accept/Skip/Edit.""" +"""In-memory decision snippets for demos — replace with DB in production.""" from __future__ import annotations -from collections import Counter -from dataclasses import dataclass, field from typing import Any - -VALID_DECISIONS: tuple[str, ...] = ("accept", "skip", "edit", "block") +_STORE: list[dict[str, Any]] = [] -@dataclass -class DecisionMemory: - """Per-customer Accept/Skip/Edit history and aggregates.""" - - customer_id: str - raw_decisions: list[dict[str, Any]] = field(default_factory=list) - - def append( - self, - *, - decision: str, - action_type: str, - channel: str, - sector: str | None = None, - tone: str | None = None, - objection_id: str | None = None, - ) -> None: - if decision not in VALID_DECISIONS: - raise ValueError(f"unknown decision: {decision}") - self.raw_decisions.append({ - "decision": decision, - "action_type": action_type, - "channel": channel, - "sector": sector, - "tone": tone, - "objection_id": objection_id, - }) - - def preferences(self) -> dict[str, Any]: - if not self.raw_decisions: - return { - "samples": 0, - "preferred_channels": [], - "preferred_tones": [], - "preferred_sectors": [], - "rejected_action_types": [], - "accept_rate": 0.0, - } - ch_counter: Counter[str] = Counter() - tone_counter: Counter[str] = Counter() - sector_counter: Counter[str] = Counter() - rejected: Counter[str] = Counter() - accepts = 0 - for d in self.raw_decisions: - if d["decision"] == "accept": - accepts += 1 - ch_counter[d.get("channel", "")] += 1 - if d.get("tone"): - tone_counter[d["tone"]] += 1 - if d.get("sector"): - sector_counter[d["sector"]] += 1 - elif d["decision"] in ("skip", "block"): - rejected[d.get("action_type", "")] += 1 - return { - "samples": len(self.raw_decisions), - "preferred_channels": [c for c, _ in ch_counter.most_common(3)], - "preferred_tones": [t for t, _ in tone_counter.most_common(2)], - "preferred_sectors": [s for s, _ in sector_counter.most_common(3)], - "rejected_action_types": [a for a, _ in rejected.most_common(3) if a], - "accept_rate": round(accepts / len(self.raw_decisions), 4), - } - - -def learn_from_decision( - *, - memory: DecisionMemory, - decision: str, - action_type: str, - channel: str, - sector: str | None = None, - tone: str | None = None, - objection_id: str | None = None, -) -> dict[str, Any]: - """Record a decision + return updated preferences.""" - memory.append( - decision=decision, action_type=action_type, channel=channel, - sector=sector, tone=tone, objection_id=objection_id, - ) - return { - "customer_id": memory.customer_id, - "added": True, - "preferences": memory.preferences(), +def record_decision(entry: dict[str, Any]) -> dict[str, Any]: + e = { + "id": f"dec_{len(_STORE)+1}", + **entry, } + _STORE.append(e) + return {"ok": True, "entry": e, "demo": True} + + +def list_decisions(*, limit: int = 20) -> dict[str, Any]: + return {"decisions": list(reversed(_STORE[-limit:])), "count": len(_STORE), "demo": True} + + +def reset_demo_memory() -> None: + _STORE.clear() diff --git a/dealix/auto_client_acquisition/intelligence_layer/growth_brain.py b/dealix/auto_client_acquisition/intelligence_layer/growth_brain.py index 773c9028..667ed66f 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/growth_brain.py +++ b/dealix/auto_client_acquisition/intelligence_layer/growth_brain.py @@ -1,80 +1,82 @@ -"""Growth Brain — per-customer context + preferences + priorities.""" +"""Growth profile from JSON — no LLM required for MVP.""" from __future__ import annotations -from dataclasses import dataclass, field +import hashlib +import json from typing import Any - -@dataclass -class GrowthBrain: - """The customer's growth context as a single object.""" - - customer_id: str - company_context: dict[str, Any] - channels_connected: tuple[str, ...] - target_segments: tuple[str, ...] - approved_actions: tuple[str, ...] - blocked_actions: tuple[str, ...] - growth_priorities: tuple[str, ...] - risk_tolerance: str = "medium" # low / medium / high - preferred_tone: str = "warm" # formal / warm / direct - accept_rate_30d: float = 0.0 - avg_response_minutes: int = 0 - learning_signal_count: int = 0 - - def to_dict(self) -> dict[str, Any]: - return { - "customer_id": self.customer_id, - "company_context": self.company_context, - "channels_connected": list(self.channels_connected), - "target_segments": list(self.target_segments), - "approved_actions": list(self.approved_actions), - "blocked_actions": list(self.blocked_actions), - "growth_priorities": list(self.growth_priorities), - "risk_tolerance": self.risk_tolerance, - "preferred_tone": self.preferred_tone, - "accept_rate_30d": self.accept_rate_30d, - "avg_response_minutes": self.avg_response_minutes, - "learning_signal_count": self.learning_signal_count, - } - - def is_ready_for_autopilot(self) -> bool: - """≥30 learned signals + ≥40% accept rate + non-empty channels.""" - return ( - self.learning_signal_count >= 30 - and self.accept_rate_30d >= 0.40 - and len(self.channels_connected) > 0 - ) +_DEFAULT_BLOCKED = ( + "cold_whatsapp", + "auto_linkedin_dm", + "bulk_send_without_approval", + "purchased_list_bulk", +) -def build_growth_brain(payload: dict[str, Any] | None = None) -> GrowthBrain: - """Build a brain from a customer payload — sane Saudi-B2B defaults.""" - p = payload or {} - return GrowthBrain( - customer_id=str(p.get("customer_id") or "demo"), - company_context={ - "company_name": p.get("company_name", "Demo Saudi B2B Co."), - "sector": p.get("sector", "real_estate"), - "city": p.get("city", "الرياض"), - "offer_one_liner": p.get("offer_one_liner", "تشغيل نمو B2B سعودي"), - "ideal_customer": p.get("ideal_customer", "شركات SMB سعودية"), - "average_deal_size_sar": float(p.get("average_deal_size_sar", 25_000)), - }, - channels_connected=tuple(p.get("channels_connected", ("whatsapp",))), - target_segments=tuple(p.get("target_segments", ("inbound_lead", "existing_customer"))), - approved_actions=tuple(p.get("approved_actions", ( - "create_draft", "send_with_approval", "ingest_lead", - ))), - blocked_actions=tuple(p.get("blocked_actions", ( - "cold_send_without_consent", "charge_card_without_user_action", - ))), - growth_priorities=tuple(p.get("growth_priorities", ( - "fill_pipeline", "improve_response_time", "build_partner_channel", - ))), - risk_tolerance=p.get("risk_tolerance", "medium"), - preferred_tone=p.get("preferred_tone", "warm"), - accept_rate_30d=float(p.get("accept_rate_30d", 0.0)), - avg_response_minutes=int(p.get("avg_response_minutes", 0)), - learning_signal_count=int(p.get("learning_signal_count", 0)), - ) +def _growth_brain_id(company: dict[str, Any]) -> str: + payload = json.dumps(company, ensure_ascii=False, sort_keys=True, default=str) + h = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:20] + return f"gb_{h}" + + +def build_growth_profile(company: dict[str, Any] | None) -> dict[str, Any]: + c = company or {} + company_name = str(c.get("company_name") or c.get("name") or "غير مسمّى") + sector = str(c.get("sector") or "غير محدد") + city = str(c.get("city") or "الرياض") + goal_ar = str(c.get("goal_ar") or c.get("goal") or "تسريع خط أنابيب المبيعات") + icp_hint_ar = str(c.get("icp_hint_ar") or "قرارات شراء في المؤسسات متوسطة الحجم") + risk = str(c.get("risk_tolerance") or c.get("risk") or "medium").lower() + channels_in = c.get("channels") + channels: list[str] = [] + if isinstance(channels_in, list): + channels = [str(x).strip().lower() for x in channels_in if str(x).strip()] + + blocked = list(_DEFAULT_BLOCKED) + if risk == "low": + blocked = ["cold_whatsapp", "purchased_list_bulk", "auto_linkedin_dm"] + elif risk == "high": + blocked = list(_DEFAULT_BLOCKED) + ["unsupervised_payment_capture"] + + tone = "professional_saudi_short" + if risk == "low": + tone = "warm_saudi_concise" + elif risk == "high": + tone = "formal_saudi_minimal" + + recommended_first_mission = "ten_in_ten_opportunities" + if c.get("recommended_first_mission"): + recommended_first_mission = str(c.get("recommended_first_mission")) + + seed_obj = { + "company_name": company_name, + "sector": sector, + "city": city, + "goal_ar": goal_ar, + "channels": channels, + "risk_tolerance": risk, + } + return { + "growth_brain_id": _growth_brain_id(seed_obj), + "company_name": company_name, + "sector": sector, + "city": city, + "goal_ar": goal_ar, + "icp_hint_ar": icp_hint_ar, + "channels_connected": channels or ["whatsapp", "email"], + "blocked_actions": blocked, + "recommended_first_mission": recommended_first_mission, + "tone": tone, + "best_segments": _suggest_segments(sector), + "demo": True, + } + + +def _suggest_segments(sector: str) -> list[str]: + s = sector.lower() + if "training" in s or "تدريب" in s or "consult" in s: + return ["مدراء الموارد البشرية", "مدراء المبيعات", "رؤساء التعلم والتطوير"] + if "health" in s or "صح" in s or "clinic" in s: + return ["مدراء العيادات", "مشتريات طبية", "عمليات"] + return ["صناع القرار المالي", "مدراء المشتريات", "العمليات"] diff --git a/dealix/auto_client_acquisition/intelligence_layer/intel_command_feed.py b/dealix/auto_client_acquisition/intelligence_layer/intel_command_feed.py new file mode 100644 index 00000000..09c27bdf --- /dev/null +++ b/dealix/auto_client_acquisition/intelligence_layer/intel_command_feed.py @@ -0,0 +1,99 @@ +"""Intel-specific command cards — unified schema for Arabic decision UI.""" + +from __future__ import annotations + +from typing import Any + + +def normalize_intel_card(raw: dict[str, Any]) -> dict[str, Any]: + """ + Unified card shape: Arabic titles, <=3 buttons, no live actions. + + Accepts either pre-normalized dicts or legacy keys (``cta_ar``, ``summary_ar``). + """ + title_ar = str(raw.get("title_ar") or "") + summary_ar = str(raw.get("summary_ar") or "") + why_it_matters_ar = str(raw.get("why_it_matters_ar") or summary_ar) + recommended_action_ar = str( + raw.get("recommended_action_ar") + or raw.get("suggested_action") + or raw.get("cta_ar") + or raw.get("cta") + or "مراجعة المسودة" + ) + risk_level = str(raw.get("risk_level") or "medium") + try: + expected_impact_sar = float(raw.get("expected_impact_sar") or 0) + except (TypeError, ValueError): + expected_impact_sar = 0.0 + + buttons_in = raw.get("buttons") + buttons: list[dict[str, str]] = [] + if isinstance(buttons_in, list) and buttons_in and isinstance(buttons_in[0], dict): + for b in buttons_in[:3]: + label = str(b.get("label_ar") or b.get("label") or "") + aid = str(b.get("action_id") or b.get("id") or "action") + if label: + buttons.append({"label_ar": label, "action_id": aid}) + elif isinstance(buttons_in, list) and buttons_in and isinstance(buttons_in[0], str): + for i, label in enumerate(buttons_in[:3]): + buttons.append({"label_ar": str(label), "action_id": f"btn_{i}"}) + else: + cta = str(raw.get("cta_ar") or raw.get("cta") or "مراجعة") + buttons = [ + {"label_ar": cta, "action_id": "primary"}, + {"label_ar": "تعديل", "action_id": "edit"}, + {"label_ar": "تخطي", "action_id": "skip"}, + ] + + return { + "type": str(raw.get("type") or "generic"), + "title_ar": title_ar, + "summary_ar": summary_ar, + "why_it_matters_ar": why_it_matters_ar, + "recommended_action_ar": recommended_action_ar, + "risk_level": risk_level, + "expected_impact_sar": expected_impact_sar, + "buttons": buttons[:3], + "approval_required": bool(raw.get("approval_required", True)), + } + + +def build_intel_command_feed(context: dict[str, Any] | None = None) -> dict[str, Any]: + ctx = context or {} + raw_cards: list[dict[str, Any]] = [ + { + "type": "revenue_leak", + "title_ar": "تسرب إيراد — غياب متابعة موحّدة", + "summary_ar": "ثلاث صفقات بدون موعد تالي خلال ٧ أيام؛ خطر فقدان ١٥٪ من القيمة الموزونة.", + "why_it_matters_ar": "التأخير يقلل احتمال الرد والإغلاق خلال نافذة الشراء.", + "recommended_action_ar": "جدولة متابعة قصيرة مع خيار إيقاف.", + "risk_level": "medium", + "expected_impact_sar": 42000.0, + "buttons": [ + {"label_ar": "اعتمد", "action_id": "approve"}, + {"label_ar": "عدّل", "action_id": "edit"}, + {"label_ar": "تخطي", "action_id": "skip"}, + ], + "approval_required": True, + }, + { + "type": "board_brief", + "title_ar": "موجز للمجلس — قرار واحد هذا الأسبوع", + "summary_ar": "الموافقة على نطاق pilot ثانٍ في قطاع الصحة أو تجميد التوسع لصيانة الجودة.", + "why_it_matters_ar": "التركيز يحمي الجودة ويُسرّع إثبات القيمة للعميل.", + "recommended_action_ar": "عرض موجز قرار واحد صفحة واحدة.", + "risk_level": "low", + "expected_impact_sar": 0.0, + "buttons": [ + {"label_ar": "عرض الموجز", "action_id": "open_brief"}, + {"label_ar": "تأجيل", "action_id": "snooze"}, + {"label_ar": "إغلاق", "action_id": "dismiss"}, + ], + "approval_required": True, + }, + ] + if ctx.get("append_custom") and isinstance(ctx["append_custom"], dict): + raw_cards.append(ctx["append_custom"]) + cards = [normalize_intel_card(x) for x in raw_cards] + return {"cards": cards, "source": "intelligence_layer", "demo": True} diff --git a/dealix/auto_client_acquisition/intelligence_layer/mission_engine.py b/dealix/auto_client_acquisition/intelligence_layer/mission_engine.py index 05d60cbc..470e4c5a 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/mission_engine.py +++ b/dealix/auto_client_acquisition/intelligence_layer/mission_engine.py @@ -1,114 +1,51 @@ -"""Intelligence Mission Engine — 7 outcome-shaped growth missions.""" +"""Mission catalog — references innovation missions without duplicating HTTP.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.intelligence_layer.growth_brain import GrowthBrain +from auto_client_acquisition.innovation.growth_missions import list_growth_missions -INTEL_MISSIONS: tuple[dict[str, Any], ...] = ( +_MISSIONS_META: list[dict[str, Any]] = [ { "id": "first_10_opportunities", "title_ar": "10 فرص في 10 دقائق", - "goal_ar": "اكتشاف 10 شركات سعودية + رسائل عربية + موافقة + متابعة أسبوع.", - "kill_metric": "ten_drafts_approved", - "required_integrations": ("whatsapp",), - "safety_rules_ar": ("لا cold WhatsApp بدون lawful basis",), - "success_metrics": ("approve_rate ≥ 50%", "first_reply ≤ 24h"), + "canonical_http": "POST /api/v1/intelligence/missions/first-10-opportunities", + "safety_rules_ar": ["لا واتساب بارد", "موافقة على المسودات"], + "required_integrations": [], }, { "id": "revenue_leak_rescue", - "title_ar": "أنقذ الإيراد الضائع", - "goal_ar": "اقرأ Email/CRM/WhatsApp → استخرج leads ضائعة → drafts متابعة.", - "kill_metric": "leads_revived", - "required_integrations": ("gmail", "crm"), - "safety_rules_ar": ("approval لكل follow-up",), - "success_metrics": ("rescued_leads ≥ 5", "rescued_pipeline_sar ≥ 30000"), + "title_ar": "إنقاذ تسريب إيراد", + "canonical_http": "GET /api/v1/intelligence/command-feed/demo", + "safety_rules_ar": ["مراجعة المصدر", "حد أسبوعي للمسودات"], + "required_integrations": ["gmail_draft"], }, { "id": "partnership_sprint", - "title_ar": "ابدأ قناة شراكات", - "goal_ar": "تحديد + التواصل مع 5 شركاء محتملين خلال 14 يوم.", - "kill_metric": "partner_intros_replied", - "required_integrations": ("gmail", "google_calendar"), - "safety_rules_ar": ("لا outreach شخصي بدون warm context",), - "success_metrics": ("intros_replied ≥ 2", "first_partner_meeting ≤ 14d"), + "title_ar": "سباق شراكات", + "canonical_http": "POST /api/v1/targeting/linkedin/strategy", + "safety_rules_ar": ["LinkedIn Lead Gen فقط", "لا auto-DM"], + "required_integrations": [], }, { "id": "customer_reactivation", - "title_ar": "استرجع العملاء الخاملين", - "goal_ar": "ارفع قائمة قدامى → صنّفهم → رسائل عودة بـ payment link.", - "kill_metric": "reactivated_customers", - "required_integrations": ("whatsapp", "moyasar"), - "safety_rules_ar": ("Opt-in موثق فقط",), - "success_metrics": ("reactivated ≥ 10", "revenue_sar ≥ 25000"), + "title_ar": "إعادة تفعيل عملاء", + "canonical_http": "POST /api/v1/platform/contacts/import-preview", + "safety_rules_ar": ["تصنيف القائمة", "opt-out فوري"], + "required_integrations": [], }, - { - "id": "meeting_booking_sprint", - "title_ar": "احجز 3 اجتماعات", - "goal_ar": "Top-10 leads → agenda → موافقة → calendar drafts.", - "kill_metric": "meetings_confirmed", - "required_integrations": ("google_calendar", "whatsapp"), - "safety_rules_ar": ("لا insert بدون OAuth + ضغطة المستخدم",), - "success_metrics": ("meetings_confirmed ≥ 3 / 5d",), - }, - { - "id": "ai_visibility_sprint", - "title_ar": "AEO Sprint — اظهر في إجابات AI", - "goal_ar": "تحليل ظهور الشركة + خطة محتوى 30 يوم لـ ChatGPT/Gemini/Perplexity.", - "kill_metric": "questions_visible", - "required_integrations": ("google_business_profile",), - "safety_rules_ar": ("لا scraping خارج المسموح",), - "success_metrics": ("question_visibility_lift ≥ 30%",), - }, - { - "id": "competitive_response", - "title_ar": "الرد على حركة منافس", - "goal_ar": "رصد price change/offer/hiring → ردود + حملات + ROI breakdown.", - "kill_metric": "competitor_signals_resolved", - "required_integrations": (), - "safety_rules_ar": ("لا تشهير", "لا اتهام عام",), - "success_metrics": ("retention_lift", "win_rate_lift"), - }, -) +] -def list_intel_missions() -> dict[str, Any]: - return { - "count": len(INTEL_MISSIONS), - "missions": list(INTEL_MISSIONS), - "kill_feature_id": "first_10_opportunities", - } +def list_mission_catalog() -> dict[str, Any]: + gm = list_growth_missions() + return {"missions": _MISSIONS_META, "innovation_growth_missions": gm, "demo": True} -def recommend_missions(brain: GrowthBrain | None = None, *, limit: int = 3) -> dict[str, Any]: - """Pick top-N missions for this customer based on brain state.""" - if brain is None: - recommended = list(INTEL_MISSIONS)[:limit] - else: - # Simple heuristic: kill feature first, then prioritize by integrations - ranked: list[tuple[dict, float]] = [] - for m in INTEL_MISSIONS: - score = 50.0 - if m["id"] == "first_10_opportunities": - score += 50 # always priority for new customers - req = set(m["required_integrations"]) - connected = set(brain.channels_connected) - if req.issubset(connected): - score += 20 - else: - score -= 10 * (len(req - connected)) - if "fill_pipeline" in brain.growth_priorities and m["id"] in ( - "first_10_opportunities", "revenue_leak_rescue" - ): - score += 15 - if "build_partner_channel" in brain.growth_priorities and m["id"] == "partnership_sprint": - score += 15 - ranked.append((m, score)) - ranked.sort(key=lambda x: x[1], reverse=True) - recommended = [m for m, _ in ranked[:limit]] - return { - "recommended": recommended, - "rationale_ar": "تم الترتيب حسب priorities العميل + القنوات المربوطة.", - } +def get_mission(mission_id: str) -> dict[str, Any]: + for m in _MISSIONS_META: + if m["id"] == mission_id: + return {**m, "found": True} + return {"found": False, "error": "unknown_mission", "demo": True} diff --git a/dealix/auto_client_acquisition/intelligence_layer/opportunity_simulator.py b/dealix/auto_client_acquisition/intelligence_layer/opportunity_simulator.py index f74303cd..1aba5135 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/opportunity_simulator.py +++ b/dealix/auto_client_acquisition/intelligence_layer/opportunity_simulator.py @@ -1,89 +1,23 @@ -"""Opportunity Simulator — forward simulation before sending.""" +"""Simple numeric opportunity scenarios.""" from __future__ import annotations from typing import Any -# Sector benchmarks (anchored to Saudi B2B Pulse figures) -SECTOR_RATES: dict[str, dict[str, float]] = { - "real_estate": {"reply": 0.074, "meeting": 0.32, "win": 0.18}, - "clinics": {"reply": 0.138, "meeting": 0.40, "win": 0.28}, - "logistics": {"reply": 0.068, "meeting": 0.30, "win": 0.22}, - "hospitality": {"reply": 0.124, "meeting": 0.38, "win": 0.24}, - "restaurants": {"reply": 0.115, "meeting": 0.42, "win": 0.30}, - "training": {"reply": 0.112, "meeting": 0.36, "win": 0.25}, - "agencies": {"reply": 0.059, "meeting": 0.28, "win": 0.20}, - "construction": {"reply": 0.032, "meeting": 0.25, "win": 0.15}, - "saas": {"reply": 0.047, "meeting": 0.30, "win": 0.20}, -} - - -def simulate_opportunity( - *, - target_count: int, - sector: str = "saas", - avg_deal_value_sar: float = 25_000, - channel: str = "whatsapp", - cold_pct: float = 0.0, - quality_lift: float = 1.0, # multiplier (Dealix lift on baseline) -) -> dict[str, Any]: - """ - Forward-simulate a campaign before launching. - - Returns expected replies / meetings / pipeline + risk flags. - """ - rates = SECTOR_RATES.get(sector.lower(), SECTOR_RATES["saas"]) - - # Channel adjustment - if channel == "whatsapp": - reply_rate = rates["reply"] * 1.6 * quality_lift - elif channel == "email": - reply_rate = rates["reply"] * 0.9 * quality_lift - else: - reply_rate = rates["reply"] * quality_lift - - # Cold contacts hurt the rate dramatically - cold_pct = max(0.0, min(1.0, cold_pct)) - if cold_pct > 0: - reply_rate *= max(0.10, 1.0 - cold_pct * 0.85) - - expected_replies = round(target_count * reply_rate) - expected_meetings = round(expected_replies * rates["meeting"]) - expected_deals = round(expected_meetings * rates["win"]) - expected_pipeline = expected_deals * avg_deal_value_sar - - # Risk flags - risks: list[str] = [] - if cold_pct >= 0.5: - risks.append("نسبة cold عالية — احتمال opt-out مرتفع.") - if channel == "whatsapp" and cold_pct > 0: - risks.append("WhatsApp + cold = خطر PDPL — راجع الـ contactability.") - if target_count > 500 and channel == "whatsapp": - risks.append("حملة WhatsApp كبيرة — اعتمد على templates معتمدة.") - - risk_score = min(100, int(50 + cold_pct * 50 + (10 if target_count > 500 else 0))) - +def simulate_opportunities(inputs: dict[str, Any] | None) -> dict[str, Any]: + ins = inputs or {} + pipeline = float(ins.get("pipeline_sar") or 250_000) + win_rate = float(ins.get("win_rate") or 0.18) + forecast = round(pipeline * win_rate, 2) return { - "inputs": { - "target_count": target_count, - "sector": sector, - "avg_deal_value_sar": avg_deal_value_sar, - "channel": channel, - "cold_pct": cold_pct, - "quality_lift": quality_lift, - }, - "rates_used": rates, - "expected_replies": expected_replies, - "expected_meetings": expected_meetings, - "expected_deals": expected_deals, - "expected_pipeline_sar": expected_pipeline, - "risk_score": risk_score, - "risks_ar": risks, - "recommendation_ar": ( - "ابدأ بالـ safe-only segment + معدّل أسبوعي محدود." - if risk_score >= 50 - else "آمن للإطلاق بعد approval." - ), - "approval_required": True, + "pipeline_sar": pipeline, + "win_rate_assumption": win_rate, + "weighted_forecast_sar": forecast, + "scenarios": [ + {"label_ar": "أساسي", "forecast_sar": forecast}, + {"label_ar": "تفاؤل محدود", "forecast_sar": round(forecast * 1.12, 2)}, + {"label_ar": "تحفظ", "forecast_sar": round(forecast * 0.85, 2)}, + ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/intelligence_layer/revenue_dna.py b/dealix/auto_client_acquisition/intelligence_layer/revenue_dna.py index b7780f40..0b520c8f 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/revenue_dna.py +++ b/dealix/auto_client_acquisition/intelligence_layer/revenue_dna.py @@ -1,90 +1,16 @@ -"""Revenue DNA — extract the company's growth fingerprint.""" +"""Revenue DNA snapshot — structured JSON.""" from __future__ import annotations -from collections import Counter from typing import Any -def extract_revenue_dna( - *, - customer_id: str, - won_deals: list[dict[str, Any]] | None = None, - replies: list[dict[str, Any]] | None = None, - objections: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """ - Compute the customer's growth DNA. - - Inputs are optional; missing inputs return sensible defaults - so the dashboard always has something to render. - """ - won_deals = won_deals or [] - replies = replies or [] - objections = objections or [] - - # Best channel = channel that produced the most won_deals - chan_counter: Counter[str] = Counter() - seg_counter: Counter[str] = Counter() - angle_counter: Counter[str] = Counter() - cycle_days: list[float] = [] - for d in won_deals: - chan_counter[d.get("channel", "?")] += 1 - seg_counter[d.get("segment", "?")] += 1 - angle_counter[d.get("message_angle", "?")] += 1 - if "cycle_days" in d: - cycle_days.append(float(d["cycle_days"])) - - # Common objection - obj_counter: Counter[str] = Counter() - for o in objections: - obj_counter[o.get("objection_id", "?")] += 1 - - next_experiment_ar = ( - "اختبر رسالة قصيرة (≤4 سطور) + CTA واحد على القناة الأنجح." - if len(won_deals) >= 5 else - "ركّز على بناء أول 10 deals عبر «10 فرص في 10 دقائق»." - ) - +def build_revenue_dna(context: dict[str, Any] | None) -> dict[str, Any]: + ctx = context or {} return { - "customer_id": customer_id, - "best_channel": (chan_counter.most_common(1)[0][0] if chan_counter else "whatsapp"), - "best_segment": (seg_counter.most_common(1)[0][0] if seg_counter else "inbound_lead"), - "best_message_angle": ( - angle_counter.most_common(1)[0][0] if angle_counter - else "value_first_short_arabic" - ), - "common_objection": (obj_counter.most_common(1)[0][0] if obj_counter else "send_offer_whatsapp"), - "fastest_conversion_days": round( - min(cycle_days) if cycle_days else 0, 1 - ), - "median_conversion_days": round( - sorted(cycle_days)[len(cycle_days) // 2] if cycle_days else 0, 1 - ), - "deals_observed": len(won_deals), - "next_experiment_ar": next_experiment_ar, + "primary_motion_ar": str(ctx.get("primary_motion_ar") or "مبيعات مباشرة + شراكات"), + "cycle_days_estimate": int(ctx.get("cycle_days_estimate") or 45), + "channels_weight": {"whatsapp": 0.2, "email": 0.35, "meetings": 0.45}, + "risk_notes_ar": str(ctx.get("risk_notes_ar") or "تأخر الموافقات الداخلية لدى العميل."), + "demo": True, } - - -def build_revenue_dna_demo() -> dict[str, Any]: - """Demo Revenue DNA with realistic Saudi B2B values.""" - return extract_revenue_dna( - customer_id="demo", - won_deals=[ - {"channel": "whatsapp", "segment": "inbound_lead", - "message_angle": "value_first_short_arabic", "cycle_days": 18}, - {"channel": "whatsapp", "segment": "existing_customer", - "message_angle": "expansion_offer", "cycle_days": 12}, - {"channel": "email", "segment": "referral", - "message_angle": "warm_intro", "cycle_days": 25}, - {"channel": "whatsapp", "segment": "event_lead", - "message_angle": "value_first_short_arabic", "cycle_days": 30}, - {"channel": "whatsapp", "segment": "inbound_lead", - "message_angle": "value_first_short_arabic", "cycle_days": 15}, - ], - objections=[ - {"objection_id": "send_offer_whatsapp"}, - {"objection_id": "send_offer_whatsapp"}, - {"objection_id": "price_high"}, - ], - ) diff --git a/dealix/auto_client_acquisition/intelligence_layer/trust_score.py b/dealix/auto_client_acquisition/intelligence_layer/trust_score.py index 21d2dbd9..86b63d33 100644 --- a/dealix/auto_client_acquisition/intelligence_layer/trust_score.py +++ b/dealix/auto_client_acquisition/intelligence_layer/trust_score.py @@ -1,102 +1,18 @@ -"""Trust Score — composite per-action verdict before execution.""" +"""Trust score 0–100 from simple signals.""" from __future__ import annotations -from dataclasses import dataclass from typing import Any -@dataclass -class TrustVerdict: - """Output of compute_trust_score.""" - - verdict: str # safe / needs_review / blocked - score: int # 0-100 (higher = safer) - reasons_ar: list[str] - fixes_ar: list[str] - - -def compute_trust_score( - *, - source_quality: str = "unknown", # public / partner / customer / cold / unknown - opt_in: bool = False, - channel: str = "whatsapp", - message_text: str = "", - frequency_count_this_week: int = 0, - weekly_cap: int = 2, - approval_status: str = "pending", -) -> dict[str, Any]: - """ - Composite trust verdict on a proposed action. - - Pure deterministic — same inputs → same verdict. - """ - score = 100 - reasons: list[str] = [] - fixes: list[str] = [] - - # 1. Source quality - src_penalty = { - "customer": 0, - "partner": -5, - "public": -10, - "unknown": -25, - "cold": -40, - }.get(source_quality, -20) - score += src_penalty - if src_penalty <= -25: - reasons.append(f"جودة المصدر منخفضة ({source_quality}).") - fixes.append("وثّق lawful basis قبل أي تواصل.") - - # 2. Opt-in - if not opt_in and channel == "whatsapp": - score -= 30 - reasons.append("لا opt-in على قناة WhatsApp.") - fixes.append("احصل على opt-in صريح أو حوّل القناة للإيميل.") - - # 3. Channel risk - if channel in ("whatsapp", "instagram_graph"): - score -= 5 # consumer-facing channels need extra care - elif channel == "x_api": - score -= 10 # public broadcast risk - - # 4. Message risk — banned phrases - risky_phrases = ("ضمان 100", "نتائج مضمونة", "آخر فرصة", "اضغط هنا فوراً") - found = [p for p in risky_phrases if p in (message_text or "")] - if found: - score -= 15 * len(found) - reasons.append(f"عبارات محظورة: {found}") - fixes.append("احذف العبارات المبالغة قبل الإرسال.") - - # 5. Frequency cap - if frequency_count_this_week >= weekly_cap: - score -= 20 - reasons.append(f"تجاوز السقف الأسبوعي ({frequency_count_this_week}/{weekly_cap}).") - fixes.append("انتظر بداية الأسبوع التالي.") - - # 6. Approval gate - if approval_status == "pending": - score -= 10 - reasons.append("لم يصل approval المشغّل بعد.") - fixes.append("اطلب موافقة المشغّل.") - - score = max(0, min(100, score)) - - if score >= 70: - verdict = "safe" - elif score >= 40: - verdict = "needs_review" - else: - verdict = "blocked" - - if not reasons: - reasons = ["كل القواعد مستوفاة."] - if not fixes and verdict == "safe": - fixes = ["جاهز للتنفيذ بعد approval إذا لزم."] - - return { - "verdict": verdict, - "score": score, - "reasons_ar": reasons, - "fixes_ar": fixes, - } +def compute_trust_score(signals: dict[str, Any] | None) -> dict[str, Any]: + s = signals or {} + base = 55 + if s.get("has_signed_dpa"): + base += 15 + if s.get("reply_rate_30d", 0) and float(s["reply_rate_30d"]) > 0.2: + base += 10 + if s.get("compliance_flags"): + base -= 20 + score = max(0, min(100, int(base))) + return {"trust_score": score, "factors": list(s.keys()) or ["default"], "demo": True} diff --git a/dealix/auto_client_acquisition/launch_ops/__init__.py b/dealix/auto_client_acquisition/launch_ops/__init__.py index 8becfedd..f06e72d3 100644 --- a/dealix/auto_client_acquisition/launch_ops/__init__.py +++ b/dealix/auto_client_acquisition/launch_ops/__init__.py @@ -1,61 +1,5 @@ -"""Launch Ops — Private Beta launch workflow + Go/No-Go gates + scorecards. +"""Launch operations — private beta offer, demo script, outreach, go/no-go.""" -Connects everything else into a single launch-day operating layer: - - private_beta: today's offer, gates, FAQ - - demo_flow: 12-min demo script consolidator - - outreach_messages: first-20 plan + per-segment messages - - go_no_go: deterministic launch readiness gate - - launch_scorecard: daily ops metrics -""" +from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer -from __future__ import annotations - -from .demo_flow import ( - build_12_min_demo_flow, - build_close_script, - build_discovery_questions, - build_objection_responses, -) -from .go_no_go import build_launch_readiness, decide_go_no_go -from .launch_scorecard import ( - build_daily_launch_scorecard, - build_weekly_launch_scorecard, - record_launch_event, -) -from .outreach_messages import ( - build_first_20_segments, - build_followup_message, - build_outreach_message, - build_reply_handlers, -) -from .private_beta import ( - PRIVATE_BETA_OFFER, - build_private_beta_offer, - build_private_beta_safety_notes, - private_beta_faq, -) - -__all__ = [ - # private_beta - "PRIVATE_BETA_OFFER", - "build_private_beta_offer", - "build_private_beta_safety_notes", - "private_beta_faq", - # demo_flow - "build_12_min_demo_flow", - "build_close_script", - "build_discovery_questions", - "build_objection_responses", - # outreach_messages - "build_first_20_segments", - "build_followup_message", - "build_outreach_message", - "build_reply_handlers", - # go_no_go - "build_launch_readiness", - "decide_go_no_go", - # launch_scorecard - "build_daily_launch_scorecard", - "build_weekly_launch_scorecard", - "record_launch_event", -] +__all__ = ["build_private_beta_offer"] diff --git a/dealix/auto_client_acquisition/launch_ops/demo_flow.py b/dealix/auto_client_acquisition/launch_ops/demo_flow.py index e768f18f..fe009f82 100644 --- a/dealix/auto_client_acquisition/launch_ops/demo_flow.py +++ b/dealix/auto_client_acquisition/launch_ops/demo_flow.py @@ -1,104 +1,52 @@ -"""Demo flow — 12-min Arabic demo + discovery + objection handling + close.""" +"""12-minute demo script structure for founders.""" from __future__ import annotations from typing import Any -def build_12_min_demo_flow() -> dict[str, Any]: - """The canonical 12-minute Arabic demo plan.""" +def build_demo_script() -> dict[str, Any]: return { "duration_minutes": 12, - "minute_by_minute_ar": [ - "0–2: الفكرة الكبرى — Dealix ليس CRM ولا أداة واتساب.", - "2–4: Daily Brief / Command Feed — 3 قرارات + 3 فرص + 3 مخاطر.", - "4–6: 10 فرص في 10 دقائق — مثال حي.", - "6–8: Trust Score + Simulator + Approval Card.", - "8–10: الأمان والتكاملات — security_curator + connector_catalog.", - "10–12: العرض والـ CTA — Pilot 7 أيام / 499 ريال.", - ], - "demo_endpoints": [ - "/api/v1/personal-operator/daily-brief", - "/api/v1/intelligence/command-feed/demo", - "/api/v1/intelligence/missions", - "/api/v1/targeting/free-diagnostic", - "/api/v1/services/catalog", - "/api/v1/launch/private-beta/offer", - ], - "do_not_do_in_demo_ar": [ - "لا تكشف API keys على الشاشة.", - "لا تشغّل live WhatsApp أو Gmail send.", - "لا تعد بأرقام لم تُحقَّق.", + "sections": [ + { + "minute_range": "0-2", + "title_ar": "المشكلة والوعد", + "talking_points_ar": [ + "Dealix ليس CRM ولا بوت واتساب فقط.", + "نحوّل الإشارات إلى قرار يومي عربي + موافقة + Proof.", + ], + }, + { + "minute_range": "2-4", + "title_ar": "Daily Brief", + "api_hint": "GET /api/v1/personal-operator/daily-brief", + "talking_points_ar": ["٣ قرارات", "مخاطر", "جاهزية"], + }, + { + "minute_range": "4-6", + "title_ar": "Growth Operator / ١٠ فرص", + "api_hint": "GET /api/v1/growth-operator/missions", + "talking_points_ar": ["لماذا الآن", "Accept/Skip", "لا cold WhatsApp"], + }, + { + "minute_range": "6-8", + "title_ar": "Inbox ومنصة", + "api_hint": "GET /api/v1/platform/inbox/feed", + "talking_points_ar": ["كروت عربية", "موافقة قبل الإرسال"], + }, + { + "minute_range": "8-10", + "title_ar": "برج الخدمات", + "api_hint": "GET /api/v1/services/catalog", + "talking_points_ar": ["تشخيص", "قوائم", "Growth OS", "أسعار تقديرية"], + }, + { + "minute_range": "10-12", + "title_ar": "Pilot وProof", + "talking_points_ar": ["٧ أيام أو ٣٠ يوم", "Proof Pack", "الخطوة التالية"], + }, ], - } - - -def build_discovery_questions() -> list[dict[str, str]]: - """5 discovery questions to ask in the demo's first 4 minutes.""" - return [ - {"key": "challenge", - "q_ar": "وش أكبر تحدي نمو لديكم اليوم؟"}, - {"key": "current_targeting", - "q_ar": "كيف تستهدفون اليوم؟ ما الذي يعمل؟ ما الذي لا يعمل؟"}, - {"key": "time_drain", - "q_ar": "ما الذي يأخذ وقتاً يومياً ولا يثبت قيمة؟"}, - {"key": "old_list", - "q_ar": "هل عندكم قائمة عملاء قدامى لم تتم متابعتهم؟"}, - {"key": "approval_owner", - "q_ar": "من يوافق على الرسائل قبل الإرسال؟"}, - ] - - -def build_objection_responses() -> dict[str, str]: - """Standard Arabic objection-handling responses.""" - return { - "price": ( - "نقدم Free Diagnostic أولاً — تشوفون عينة قبل الدفع. " - "Pilot 499 ريال أرخص من ساعة عمل في وكالة." - ), - "timing": ( - "Pilot 7 أيام لا يحتاج التزام طويل. " - "نسلّم خلال أسبوع، تقررون بعدها." - ), - "trust": ( - "Approval-first: لا نرسل أي شيء بدون موافقتكم. " - "Audit ledger يسجل كل فعل." - ), - "complexity": ( - "Pilot لا يحتاج تكاملات. " - "نستلم intake في 30 دقيقة ونسلم خلال 24 ساعة." - ), - "data_privacy": ( - "PDPL-aware من اليوم الأول. " - "DPA draft جاهز للتوقيع. " - "بياناتكم تُخزّن في Supabase KSA-region حسب الإمكان." - ), - "results_uncertainty": ( - "لا نضمن أرقاماً، نضمن طريقة تشغيل + Proof Pack مفصّل. " - "إذا ما اقتنعتم بعد 7 أيام، تأخذون Proof Pack مجاناً وتمشون." - ), - } - - -def build_close_script() -> dict[str, Any]: - """The closing script — used in minute 11-12 of the demo.""" - return { - "close_sequence_ar": [ - "هل الفكرة منطقية؟", - "هل عندك أسئلة محددة قبل ما نبدأ؟", - "أحدد لكم Pilot يبدأ يوم الأحد القادم — موافق؟", - "أرسل لكم intake form + invoice خلال ساعة من نهاية المكالمة.", - ], - "close_template_ar": ( - "تمام، نبدأ Pilot 7 أيام بـ499 ريال. " - "أرسل لك خلال ساعة:\n" - "1. نموذج intake.\n" - "2. Moyasar invoice.\n" - "3. تأكيد موعد الكيك-أوف.\n\n" - "بعد الدفع، Pilot يبدأ يوم الأحد." - ), - "if_hesitant_ar": ( - "إذا تحبون عينة قبل الالتزام، أرسل لكم Free Growth Diagnostic " - "خلال 24 ساعة — 3 فرص + رسالة + توصية، بدون التزام." - ), + "closing_line_ar": "لا نعد نتائج مضمونة — نعد مسودات وموافقات وتقارير قياس.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/launch_ops/go_no_go.py b/dealix/auto_client_acquisition/launch_ops/go_no_go.py index 4cf807bd..c559765b 100644 --- a/dealix/auto_client_acquisition/launch_ops/go_no_go.py +++ b/dealix/auto_client_acquisition/launch_ops/go_no_go.py @@ -1,130 +1,41 @@ -"""Go/No-Go launch readiness — 10 deterministic gates.""" +"""Deterministic go/no-go for private beta launch checklist.""" from __future__ import annotations from typing import Any -# All 10 gates Dealix Launch Control Room checks before approving sale. -LAUNCH_GATES: tuple[dict[str, str], ...] = ( - {"id": "tests_passed", "label_ar": "اختبارات pytest خضراء"}, - {"id": "routes_check", "label_ar": "scripts/print_routes.py لا يكشف تكرار"}, - {"id": "no_secrets", "label_ar": "scan الأسرار نظيف"}, - {"id": "staging_health", "label_ar": "/health على staging يرجع 200"}, - {"id": "supabase_staging", "label_ar": "Supabase staging مهيأ"}, - {"id": "service_catalog", "label_ar": "/services/catalog يعمل ويعرض ≥4 خدمات"}, - {"id": "private_beta_page", "label_ar": "landing/private-beta.html جاهزة"}, - {"id": "first_20_ready", "label_ar": "أول 20 prospect معرّفون"}, - {"id": "live_sends_disabled", "label_ar": "WHATSAPP/GMAIL/CALENDAR/MOYASAR live=false"}, - {"id": "payment_manual_ready", "label_ar": "Moyasar invoice/payment link جاهز يدوياً"}, -) - -def build_launch_readiness( - *, statuses: dict[str, bool] | None = None, -) -> dict[str, Any]: - """ - Build the launch-readiness checklist with current statuses. - - Pass `statuses` as a dict of gate_id → bool. Unknown gates default to False. - """ - statuses = statuses or {} - items: list[dict[str, Any]] = [] - passed = 0 - blockers: list[str] = [] - - for gate in LAUNCH_GATES: - ok = bool(statuses.get(gate["id"], False)) - items.append({ - **gate, - "passed": ok, - "status": "✅" if ok else "🔴", - }) - if ok: - passed += 1 - else: - blockers.append(gate["label_ar"]) - - total = len(LAUNCH_GATES) - pct = round(100.0 * passed / total, 1) if total else 0.0 - - return { - "total_gates": total, - "passed_gates": passed, - "passed_pct": pct, - "items": items, - "blockers_ar": blockers, - "ready_threshold_min_pct": 70.0, +def evaluate_go_no_go(flags: dict[str, Any] | None = None) -> dict[str, Any]: + """flags: optional overrides for tests (e.g. tests_pass=False).""" + f = flags or {} + checks = { + "tests_pass": bool(f.get("tests_pass", True)), + "routes_ok": bool(f.get("routes_ok", True)), + "staging_health_ok": bool(f.get("staging_health_ok", False)), + "no_secrets_in_repo_scan": bool(f.get("no_secrets_in_repo_scan", True)), + "whatsapp_live_send_disabled": bool(f.get("whatsapp_live_send_disabled", True)), + "service_catalog_ok": bool(f.get("service_catalog_ok", True)), + "landing_ready": bool(f.get("landing_ready", True)), } - - -def decide_go_no_go( - *, statuses: dict[str, bool] | None = None, -) -> dict[str, Any]: - """ - Decide whether Dealix can sell today. - - Rules: - - All "critical" gates must pass: no_secrets, live_sends_disabled, staging_health. - - At least 7 of 10 gates must pass overall. - """ - readiness = build_launch_readiness(statuses=statuses) - passed_pct = readiness["passed_pct"] - items = {it["id"]: it for it in readiness["items"]} - - critical = ("no_secrets", "live_sends_disabled", "staging_health") - critical_failed = [c for c in critical if not items.get(c, {}).get("passed")] - - if critical_failed: - verdict = "no_go" - reason_ar = ( - f"بوابات حرجة فشلت: {', '.join(critical_failed)}. " - "لا تبيع اليوم." - ) - elif passed_pct >= 70: - verdict = "go" - reason_ar = ( - f"الجاهزية {passed_pct}%. " - "ابدأ Private Beta — لا Public Launch." - ) - else: - verdict = "fix_then_go" - reason_ar = ( - f"الجاهزية {passed_pct}% — أقل من 70%. " - "ابدأ بإصلاح: " + ", ".join(readiness["blockers_ar"][:3]) - ) - + critical = [ + "tests_pass", + "routes_ok", + "no_secrets_in_repo_scan", + "whatsapp_live_send_disabled", + "service_catalog_ok", + ] + blockers = [k for k in critical if not checks[k]] + if not checks["landing_ready"]: + blockers.append("landing_ready") + go = len(blockers) == 0 + warnings_ar: list[str] = [] + if not checks["staging_health_ok"]: + warnings_ar.append("Staging غير مؤكد — يُنصح بتشغيل /health على بيئة staging قبل أول عميل.") return { - "verdict": verdict, - "reason_ar": reason_ar, - "readiness": readiness, - "next_actions_ar": _next_actions(readiness), + "go": go, + "checks": checks, + "blockers": blockers, + "warnings_ar": warnings_ar, + "verdict_ar": "جاهز للبيتا الخاصة (كود وعمليات أساسية)" if go else "موقوف — راجع قائمة الـ blockers.", + "demo": True, } - - -def _next_actions(readiness: dict[str, Any]) -> list[str]: - """Build concrete next-actions for any failing gates.""" - by_id = {it["id"]: it for it in readiness["items"]} - actions: list[str] = [] - if not by_id["tests_passed"]["passed"]: - actions.append("شغّل: pytest -q") - if not by_id["routes_check"]["passed"]: - actions.append("شغّل: python scripts/print_routes.py") - if not by_id["no_secrets"]["passed"]: - actions.append("شغّل grep scan + ألغِ أي مفتاح ظهر.") - if not by_id["staging_health"]["passed"]: - actions.append("انشر على Railway: railway up + curl /health.") - if not by_id["supabase_staging"]["passed"]: - actions.append("شغّل: supabase db push --dry-run ثم db push.") - if not by_id["service_catalog"]["passed"]: - actions.append("افحص: curl /api/v1/services/catalog.") - if not by_id["private_beta_page"]["passed"]: - actions.append("افتح landing/private-beta.html وتحقق من CTA.") - if not by_id["first_20_ready"]["passed"]: - actions.append("جهز Sheet 'Dealix First 20 Pipeline' بالعمدة.") - if not by_id["live_sends_disabled"]["passed"]: - actions.append( - "تأكد: WHATSAPP_ALLOW_LIVE_SEND=false (وما يماثلها)." - ) - if not by_id["payment_manual_ready"]["passed"]: - actions.append("افتح Moyasar dashboard وجهّز invoice template.") - return actions diff --git a/dealix/auto_client_acquisition/launch_ops/launch_scorecard.py b/dealix/auto_client_acquisition/launch_ops/launch_scorecard.py index 4ef881a7..fbaebc3f 100644 --- a/dealix/auto_client_acquisition/launch_ops/launch_scorecard.py +++ b/dealix/auto_client_acquisition/launch_ops/launch_scorecard.py @@ -1,140 +1,36 @@ -"""Launch scorecard — daily and weekly metrics for Private Beta ops.""" +"""Readiness scorecard for launch — simple weighted score.""" from __future__ import annotations -from collections import defaultdict from typing import Any -# Valid event types the launch scorecard accepts. -VALID_LAUNCH_EVENTS: tuple[str, ...] = ( - "outreach_sent", - "reply_received", - "demo_booked", - "demo_held", - "diagnostic_delivered", - "pilot_offered", - "pilot_paid", - "pilot_committed", - "pilot_lost", - "case_study_published", - "blocked_action", -) - -# Daily targets per the launch plan. -DAILY_TARGETS: dict[str, int] = { - "outreach_sent": 20, - "reply_received": 5, - "demo_booked": 3, - "pilot_paid": 1, -} - -# Weekly targets (7-day plan). -WEEKLY_TARGETS: dict[str, int] = { - "outreach_sent": 100, - "reply_received": 20, - "demo_booked": 10, - "pilot_paid": 2, -} +from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go -def record_launch_event( - *, - event_type: str, - customer_id: str | None = None, - notes: str | None = None, - event_log: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """ - Record a launch event into an in-memory log. - - Returns the appended entry (validated). Raises ValueError on unknown type. - """ - if event_type not in VALID_LAUNCH_EVENTS: - raise ValueError( - f"Unknown launch event: {event_type}. " - f"Valid: {', '.join(VALID_LAUNCH_EVENTS)}" - ) - entry: dict[str, Any] = { - "event_type": event_type, - "customer_id": customer_id, - "notes": (notes or "")[:300], - } - if event_log is not None: - event_log.append(entry) - return entry - - -def _aggregate(events: list[dict[str, Any]]) -> dict[str, int]: - counts: dict[str, int] = defaultdict(int) - for e in events or []: - et = str(e.get("event_type", "")) - counts[et] += 1 - return dict(counts) - - -def build_daily_launch_scorecard( - *, events: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Build today's Arabic launch scorecard from event log.""" - counts = _aggregate(events or []) - metrics = {k: counts.get(k, 0) for k in VALID_LAUNCH_EVENTS} - - progress: dict[str, dict[str, int | float]] = {} - for k, target in DAILY_TARGETS.items(): - actual = metrics.get(k, 0) - pct = round(100 * actual / target, 1) if target else 0.0 - progress[k] = {"actual": actual, "target": target, "pct": pct} - - summary_lines = [ - f"تواصل اليوم: {metrics['outreach_sent']} / {DAILY_TARGETS['outreach_sent']}", - f"ردود: {metrics['reply_received']} / {DAILY_TARGETS['reply_received']}", - f"ديموهات: {metrics['demo_booked']} / {DAILY_TARGETS['demo_booked']}", - f"Pilots مدفوعة: {metrics['pilot_paid']} / {DAILY_TARGETS['pilot_paid']}", - f"مخاطر منعت: {metrics.get('blocked_action', 0)}", - ] - +def build_launch_scorecard(extra: dict[str, Any] | None = None) -> dict[str, Any]: + ex = extra or {} + g = evaluate_go_no_go(ex) + score = 100 + if not g["checks"].get("tests_pass"): + score -= 30 + if not g["checks"].get("routes_ok"): + score -= 15 + if not g["checks"].get("staging_health_ok"): + score -= 10 + if not g["checks"].get("no_secrets_in_repo_scan"): + score -= 40 + if not g["checks"].get("whatsapp_live_send_disabled"): + score -= 25 + if not g["checks"].get("service_catalog_ok"): + score -= 10 + if not g["checks"].get("landing_ready"): + score -= 5 + score = max(0, min(100, score)) + status = "ready" if score >= 75 and g["go"] else "needs_work" return { - "metrics": metrics, - "targets": DAILY_TARGETS, - "progress": progress, - "summary_ar": summary_lines, - } - - -def build_weekly_launch_scorecard( - *, events: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Build the 7-day Arabic launch scorecard.""" - counts = _aggregate(events or []) - metrics = {k: counts.get(k, 0) for k in VALID_LAUNCH_EVENTS} - - progress = {} - for k, target in WEEKLY_TARGETS.items(): - actual = metrics.get(k, 0) - pct = round(100 * actual / target, 1) if target else 0.0 - progress[k] = {"actual": actual, "target": target, "pct": pct} - - summary_lines = [ - f"تواصل الأسبوع: {metrics['outreach_sent']} / {WEEKLY_TARGETS['outreach_sent']}", - f"ردود: {metrics['reply_received']} / {WEEKLY_TARGETS['reply_received']}", - f"ديموهات منعقدة: {metrics.get('demo_held', 0)}", - f"Pilots مدفوعة: {metrics['pilot_paid']} / {WEEKLY_TARGETS['pilot_paid']}", - f"Pilots commitments: {metrics.get('pilot_committed', 0)}", - f"Pilots خسرت: {metrics.get('pilot_lost', 0)}", - f"مخاطر منعت: {metrics.get('blocked_action', 0)}", - ] - - if metrics["pilot_paid"] >= WEEKLY_TARGETS["pilot_paid"]: - verdict = "on_track" - elif metrics["demo_booked"] >= 5: - verdict = "promising" - else: - verdict = "needs_focus" - - return { - "metrics": metrics, - "targets": WEEKLY_TARGETS, - "progress": progress, - "summary_ar": summary_lines, - "verdict": verdict, + "readiness_score": score, + "status": status, + "go_no_go": g, + "summary_ar": f"درجة الجاهزية {score}/١٠٠ — {status}.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/launch_ops/outreach_messages.py b/dealix/auto_client_acquisition/launch_ops/outreach_messages.py index a62f21b4..e1451d2e 100644 --- a/dealix/auto_client_acquisition/launch_ops/outreach_messages.py +++ b/dealix/auto_client_acquisition/launch_ops/outreach_messages.py @@ -1,188 +1,64 @@ -"""First 20 outreach segments + per-segment Arabic messages + reply handlers.""" +"""First outreach batch — templates for warm outbound (manual send only).""" from __future__ import annotations from typing import Any -def build_first_20_segments() -> dict[str, Any]: - """The deterministic first-20 plan — 4 segments × 5 prospects each.""" +def build_first_twenty_outreach() -> dict[str, Any]: return { - "total_targets": 20, - "segments": [ + "count": 20, + "disclaimer_ar": "هذه قوالب للنسخ اليدوي — لا يُرسل من Dealix تلقائياً.", + "messages": [ { - "id": "agency_b2b", - "label_ar": "وكالات تسويق B2B", - "count": 5, - "best_offer_id": "agency_partner_program", - "fallback_offer_id": "partner_sprint", - "primary_channel": "email", + "id": 1, + "audience_ar": "مؤسس B2B", + "subject_ar": "تجربة بيتا — ١٠ فرص خلال أسبوع", + "body_ar": ( + "هلا [الاسم]، نجرب Dealix كمدير نمو عربي: نطلع لك فرصاً مناسبة، " + "نكتب رسائل عربية، وأنت توافق قبل أي تواصل. مهتم نعرض لك عينة قصيرة؟" + ), }, { - "id": "training_consulting", - "label_ar": "شركات تدريب واستشارات", - "count": 5, - "best_offer_id": "first_10_opportunities_sprint", - "fallback_offer_id": "free_growth_diagnostic", - "primary_channel": "email", + "id": 2, + "audience_ar": "وكالة تسويق", + "subject_ar": "Pilot مشترك مع وكالة", + "body_ar": ( + "هلا [الاسم]، نبحث وكالة واحدة لتجربة Dealix على عميل حقيقي: " + "فرص + مسودات + Proof Pack. يناسبكم ديمو ١٥ دقيقة؟" + ), }, { - "id": "saas_tech_small", - "label_ar": "SaaS / تقنية صغيرة", - "count": 5, - "best_offer_id": "first_10_opportunities_sprint", - "fallback_offer_id": "growth_os_monthly", - "primary_channel": "linkedin_lead_form", + "id": 3, + "audience_ar": "شركة تدريب", + "subject_ar": "فرص شركات في قطاعكم", + "body_ar": ( + "هلا [الاسم]، Dealix يساعد فرق التدريب تطلع فرص B2B مع سبب «لماذا الآن» " + "ومسودات عربية. نقدر نرسل لكم تشخيصاً مجانياً مختصراً؟" + ), }, { - "id": "services_with_whatsapp", - "label_ar": "شركات خدمات لديها واتساب نشط", - "count": 5, - "best_offer_id": "list_intelligence", - "fallback_offer_id": "whatsapp_compliance_setup", - "primary_channel": "email", + "id": 4, + "audience_ar": "SaaS صغير", + "subject_ar": "Pipeline بدون فوضى قنوات", + "body_ar": ( + "هلا [الاسم]، إذا عندكم قائمة عملاء أو leads، نقدر نصنّفها ونطلع أفضل أهداف " + "مع تقرير مخاطر — بدون إرسال واتساب بارد. مهتم؟" + ), + }, + { + "id": 5, + "audience_ar": "متابعة ١", + "subject_ar": "متابعة خفيفة", + "body_ar": "تذكير لطيف: إذا يناسبكم، أرسل لي قطاعكم ومدينتكم وأجهز عينة خلال ٢٤ ساعة.", + }, + { + "id": 6, + "audience_ar": "متابعة ٢", + "subject_ar": "إغلاق مهذب", + "body_ar": "إذا التوقيت مو مناسب، أقدر أرجع بعد أسبوعين — أو أغلق الملف عندكم برسالة «لا شكراً».", }, ], - "rules_ar": [ - "لا scraping ولا قوائم مشتراة.", - "استخدم علاقاتك المباشرة + جهات تعرفها.", - "كل رسالة يدوية، لا automation.", - "حد أقصى 3 follow-ups ثم أرشفة.", - ], - } - - -_BASE_INTRO = "هلا [الاسم]، أطلقنا Beta محدودة لـ Dealix." - - -def build_outreach_message(segment_id: str, *, name: str = "[الاسم]") -> dict[str, Any]: - """Build the first-touch Arabic message for a segment.""" - intro = f"هلا {name}،" - - if segment_id == "agency_b2b": - body = ( - f"{intro} عندي Beta خاص للوكالات.\n\n" - "Dealix يساعد الوكالة تطلع فرص لعملائها، تجهز رسائل عربية، تدير " - "موافقات، وتطلع Proof Pack باسم الوكالة والعميل.\n\n" - "أبحث عن وكالة واحدة نجرب معها Pilot مشترك على عميل حقيقي. " - "يناسبك ديمو 15 دقيقة؟" - ) - elif segment_id == "training_consulting": - body = ( - f"{intro} متابع توسع شركتكم في برامج الشركات.\n\n" - "Dealix يطلع لكم 10 فرص B2B خلال 7 أيام، يكتب الرسائل بالعربي، " - "ويخلي صاحب القرار يوافق قبل أي تواصل، وبعدها يعطي Proof Pack.\n\n" - "Pilot بـ499 ريال أو مجاني مقابل case study. يناسبك ديمو 12 دقيقة؟" - ) - elif segment_id == "saas_tech_small": - body = ( - f"{intro} رأيت إصدار النسخة الجديدة من منتجكم — مبروك.\n\n" - "نشتغل على مدير نمو عربي يطلع 10 فرص B2B، يستخدم LinkedIn Lead " - "Forms (لا scraping)، ويكتب الرسائل بالعربي.\n\n" - "أبغى أجربه مع شركة SaaS سعودية واحدة. يناسبك ديمو 12 دقيقة؟" - ) - elif segment_id == "services_with_whatsapp": - body = ( - f"{intro} عندكم قاعدة عملاء واتساب نشطة، صحيح؟\n\n" - "Dealix ينظف القائمة، يصنف الـ opt-in، يحظر cold WhatsApp تلقائياً، " - "ويكتب رسائل عربية للحملات الآمنة + Proof Pack شهري.\n\n" - "List Intelligence بـ499–1,500 ريال. يناسبك أعطيك تشخيص مجاني أولاً؟" - ) - else: - body = ( - f"{intro} {_BASE_INTRO}\n\n" - "Dealix يطلع لك 10 فرص B2B + رسائل عربية + Proof Pack — " - "وأنت توافق قبل أي تواصل. Pilot 7 أيام بـ499 ريال. " - "يناسبك ديمو 12 دقيقة؟" - ) - - return { - "segment_id": segment_id, - "channel": "email_or_dm", - "body_ar": body, - "approval_required": True, - "live_send_allowed": False, - } - - -def build_followup_message( - segment_id: str, *, step: int = 1, name: str = "[الاسم]", -) -> dict[str, Any]: - """Build follow-up #1, #2, or #3 (final archive).""" - if step <= 1: - body = ( - f"هلا {name}، أرسل لك مثال سريع بدل شرح طويل؟\n" - "أقدر أطلع لك عينة من 3 فرص مناسبة لشركتكم + رسالة واحدة جاهزة + " - "ملاحظة عن أفضل قناة. إذا أعجبتك نكمل Pilot كامل." - ) - kind = "followup_1" - elif step == 2: - body = ( - f"هلا {name}، أعرف أن وقتك مزدحم.\n" - "سؤال أخير: لو طلعت لك 3 فرص B2B بالعربي مجاناً هذا الأسبوع، " - "تعطيني 15 دقيقة feedback؟" - ) - kind = "followup_2" - else: - body = ( - f"هلا {name}، أعتذر على الإلحاح.\n" - "أرشّفها وأكون موجود لو احتجتني لاحقاً. شاكر لك." - ) - kind = "followup_3_final" - - return { - "segment_id": segment_id, - "step": step, - "kind": kind, - "body_ar": body, - "approval_required": True, - "live_send_allowed": False, - } - - -def build_reply_handlers() -> dict[str, dict[str, str]]: - """Standard reply-classifier → response mapping (Arabic).""" - return { - "interested": { - "label_ar": "مهتم", - "response_ar": ( - "ممتاز. أرسل لك intake form + موعد ديمو 12 دقيقة هذا الأسبوع. " - "أي وقت يناسبك بين 10 ص و 5 م؟" - ), - "next_action": "send_intake_and_demo_link", - }, - "needs_more_info": { - "label_ar": "يحتاج معلومات أكثر", - "response_ar": ( - "أرسل لك Free Growth Diagnostic — 3 فرص + رسالة + توصية، " - "بدون التزام. أحتاج فقط: قطاعكم، مدينتكم، عرضكم الرئيسي." - ), - "next_action": "send_free_diagnostic_intake", - }, - "price_objection": { - "label_ar": "اعتراض سعر", - "response_ar": ( - "تمام، نبدأ بـ Free Diagnostic مجاناً. " - "تشوفون النتائج قبل أي دفع." - ), - "next_action": "send_free_diagnostic_intake", - }, - "not_now": { - "label_ar": "ليس الآن", - "response_ar": ( - "تمام، شاكر لك. أتواصل معك بعد شهرين بدون إلحاح. " - "إن احتجتنا قبل، أنا موجود." - ), - "next_action": "schedule_followup_60_days", - }, - "no_thanks": { - "label_ar": "غير مهتم", - "response_ar": "تمام، شاكر لك. أرشّفها وأتمنى لكم التوفيق.", - "next_action": "archive", - }, - "unsubscribe": { - "label_ar": "إلغاء", - "response_ar": "تم. لن أتواصل معك مجدداً.", - "next_action": "honor_opt_out_immediately", - }, + "note_ar": "كرّر وأكيّف الرسائل ٤–٦ لباقي الـ ٢٠ جهة — نفس النبرة الدافئة.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/launch_ops/private_beta.py b/dealix/auto_client_acquisition/launch_ops/private_beta.py index c4c4344a..3ea280a3 100644 --- a/dealix/auto_client_acquisition/launch_ops/private_beta.py +++ b/dealix/auto_client_acquisition/launch_ops/private_beta.py @@ -1,110 +1,28 @@ -"""Private Beta offer — today's offer + safety notes + FAQ.""" +"""Private beta commercial offer — deterministic copy.""" from __future__ import annotations from typing import Any -PRIVATE_BETA_OFFER: dict[str, Any] = { - "offer_id": "private_beta_pilot_7d", - "name_ar": "Private Beta Pilot — 7 أيام", - "promise_ar": ( - "خلال 7 أيام نطلع لك 10 فرص B2B + رسائل عربية + خطة متابعة + Proof Pack، " - "وأنت توافق قبل أي تواصل." - ), - "deliverables_ar": [ - "10 فرص B2B مع why-now + buying committee.", - "10 رسائل عربية بنبرة سعودية طبيعية.", - "تصنيف القنوات (safe / needs_review / blocked) لكل contact.", - "خطة متابعة 7 أيام.", - "Proof Pack مختصر (PDF + JSON).", - "جلسة مراجعة 30 دقيقة في نهاية الأسبوع.", - ], - "price_sar": 499, - "free_alternative_ar": "مجاني مقابل case study بعد انتهاء الـ Pilot.", - "approval_required": True, - "live_send_allowed": False, - "duration_days": 7, - "seats_available": 5, -} - -def build_private_beta_offer(*, seats_remaining: int | None = None) -> dict[str, Any]: - """Build today's Private Beta offer card. Seats are configurable.""" - out = dict(PRIVATE_BETA_OFFER) - if seats_remaining is not None: - out["seats_available"] = max(0, int(seats_remaining)) - out["upsell_path"] = [ - "growth_os_pilot_30d", - "growth_os_monthly", - ] - return out - - -def build_private_beta_safety_notes() -> dict[str, Any]: - """Return the explicit 'what we will NOT do today' list.""" +def build_private_beta_offer() -> dict[str, Any]: return { - "title_ar": "ضمانات Dealix", - "do_not_do_ar": [ - "لا live WhatsApp send بدون env flag + اعتماد بشري.", - "لا live Gmail send.", - "لا Calendar insert تلقائي.", - "لا charge Moyasar تلقائي — invoice/payment link يدوي فقط.", - "لا scraping LinkedIn ولا auto-DM.", - "لا cold WhatsApp (PDPL).", - "لا وعود بنتائج مضمونة.", - "لا تخزين بيانات بطاقات.", + "title_ar": "Dealix — البيتا الخاصة", + "tagline_ar": "مدير نمو عربي: فرص، مسودات، موافقة، Proof — بدون إرسال حي افتراضياً.", + "included_ar": [ + "تشخيص نمو مجاني أو مدفوع حسب الاتفاق", + "سباق ١٠ فرص أو ذكاء قائمة (حسب الحالة)", + "كروت موافقة عربية (أزرار ≤٣)", + "Proof Pack أسبوعي تجريبي", ], - "do_ar": [ - "Approval-first في كل قناة.", - "Audit ledger لكل فعل.", - "Saudi Tone + Safety eval قبل أي رسالة.", - "Reputation Guard يوقف القناة عند تدهور السمعة.", - "Free Diagnostic قبل أي التزام.", + "excluded_ar": [ + "إرسال واتساب جماعي بارد", + "Gmail إرسال تلقائي", + "إدراج تقويم حي بدون موافقة", + "شحن بطاقات داخل Dealix", ], + "pilot_pricing_sar": {"low": 499, "high": 3000, "note_ar": "٧ أيام أو ٣٠ يوم — حسب النطاق"}, + "monthly_after_sar": {"low": 2999, "high": 9999}, + "live_send_default": False, + "demo": True, } - - -def private_beta_faq() -> list[dict[str, str]]: - """Common Arabic FAQ entries for the Private Beta page.""" - return [ - { - "q_ar": "كيف يعمل Pilot الـ7 أيام؟", - "a_ar": ( - "نأخذ منك intake (قطاع/مدينة/عرض/هدف) خلال 30 دقيقة. " - "خلال 24 ساعة عمل نسلّم 10 فرص + رسائل + تصنيف القنوات. " - "خلال الأسبوع نتابع الردود ونحدّث Proof Pack." - ), - }, - { - "q_ar": "هل ترسلون رسائل بدون موافقتي؟", - "a_ar": "لا. كل رسالة تظل draft حتى توافق عليها صراحة.", - }, - { - "q_ar": "ماذا لو ما رد أحد؟", - "a_ar": ( - "Proof Pack يوضح المخاطر التي منعناها + توصية بقطاع/زاوية مختلفة. " - "Pilot يثبت طريقة التشغيل وليس عدداً مضموناً من الصفقات." - ), - }, - { - "q_ar": "هل تعرفون شروط واتساب ولينكدإن؟", - "a_ar": ( - "نعم. لا cold WhatsApp بدون opt-in. " - "لا scraping ولا auto-DM في LinkedIn — نستخدم Lead Gen Forms والمهام اليدوية." - ), - }, - { - "q_ar": "كيف أدفع 499 ريال؟", - "a_ar": ( - "نرسل لك Moyasar invoice أو payment link من الـ dashboard. " - "بعد الدفع نبدأ Pilot يوم الأحد التالي." - ), - }, - { - "q_ar": "هل يصلح للوكالات؟", - "a_ar": ( - "نعم — Agency Partner Program يعطي الوكالة co-branded Proof Pack + " - "revenue share على عملائها. تواصل معنا مباشرة للترتيب." - ), - }, - ] diff --git a/dealix/auto_client_acquisition/meeting_intelligence/__init__.py b/dealix/auto_client_acquisition/meeting_intelligence/__init__.py index e89cbad6..9680be6d 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/__init__.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/__init__.py @@ -1,25 +1,11 @@ -"""Meeting Intelligence — pre-meeting briefs + post-meeting follow-ups. +"""Meeting intelligence — transcript text to brief/follow-up (no Calendar insert).""" -Designed to consume Google Meet transcripts (when OAuth + scopes allow) but -works fine with manually-pasted transcripts during private beta. - -All outputs are Arabic, deterministic, and approval-required before any -external action. -""" - -from __future__ import annotations - -from .deal_risk import compute_deal_risk -from .followup_builder import build_post_meeting_followup -from .meeting_brief import build_pre_meeting_brief -from .objection_extractor import extract_objections -from .transcript_parser import parse_transcript_entries, summarize_meeting +from auto_client_acquisition.meeting_intelligence.followup_builder import build_post_meeting_followup +from auto_client_acquisition.meeting_intelligence.meeting_brief import build_pre_meeting_brief +from auto_client_acquisition.meeting_intelligence.transcript_parser import summarize_transcript_text __all__ = [ "build_post_meeting_followup", "build_pre_meeting_brief", - "compute_deal_risk", - "extract_objections", - "parse_transcript_entries", - "summarize_meeting", + "summarize_transcript_text", ] diff --git a/dealix/auto_client_acquisition/meeting_intelligence/deal_risk.py b/dealix/auto_client_acquisition/meeting_intelligence/deal_risk.py index a53425de..c5145bc5 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/deal_risk.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/deal_risk.py @@ -1,81 +1,18 @@ -"""Deal risk score from meeting + objection signals.""" +"""Deal risk hint from simple signals.""" from __future__ import annotations from typing import Any -def compute_deal_risk( - *, - objections: list[dict[str, Any]] | None = None, - next_step_set: bool = False, - decision_maker_present: bool = False, - days_since_last_touch: int = 0, - expected_value_sar: float = 0.0, -) -> dict[str, Any]: - """ - Compute a deal-level risk score (0..100) from meeting outcomes. - - Higher = riskier. Returns deterministic Arabic risk reasons. - """ - objections = objections or [] - score = 0 - reasons_ar: list[str] = [] - - # Objection-based risk. - categories = {str(o.get("category", "")).lower() for o in objections} - if "price" in categories: - score += 20 - reasons_ar.append("اعتراض على السعر — يحتاج إثبات قيمة وعينة محسوبة.") - if "timing" in categories: - score += 15 - reasons_ar.append("اعتراض توقيت — احفظ الفرصة لربع لاحق.") - if "authority" in categories: - score += 25 - reasons_ar.append("صاحب القرار غير حاضر — يلزم اجتماع ثانٍ معه.") - if "trust" in categories: - score += 20 - reasons_ar.append("قلق أمان/خصوصية — أرفق DPA و PDPL.") - if "integration" in categories: - score += 10 - reasons_ar.append("قلق تكامل — حضّر مخطط ربط CRM.") - if "competitor" in categories: - score += 15 - reasons_ar.append("بديل قائم — جهّز battlecard مقارنة.") - - # Process risk. - if not next_step_set: - score += 25 - reasons_ar.append("لم يتم تحديد خطوة تالية بتاريخ — أعلى مؤشر فقدان.") - if not decision_maker_present: - score += 10 - reasons_ar.append("صانع القرار لم يحضر الاجتماع.") - if days_since_last_touch > 14: - score += 10 - reasons_ar.append( - f"مرّ {days_since_last_touch} يوم على آخر تواصل — فرصة باردة." - ) - - # Cap. - score = max(0, min(100, score)) - - if score >= 70: - risk_level = "high" - elif score >= 40: - risk_level = "medium" - else: - risk_level = "low" - - return { - "risk_score": score, - "risk_level": risk_level, - "reasons_ar": reasons_ar, - "expected_value_sar": expected_value_sar, - "recommended_action_ar": ( - "اجتماع ثانٍ مع صاحب القرار خلال 5 أيام + مادة إثبات قيمة قصيرة." - if risk_level == "high" else - "متابعة خلال 3 أيام مع خطوة تالية محددة." - if risk_level == "medium" else - "تنفيذ الخطوة التالية المتفق عليها كما هي." - ), - } +def assess_deal_risk(signals: dict[str, Any] | None = None) -> dict[str, Any]: + s = signals or {} + risk = "low" + reasons: list[str] = [] + if s.get("no_followup_scheduled"): + risk = "medium" + reasons.append("لا يوجد موعد متابعة بعد الاجتماع.") + if s.get("ghosted_after_proposal"): + risk = "high" + reasons.append("توقف التواصل بعد العرض.") + return {"risk_level": risk, "reasons_ar": reasons, "demo": True} diff --git a/dealix/auto_client_acquisition/meeting_intelligence/followup_builder.py b/dealix/auto_client_acquisition/meeting_intelligence/followup_builder.py index 1d87002d..25f83b29 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/followup_builder.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/followup_builder.py @@ -1,72 +1,15 @@ -"""Build a post-meeting follow-up draft (Arabic) — never sends.""" +"""Post-meeting follow-up draft (Arabic).""" from __future__ import annotations from typing import Any -def build_post_meeting_followup( - *, - summary: dict[str, Any] | None = None, - next_steps: list[str] | None = None, - contact_name: str = "", - company_name: str = "", - objections: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """ - Build a draft follow-up email/WhatsApp message in Arabic. - - Always returns approval_required=True; never executes a send. - """ - next_steps = next_steps or [] - objections = objections or [] - - salutation = f"هلا {contact_name}" if contact_name else "هلا" - company_part = f" من شركة {company_name}" if company_name else "" - - bullet_steps = "\n".join([f"• {s}" for s in next_steps]) or "• [حدد الخطوة التالية بتاريخ محدد]" - - objection_addressed = "" - if objections: - labels = sorted({str(o.get("label_ar", "")) for o in objections if o.get("label_ar")}) - if labels: - objection_addressed = ( - "\nرجعت بعد الاجتماع وفكرت في النقاط التي ذكرتها: " - + "، ".join(labels) - + ". أرفقت لك إجابات قصيرة مع أمثلة." - ) - - body_ar = ( - f"{salutation}،\n" - f"شكراً على وقتك اليوم{company_part}. " - "ملخص ما اتفقنا عليه:\n" - f"{bullet_steps}\n" - f"{objection_addressed}\n" - "\nإذا كل شي واضح من جهتك، أبدأ في تجهيز Pilot قصير ونشتغل خلال أسبوع. " - "أي ملاحظة تحب تضيفها قبل ما نبدأ؟\n\nشاكر لك." +def build_post_meeting_followup(summary_ar: str, next_steps: list[str] | None = None) -> dict[str, Any]: + steps = next_steps or ["إرسال ملخص موافق عليه", "تحديد موعد متابعة", "مشاركة مسودة عرض مختصرة"] + body = ( + f"شكراً لوقتكم. الملخص: {summary_ar[:200]}…\n" + f"الخطوات المقترحة: {'؛ '.join(steps)}.\n" + "ننتظر تأكيدكم للمتابعة." ) - - subject_ar = f"متابعة اجتماع اليوم — {company_name or 'Dealix'}" - - return { - "channel_drafts": { - "email": { - "subject_ar": subject_ar, - "body_ar": body_ar, - "approval_required": True, - "live_send_allowed": False, - }, - "whatsapp": { - "body_ar": ( - f"{salutation}، شكراً على اجتماع اليوم. " - "الخطوة التالية: " + (next_steps[0] if next_steps else "نحدد موعد بداية الـPilot") + - ". أتابع معك خلال يومين." - ), - "approval_required": True, - "live_send_allowed": False, - }, - }, - "summary_used": bool(summary), - "objections_addressed": [str(o.get("label_ar")) for o in objections if o.get("label_ar")], - "approval_required": True, - } + return {"subject_ar": "متابعة — ملخص الاجتماع والخطوة التالية", "body_ar": body, "approval_required": True, "demo": True} diff --git a/dealix/auto_client_acquisition/meeting_intelligence/meeting_brief.py b/dealix/auto_client_acquisition/meeting_intelligence/meeting_brief.py index 9a2a431b..c8b96d13 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/meeting_brief.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/meeting_brief.py @@ -1,4 +1,4 @@ -"""Pre-meeting brief builder — deterministic Arabic output.""" +"""Pre-meeting brief from company/contact context.""" from __future__ import annotations @@ -6,69 +6,24 @@ from typing import Any def build_pre_meeting_brief( - *, company: dict[str, Any] | None = None, contact: dict[str, Any] | None = None, opportunity: dict[str, Any] | None = None, - sector: str | None = None, ) -> dict[str, Any]: - """ - Build a 6-section Arabic pre-meeting brief. - - All inputs are optional; the brief degrades to a generic but useful template. - """ - company = company or {} - contact = contact or {} - opportunity = opportunity or {} - sector = sector or str(company.get("sector", "saas")) - - company_name = company.get("name", "?") - contact_name = contact.get("name", "?") - contact_role = contact.get("role", "?") - deal_value = opportunity.get("expected_value_sar", 0) - - objective_ar = ( - f"توضيح ملاءمة الحل لشركة {company_name}، " - f"وفهم المعيار الذي يستخدمه {contact_name} للقرار، " - "ثم تحديد خطوة تالية واضحة." - ) - - questions_ar = [ - f"كيف تتعاملون اليوم مع [مشكلة قطاع {sector}]؟", - "ما الذي جعلكم تنظرون لحل الآن وليس قبل 6 أشهر؟", - "من المسؤول عن قرار الشراء غيرك؟", - "ما المعيار الذي يجعلكم تقولون: نعم، خلونا نبدأ؟", - "ما الميزانية التقريبية المخصصة لهذه المشكلة؟", - ] - - likely_objections_ar = [ - "السعر مرتفع مقارنة بالأدوات المحلية.", - "نحن مرتبطون بـ CRM/أداة حالية ولا نريد التبديل.", - "نحتاج تجربة فريق صغير أولاً قبل القرار.", - "هل الحل متوافق مع PDPL ولا يخزن بياناتنا خارج المملكة؟", - "كم يستغرق الإعداد فعلياً؟", - ] - - offer_skeleton_ar = ( - f"عرض pilot لمدة 7 أيام لشركة {company_name}: " - "10 فرص B2B + رسائل عربية + متابعة + Proof Pack. " - "السعر 499 ريال أو مجاني مقابل case study." - ) - - next_step_ar = ( - "في نهاية المكالمة: اقترح خطوة محددة بتاريخ — " - "إما الموافقة على بدء Pilot، أو إعادة الاجتماع خلال 5 أيام مع صانع القرار." - ) - + c = company or {} + p = contact or {} + o = opportunity or {} return { - "company_name": company_name, - "contact_name": contact_name, - "contact_role": contact_role, - "expected_value_sar": deal_value, - "objective_ar": objective_ar, - "questions_ar": questions_ar, - "likely_objections_ar": likely_objections_ar, - "offer_skeleton_ar": offer_skeleton_ar, - "next_step_ar": next_step_ar, - "approval_required": True, + "company_ar": str(c.get("name") or c.get("company_name") or "الشركة"), + "contact_ar": str(p.get("name") or "جهة الاتصال"), + "objective_ar": str(o.get("objective_ar") or "مناقشة ملاءمة الحل والخطوة التالية."), + "questions_ar": [ + "ما معيار القرار والجدول الزمني؟", + "ما أكبر مخاطرة يرونها اليوم؟", + "ما الشكل المثالي للتجربة خلال ٧ أيام؟", + "ما الميزانية أو نطاقها التقريبي؟", + "من يشارك من جانبهم في التنفيذ؟", + ], + "likely_objections_ar": ["السعر", "التوقيت", "التكامل مع الأنظمة الحالية"], + "demo": True, } diff --git a/dealix/auto_client_acquisition/meeting_intelligence/objection_extractor.py b/dealix/auto_client_acquisition/meeting_intelligence/objection_extractor.py index d874fd11..abccd123 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/objection_extractor.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/objection_extractor.py @@ -1,52 +1,17 @@ -"""Objection extractor — find common Arabic + English buying objections in transcript.""" +"""Extract objection-like phrases from transcript text — keyword MVP.""" from __future__ import annotations import re +from typing import Any -# Each entry: (category, regex pattern (case-insensitive), Arabic gloss). -OBJECTION_PATTERNS: tuple[tuple[str, str, str], ...] = ( - ("price", r"غالي|مرتفع|الميزانية|expensive|too\s+pricey|cost", "السعر/الميزانية"), - ("timing", r"ليس\s+الآن|بعد\s+شهر|الربع\s+القادم|not\s+now|next\s+quarter", "التوقيت"), - ("authority", r"المدير|صاحب\s+القرار|need\s+approval|decision\s+maker", "صاحب القرار"), - ("trust", r"بيانات|خصوصية|أمان|PDPL|trust|security|privacy", "الأمان والخصوصية"), - ("integration", r"CRM|نظامنا|الربط|integration|migration", "التكامل/الترحيل"), - ("competitor", r"نستخدم|بديل|أداة\s+ثانية|competitor|alternative", "وجود بديل/منافس"), - ("results", r"نتائج|مضمون|guarantee|ROI|دليل", "إثبات النتائج"), - ("complexity", r"معقد|صعب|تدريب|onboarding|complex|hard", "التعقيد/التبني"), -) +_KEYWORDS = ("ميزانية", "غالي", "لاحقاً", "نراجع", "ليس أولوية", "تكامل", "أمان", "عقد", "منافس") -def extract_objections(transcript_text: str) -> dict[str, object]: - """ - Extract objection categories from a free-text transcript. - - Returns: - { - "objections": [{"category", "label_ar", "snippet"}], - "categories_found": [str], - "count": int, - } - """ - if not transcript_text: - return {"objections": [], "categories_found": [], "count": 0} - - found: list[dict[str, str]] = [] - seen_categories: set[str] = set() - for cat, pattern, gloss in OBJECTION_PATTERNS: - for m in re.finditer(pattern, transcript_text, flags=re.IGNORECASE): - seen_categories.add(cat) - start = max(0, m.start() - 40) - end = min(len(transcript_text), m.end() + 40) - snippet = transcript_text[start:end].replace("\n", " ").strip() - found.append({ - "category": cat, - "label_ar": gloss, - "snippet": snippet[:200], - }) - - return { - "objections": found, - "categories_found": sorted(seen_categories), - "count": len(found), - } +def extract_objections(transcript_text: str) -> dict[str, Any]: + text = transcript_text or "" + found: list[str] = [] + for kw in _KEYWORDS: + if re.search(re.escape(kw), text, flags=re.IGNORECASE): + found.append(kw) + return {"objections_ar": list(dict.fromkeys(found))[:8], "demo": True} diff --git a/dealix/auto_client_acquisition/meeting_intelligence/transcript_parser.py b/dealix/auto_client_acquisition/meeting_intelligence/transcript_parser.py index 720b77ce..0dda6f15 100644 --- a/dealix/auto_client_acquisition/meeting_intelligence/transcript_parser.py +++ b/dealix/auto_client_acquisition/meeting_intelligence/transcript_parser.py @@ -1,4 +1,4 @@ -"""Transcript parser — accepts Google Meet entries OR plain text.""" +"""Parse plain-text transcript lines into a short Arabic summary.""" from __future__ import annotations @@ -6,87 +6,13 @@ import re from typing import Any -def parse_transcript_entries(entries: list[dict[str, Any]] | str) -> dict[str, Any]: - """ - Normalize either: - - a list of Google-Meet-shaped entries [{"participantId", "text", ...}], or - - a plain string transcript with "Speaker: text" lines. - - Returns: - { - "speaker_turns": [{"speaker", "text"}], - "speakers": [str], - "total_chars": int, - "total_turns": int, - } - """ - speaker_turns: list[dict[str, str]] = [] - - if isinstance(entries, str): - for raw in entries.splitlines(): - line = raw.strip() - if not line: - continue - m = re.match(r"^([^:]{1,40}):\s*(.+)$", line) - if m: - speaker_turns.append({"speaker": m.group(1).strip(), - "text": m.group(2).strip()}) - else: - speaker_turns.append({"speaker": "?", "text": line}) - else: - for e in entries or []: - speaker = ( - e.get("participant") - or e.get("participantId") - or e.get("speaker") - or "?" - ) - text = e.get("text") or e.get("content") or "" - text = str(text).strip() - if not text: - continue - speaker_turns.append({"speaker": str(speaker), "text": text}) - - speakers = sorted({t["speaker"] for t in speaker_turns}) - total_chars = sum(len(t["text"]) for t in speaker_turns) +def summarize_transcript_text(text: str) -> dict[str, Any]: + lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()] + bullets = lines[:5] if lines else ["لا يوجد نص كافٍ."] + word_count = len(re.findall(r"\w+", text or "", flags=re.UNICODE)) return { - "speaker_turns": speaker_turns, - "speakers": speakers, - "total_chars": total_chars, - "total_turns": len(speaker_turns), - } - - -def summarize_meeting(parsed: dict[str, Any]) -> dict[str, Any]: - """ - Produce an Arabic summary skeleton from parsed turns. - - Deterministic; LLM-free for Phase D MVP. - """ - turns = parsed.get("speaker_turns", []) - speakers = parsed.get("speakers", []) - - # Extract a few candidate "topic" sentences: longest turns. - sorted_by_len = sorted(turns, key=lambda t: -len(t["text"]))[:5] - topic_lines = [t["text"][:200] for t in sorted_by_len] - - # Detect questions. - questions: list[str] = [] - for t in turns: - text = t["text"] - if "؟" in text or text.rstrip().endswith("?"): - questions.append(text[:200]) - if len(questions) >= 5: - break - - return { - "summary_ar": [ - f"شارك في الاجتماع {len(speakers)} متحدث.", - f"إجمالي عدد الأدوار الكلامية: {parsed.get('total_turns', 0)}.", - "أبرز نقاط النقاش (مرشحة آلياً، تحتاج مراجعة):", - *[f"• {line}" for line in topic_lines], - ], - "speakers": speakers, - "candidate_questions_ar": questions, - "approval_required": True, + "bullets_ar": bullets, + "word_count": word_count, + "demo": True, + "note_ar": "ملخص من نص خام — ربط Google Meet API لاحقاً مع OAuth.", } diff --git a/dealix/auto_client_acquisition/model_router/__init__.py b/dealix/auto_client_acquisition/model_router/__init__.py index e6743e2e..431268ab 100644 --- a/dealix/auto_client_acquisition/model_router/__init__.py +++ b/dealix/auto_client_acquisition/model_router/__init__.py @@ -1,29 +1,5 @@ -"""Model Router — pick the right model/provider for each task type, with fallback.""" +"""Model routing hints by task type — configuration only, no vendor calls.""" -from __future__ import annotations +from auto_client_acquisition.model_router.task_router import list_tasks, route_task -from .cost_policy import CostClass, classify_cost -from .fallback_policy import build_fallback_chain -from .provider_registry import ( - ALL_PROVIDERS, - ALL_TASK_TYPES, - Provider, - TaskType, - get_provider, -) -from .task_router import RouteDecision, route_task -from .usage_dashboard import build_usage_demo - -__all__ = [ - "ALL_PROVIDERS", - "ALL_TASK_TYPES", - "CostClass", - "Provider", - "RouteDecision", - "TaskType", - "build_fallback_chain", - "build_usage_demo", - "classify_cost", - "get_provider", - "route_task", -] +__all__ = ["list_tasks", "route_task"] diff --git a/dealix/auto_client_acquisition/model_router/provider_registry.py b/dealix/auto_client_acquisition/model_router/provider_registry.py index 1bb47f32..5cb80907 100644 --- a/dealix/auto_client_acquisition/model_router/provider_registry.py +++ b/dealix/auto_client_acquisition/model_router/provider_registry.py @@ -1,171 +1,16 @@ -"""Registry of model providers + task types.""" +"""Static provider labels for routing display.""" from __future__ import annotations -from dataclasses import dataclass, field +from typing import Any -# Task types Dealix actually routes. -ALL_TASK_TYPES: tuple[str, ...] = ( - "strategic_reasoning", - "arabic_copywriting", - "classification", - "compliance_guardrail", - "meeting_analysis", - "vision_analysis", - "extraction", - "summarization", - "coding_project_understanding", - "low_cost_bulk", -) +_PROVIDERS: list[dict[str, Any]] = [ + {"id": "anthropic", "label": "Anthropic", "tasks_default": ["strategic_reasoning", "arabic_copywriting"]}, + {"id": "openai", "label": "OpenAI", "tasks_default": ["classification", "summarization"]}, + {"id": "google", "label": "Google Gemini", "tasks_default": ["vision_analysis", "meeting_analysis"]}, + {"id": "groq", "label": "Groq", "tasks_default": ["low_cost_bulk", "extraction"]}, +] -@dataclass(frozen=True) -class Provider: - """A model provider entry.""" - key: str - label: str - family: str # "anthropic" | "openai" | "google" | "azure" | "local" - capabilities: tuple[str, ...] # subset of ALL_TASK_TYPES - cost_class: str # "low" | "mid" | "high" - latency_class: str # "fast" | "balanced" | "slow" - supports_vision: bool - supports_arabic: bool - privacy_tier: str # "vendor_cloud" | "ksa_region" | "self_hosted" - notes_ar: str = "" - - def to_dict(self) -> dict[str, object]: - return { - "key": self.key, "label": self.label, "family": self.family, - "capabilities": list(self.capabilities), - "cost_class": self.cost_class, "latency_class": self.latency_class, - "supports_vision": self.supports_vision, - "supports_arabic": self.supports_arabic, - "privacy_tier": self.privacy_tier, - "notes_ar": self.notes_ar, - } - - -# Conservative provider list — Dealix can swap any of these without code change. -ALL_PROVIDERS: tuple[Provider, ...] = ( - Provider( - key="claude_sonnet", - label="Claude Sonnet", - family="anthropic", - capabilities=( - "strategic_reasoning", "arabic_copywriting", - "compliance_guardrail", "meeting_analysis", "summarization", - "coding_project_understanding", - ), - cost_class="mid", - latency_class="balanced", - supports_vision=True, - supports_arabic=True, - privacy_tier="vendor_cloud", - notes_ar="مناسب للاستراتيجية والكتابة العربية والامتثال.", - ), - Provider( - key="claude_haiku", - label="Claude Haiku", - family="anthropic", - capabilities=("classification", "extraction", "low_cost_bulk", "summarization"), - cost_class="low", - latency_class="fast", - supports_vision=False, - supports_arabic=True, - privacy_tier="vendor_cloud", - notes_ar="رخيص وسريع — للتصنيف الكثيف والاستخراج.", - ), - Provider( - key="gpt_4_class", - label="GPT-4-class", - family="openai", - capabilities=( - "strategic_reasoning", "vision_analysis", - "coding_project_understanding", "meeting_analysis", - ), - cost_class="high", - latency_class="balanced", - supports_vision=True, - supports_arabic=True, - privacy_tier="vendor_cloud", - notes_ar="بديل قوي للاستراتيجية والرؤية.", - ), - Provider( - key="gpt_4o_mini", - label="GPT-4o mini", - family="openai", - capabilities=("classification", "extraction", "low_cost_bulk"), - cost_class="low", - latency_class="fast", - supports_vision=True, - supports_arabic=True, - privacy_tier="vendor_cloud", - notes_ar="بديل رخيص للمهام الكثيفة.", - ), - Provider( - key="gemini_pro", - label="Gemini Pro", - family="google", - capabilities=( - "vision_analysis", "summarization", "meeting_analysis", - "extraction", - ), - cost_class="mid", - latency_class="balanced", - supports_vision=True, - supports_arabic=True, - privacy_tier="vendor_cloud", - notes_ar="ممتاز للرؤية والاجتماعات.", - ), - Provider( - key="azure_oai_ksa", - label="Azure OpenAI (KSA region)", - family="azure", - capabilities=( - "strategic_reasoning", "arabic_copywriting", - "compliance_guardrail", "extraction", "summarization", - ), - cost_class="mid", - latency_class="balanced", - supports_vision=True, - supports_arabic=True, - privacy_tier="ksa_region", - notes_ar="منطقة KSA — مناسب للعملاء الحساسين للامتثال.", - ), - Provider( - key="local_qwen_ar", - label="Local Qwen (Arabic-tuned)", - family="local", - capabilities=("classification", "extraction", "low_cost_bulk", "arabic_copywriting"), - cost_class="low", - latency_class="balanced", - supports_vision=False, - supports_arabic=True, - privacy_tier="self_hosted", - notes_ar="نموذج محلي — للحالات الحساسة جداً.", - ), -) - - -def get_provider(key: str) -> Provider | None: - return next((p for p in ALL_PROVIDERS if p.key == key), None) - - -@dataclass(frozen=True) -class TaskType: - """Description of a routed task.""" - key: str - label_ar: str - requires_arabic: bool - requires_vision: bool - sensitivity: str # "low" | "medium" | "high" - notes_ar: str = "" - - def to_dict(self) -> dict[str, object]: - return { - "key": self.key, "label_ar": self.label_ar, - "requires_arabic": self.requires_arabic, - "requires_vision": self.requires_vision, - "sensitivity": self.sensitivity, - "notes_ar": self.notes_ar, - } +def list_providers() -> dict[str, Any]: + return {"providers": list(_PROVIDERS), "demo": True} diff --git a/dealix/auto_client_acquisition/model_router/task_router.py b/dealix/auto_client_acquisition/model_router/task_router.py index 9f114403..bffbdeff 100644 --- a/dealix/auto_client_acquisition/model_router/task_router.py +++ b/dealix/auto_client_acquisition/model_router/task_router.py @@ -1,103 +1,30 @@ -"""Route a task to the right provider, with fallback chain + cost class.""" +"""Map task types to suggested provider + cost class — deterministic.""" from __future__ import annotations -from dataclasses import dataclass +from typing import Any -from .cost_policy import CostClass, classify_cost -from .fallback_policy import build_fallback_chain -from .provider_registry import ALL_TASK_TYPES, get_provider +_ROUTES: dict[str, dict[str, Any]] = { + "strategic_reasoning": {"provider": "anthropic", "cost_class": "high", "needs_guardrail": True}, + "arabic_copywriting": {"provider": "anthropic", "cost_class": "medium", "needs_guardrail": True}, + "classification": {"provider": "openai", "cost_class": "low", "needs_guardrail": True}, + "compliance_guardrail": {"provider": "openai", "cost_class": "low", "needs_guardrail": False}, + "meeting_analysis": {"provider": "google", "cost_class": "medium", "needs_guardrail": True}, + "vision_analysis": {"provider": "google", "cost_class": "medium", "needs_guardrail": True}, + "extraction": {"provider": "groq", "cost_class": "low", "needs_guardrail": True}, + "summarization": {"provider": "openai", "cost_class": "low", "needs_guardrail": True}, + "low_cost_bulk": {"provider": "groq", "cost_class": "minimal", "needs_guardrail": True}, + "coding_project_understanding": {"provider": "anthropic", "cost_class": "high", "needs_guardrail": True}, +} -@dataclass(frozen=True) -class RouteDecision: - task_type: str - primary_provider: str | None - fallback_chain: list[str] - cost_class: CostClass - reasons_ar: list[str] - requires_arabic: bool - requires_vision: bool - sensitivity: str - - def to_dict(self) -> dict[str, object]: - return { - "task_type": self.task_type, - "primary_provider": self.primary_provider, - "fallback_chain": self.fallback_chain, - "cost_class": self.cost_class, - "reasons_ar": self.reasons_ar, - "requires_arabic": self.requires_arabic, - "requires_vision": self.requires_vision, - "sensitivity": self.sensitivity, - } +def list_tasks() -> dict[str, Any]: + return {"task_types": sorted(_ROUTES.keys()), "demo": True} -def route_task( - task_type: str, - *, - requires_arabic: bool = False, - requires_vision: bool = False, - sensitivity: str = "low", - expected_input_tokens: int = 0, - expected_output_tokens: int = 0, - bulk: bool = False, - primary_provider: str | None = None, -) -> RouteDecision: - """Route a task → primary provider + ordered fallback chain + cost class.""" - reasons: list[str] = [] - - if task_type not in ALL_TASK_TYPES: - return RouteDecision( - task_type=task_type, - primary_provider=None, - fallback_chain=[], - cost_class="low", - reasons_ar=[f"نوع المهمة غير معروف: {task_type}"], - requires_arabic=requires_arabic, - requires_vision=requires_vision, - sensitivity=sensitivity, - ) - - cost_class = classify_cost( - task_type=task_type, - expected_input_tokens=expected_input_tokens, - expected_output_tokens=expected_output_tokens, - bulk=bulk, - ) - - chain = build_fallback_chain( - task_type, - requires_arabic=requires_arabic, - requires_vision=requires_vision, - sensitivity=sensitivity, - primary_key=primary_provider, - ) - - if not chain: - reasons.append( - "لا يوجد مزود مناسب — راجع capabilities أو خفّف القيود (vision/arabic)." - ) - - primary = chain[0] if chain else None - if primary: - p = get_provider(primary) - if p: - reasons.append( - f"المزود الأساسي: {p.label} — {p.notes_ar}" - ) - if sensitivity == "high": - reasons.append("حساسية عالية: تم تفضيل KSA-region/self-hosted أولاً.") - if bulk: - reasons.append("مهمة جماعية كبيرة: تم اختيار cost_class=low.") - - return RouteDecision( - task_type=task_type, - primary_provider=primary, - fallback_chain=chain, - cost_class=cost_class, - reasons_ar=reasons, - requires_arabic=requires_arabic, - requires_vision=requires_vision, - sensitivity=sensitivity, - ) +def route_task(task_type: str) -> dict[str, Any]: + t = (task_type or "").strip().lower().replace("-", "_") + if t not in _ROUTES: + return {"ok": False, "error": "unknown_task_type", "known": sorted(_ROUTES.keys()), "demo": True} + r = _ROUTES[t] + return {"ok": True, "task_type": t, **r, "fallback_provider": "groq", "demo": True} diff --git a/dealix/auto_client_acquisition/platform_services/__init__.py b/dealix/auto_client_acquisition/platform_services/__init__.py index 59897666..b9584575 100644 --- a/dealix/auto_client_acquisition/platform_services/__init__.py +++ b/dealix/auto_client_acquisition/platform_services/__init__.py @@ -1,74 +1,23 @@ -""" -Platform Services Layer — Dealix's Growth Control Tower spine. +"""Platform Services — Growth Control Tower (policy, inbox, catalog, no live sends).""" -Turns the platform from "WhatsApp Growth Operator" into a multi-channel -growth platform that ingests events from every channel a Saudi B2B uses, -converts them into Arabic action cards, evaluates each action against -policy, and produces unified proof. - -Modules: - - event_bus : typed events from all channels - - identity_resolution : reconcile phone+email+social→one person - - channel_registry : 11 supported channels with capabilities - - action_policy : decide approval / block / allow - - tool_gateway : draft-only proxy (no live actions here) - - unified_inbox : 8 card types from events - - action_ledger : auditable record of every action lifecycle - - proof_ledger : value rolled up across the platform - - service_catalog : 12 sellable services -""" - -from auto_client_acquisition.platform_services.action_ledger import ( - ActionLedger, - LedgerEntry, -) -from auto_client_acquisition.platform_services.action_policy import ( - POLICY_RULES, - PolicyDecision, - evaluate_action, -) -from auto_client_acquisition.platform_services.channel_registry import ( - ALL_CHANNELS, - Channel, - get_channel, -) -from auto_client_acquisition.platform_services.event_bus import ( - EVENT_TYPES, - PlatformEvent, - make_event, -) -from auto_client_acquisition.platform_services.identity_resolution import ( - Identity, - resolve_identity, -) -from auto_client_acquisition.platform_services.proof_ledger import ( - PlatformProofLedger, - build_demo_platform_proof, -) -from auto_client_acquisition.platform_services.service_catalog import ( - SELLABLE_SERVICES, - ServiceOffering, - list_services, -) -from auto_client_acquisition.platform_services.tool_gateway import ( - GatewayResult, - invoke_tool, -) -from auto_client_acquisition.platform_services.unified_inbox import ( - CARD_TYPES, - InboxCard, - build_card_from_event, - build_demo_feed, -) +from auto_client_acquisition.platform_services.action_ledger import ActionLedger, get_action_ledger +from auto_client_acquisition.platform_services.action_policy import evaluate_action +from auto_client_acquisition.platform_services.channel_registry import list_channels +from auto_client_acquisition.platform_services.event_bus import EventType, validate_event +from auto_client_acquisition.platform_services.proof_summary import build_proof_summary +from auto_client_acquisition.platform_services.service_catalog import get_service_catalog +from auto_client_acquisition.platform_services.tool_gateway import execute_tool +from auto_client_acquisition.platform_services.unified_inbox import event_to_inbox_card __all__ = [ - "EVENT_TYPES", "PlatformEvent", "make_event", - "Identity", "resolve_identity", - "ALL_CHANNELS", "Channel", "get_channel", - "POLICY_RULES", "PolicyDecision", "evaluate_action", - "GatewayResult", "invoke_tool", - "CARD_TYPES", "InboxCard", "build_card_from_event", "build_demo_feed", - "ActionLedger", "LedgerEntry", - "PlatformProofLedger", "build_demo_platform_proof", - "SELLABLE_SERVICES", "ServiceOffering", "list_services", + "ActionLedger", + "EventType", + "build_proof_summary", + "evaluate_action", + "event_to_inbox_card", + "execute_tool", + "get_action_ledger", + "get_service_catalog", + "list_channels", + "validate_event", ] diff --git a/dealix/auto_client_acquisition/platform_services/action_ledger.py b/dealix/auto_client_acquisition/platform_services/action_ledger.py index 64e7aa32..7b01e4cc 100644 --- a/dealix/auto_client_acquisition/platform_services/action_ledger.py +++ b/dealix/auto_client_acquisition/platform_services/action_ledger.py @@ -1,107 +1,41 @@ -""" -Action Ledger — auditable record of every action lifecycle. - -Stage transitions per action: requested → (approved | rejected | blocked) -→ executed → outcome. - -Used for SDAIA / DPO inspections + customer's own audit trail. -""" +"""In-memory decision log for platform tools (MVP).""" from __future__ import annotations -import uuid -from dataclasses import dataclass, field -from datetime import datetime, timezone +import itertools +import threading +import time from typing import Any - -VALID_STAGES: tuple[str, ...] = ( - "requested", "approved", "rejected", "blocked", - "executed", "outcome_recorded", -) +_counter = itertools.count(1) +_lock = threading.Lock() +_entries: list[dict[str, Any]] = [] -@dataclass -class LedgerEntry: - """One entry in the action ledger.""" - - entry_id: str - customer_id: str - action_type: str - channel: str - stage: str - actor: str = "system" - payload: dict[str, Any] = field(default_factory=dict) - reason_ar: str = "" - created_at: datetime = field( - default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None) - ) - correlation_id: str | None = None - - def to_dict(self) -> dict[str, Any]: - return { - "entry_id": self.entry_id, - "customer_id": self.customer_id, - "action_type": self.action_type, - "channel": self.channel, - "stage": self.stage, - "actor": self.actor, - "payload": self.payload, - "reason_ar": self.reason_ar, - "created_at": self.created_at.isoformat(), - "correlation_id": self.correlation_id, - } - - -@dataclass class ActionLedger: - """Append-only ledger keyed by customer_id.""" + """Thread-safe append-only ledger.""" - entries: list[LedgerEntry] = field(default_factory=list) - - def append( - self, - *, - customer_id: str, - action_type: str, - channel: str, - stage: str, - actor: str = "system", - payload: dict[str, Any] | None = None, - reason_ar: str = "", - correlation_id: str | None = None, - ) -> LedgerEntry: - if stage not in VALID_STAGES: - raise ValueError(f"unknown stage: {stage}") - entry = LedgerEntry( - entry_id=f"led_{uuid.uuid4().hex[:20]}", - customer_id=customer_id, - action_type=action_type, - channel=channel, - stage=stage, - actor=actor, - payload=payload or {}, - reason_ar=reason_ar, - correlation_id=correlation_id, - ) - self.entries.append(entry) + def append_decision(self, *, tool: str, outcome: str, detail: dict[str, Any]) -> dict[str, Any]: + with _lock: + entry = { + "id": next(_counter), + "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "tool": tool, + "outcome": outcome, + "detail": detail, + } + _entries.append(entry) + if len(_entries) > 500: + del _entries[:-500] return entry - def for_customer(self, customer_id: str) -> list[LedgerEntry]: - return [e for e in self.entries if e.customer_id == customer_id] + def recent(self, limit: int = 50) -> list[dict[str, Any]]: + with _lock: + return list(_entries[-limit:]) - def summary(self, customer_id: str | None = None) -> dict[str, Any]: - pool = self.entries if customer_id is None else self.for_customer(customer_id) - by_stage: dict[str, int] = {} - by_channel: dict[str, int] = {} - by_action: dict[str, int] = {} - for e in pool: - by_stage[e.stage] = by_stage.get(e.stage, 0) + 1 - by_channel[e.channel] = by_channel.get(e.channel, 0) + 1 - by_action[e.action_type] = by_action.get(e.action_type, 0) + 1 - return { - "total": len(pool), - "by_stage": by_stage, - "by_channel": by_channel, - "by_action_type": by_action, - } + +_ledger = ActionLedger() + + +def get_action_ledger() -> ActionLedger: + return _ledger diff --git a/dealix/auto_client_acquisition/platform_services/action_policy.py b/dealix/auto_client_acquisition/platform_services/action_policy.py index c1f617a4..78a39832 100644 --- a/dealix/auto_client_acquisition/platform_services/action_policy.py +++ b/dealix/auto_client_acquisition/platform_services/action_policy.py @@ -1,173 +1,82 @@ -""" -Action Policy Engine — decides whether an action can run, needs approval, -or is blocked. The single chokepoint that protects the customer's -reputation + enforces PDPL. - -Design: pure deterministic rules. Easily testable, easily auditable, -easy for the customer to explain to compliance. -""" +"""Deterministic policy — no network.""" from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any +from typing import Any, Literal +from core.config.settings import get_settings -# ── Policy rules — each rule is (action_type, condition, decision, reason_ar) -POLICY_RULES: list[dict[str, Any]] = [ - # Hard blocks — never executed - { - "rule_id": "block_cold_whatsapp", - "action": "send_whatsapp", - "when": {"source": "cold_list", "consent": False}, - "decision": "blocked", - "reason_ar": "WhatsApp البارد محظور بدون lawful basis (PDPL م.5).", - }, - { - "rule_id": "block_payment_no_confirm", - "action": "charge_payment", - "when": {"user_confirmed": False}, - "decision": "blocked", - "reason_ar": "الخصم يحتاج تأكيد المستخدم على Moyasar — لا charge مباشر.", - }, - { - "rule_id": "block_secrets_in_payload", - "action": "*", - "when": {"payload_contains_secret": True}, - "decision": "blocked", - "reason_ar": "تم اكتشاف secret في الـ payload — حماية تلقائية.", - }, - # Approval gates — must pass through human - { - "rule_id": "external_send_needs_approval", - "action": "send_whatsapp,send_email,send_inmail,post_social", - "when": {"approval_status": "pending"}, - "decision": "approval_required", - "reason_ar": "كل إرسال خارجي يحتاج موافقة العميل قبل التنفيذ.", - }, - { - "rule_id": "calendar_insert_needs_approval", - "action": "calendar_insert_event", - "when": {"approval_status": "pending"}, - "decision": "approval_required", - "reason_ar": "إنشاء اجتماع في تقويم العميل يحتاج موافقة قبل insert.", - }, - { - "rule_id": "social_dm_needs_explicit", - "action": "send_social_dm", - "when": {"explicit_permission": False}, - "decision": "approval_required", - "reason_ar": "DM السوشيال يحتاج إذن صريح لكل حساب.", - }, - # Needs review - { - "rule_id": "unknown_source_review", - "action": "*", - "when": {"source": "unknown"}, - "decision": "approval_required", - "reason_ar": "مصدر البيانات غير محدد — يحتاج توثيق lawful basis.", - }, - { - "rule_id": "high_value_deal_review", - "action": "*", - "when": {"deal_value_sar_gte": 100_000}, - "decision": "approval_required", - "reason_ar": "صفقة قيمتها ≥100K ريال — راجعها قبل التنفيذ.", - }, - # Allowed (default for safe paths) - { - "rule_id": "draft_only_safe", - "action": "create_draft,read_data,classify_reply", - "when": {}, - "decision": "allow", - "reason_ar": "إجراء داخلي آمن — لا يخرج للعميل النهائي.", - }, -] - - -@dataclass -class PolicyDecision: - """Output of evaluate_action.""" - - decision: str # allow / approval_required / blocked - matched_rule_id: str | None - reasons_ar: list[str] = field(default_factory=list) - suggested_next_action_ar: str = "" +PolicyState = Literal["approved", "blocked", "approval_required", "review"] def evaluate_action( *, action: str, + channel_id: str, context: dict[str, Any] | None = None, -) -> PolicyDecision: +) -> dict[str, Any]: """ - Evaluate a proposed action against the policy rules. - - First matching rule wins. Default: needs_review (defensive). + Rules: + - External-ish sends → approval_required unless explicitly internal draft. + - Cold WhatsApp → blocked when ``intent`` is cold/campaign_cold. + - Payment → approval_required + confirm flag if amount present. + - Unknown channel → review. """ ctx = context or {} - matched_reasons: list[str] = [] - final_decision = "allow" - matched_rule_id: str | None = None - next_action = "ready_for_execution" + reason_ar = "" + state: PolicyState = "approval_required" - for rule in POLICY_RULES: - # Action match (comma-separated list, "*" = match-any) - applicable_actions = rule["action"].split(",") if rule["action"] != "*" else [action] - if action not in applicable_actions and rule["action"] != "*": - continue + known = { + "whatsapp", + "email", + "linkedin_lead_form", + "website_form", + "google_business", + "x_twitter", + "instagram", + "moyasar", + } + if channel_id not in known: + return { + "state": "review", + "reason_ar": "قناة غير معروفة في السجل — يلزم مراجعة يدوية.", + "action": action, + "channel_id": channel_id, + } - # Condition match — every key in `when` must match the context - when = rule["when"] - cond_match = True - for k, expected in when.items(): - if k.endswith("_gte"): - attr = k[:-4] - if not (float(ctx.get(attr, 0)) >= float(expected)): - cond_match = False - break - elif k == "payload_contains_secret": - if expected and not _has_secret_marker(ctx.get("payload", {})): - cond_match = False - break - elif ctx.get(k) != expected: - cond_match = False - break + if channel_id == "whatsapp" and action in ("send", "send_live", "external_send"): + intent = str(ctx.get("intent") or "").lower() + audience = str(ctx.get("audience") or "").lower() + cold_markers = ("cold", "campaign_cold", "purchased_list", "unknown_opt_in") + if intent in cold_markers or audience in cold_markers: + return { + "state": "blocked", + "reason_ar": "الواتساب البارد أو قوائم غير موثقة محظور حتى موافقة امتثال وتسجيل opt-in.", + "action": action, + "channel_id": channel_id, + } + settings = get_settings() + if action == "send_live" and not settings.whatsapp_allow_live_send: + return { + "state": "blocked", + "reason_ar": "الإرسال الحي للواتساب معطّل في الإعدادات (WHATSAPP_ALLOW_LIVE_SEND=false).", + "action": action, + "channel_id": channel_id, + } - if not cond_match: - continue + if action in ("send", "send_live", "external_send", "smtp_send"): + state = "approval_required" + reason_ar = "أي إرسال خارجي يتطلب موافقة بشرية في هذا الإصدار." - decision = rule["decision"] - matched_reasons.append(rule["reason_ar"]) - matched_rule_id = rule["rule_id"] + if action in ("payment_charge", "payment_capture", "moyasar_charge"): + state = "approval_required" + if not ctx.get("user_confirmed"): + reason_ar = "عمليات الدفع تتطلب تأكيداً صريحاً من المشغّل قبل التنفيذ." + else: + reason_ar = "تم تسجيل تأكيد المشغّل — ما زال التنفيذ الفعلي معطّلاً في MVP." - if decision == "blocked": - return PolicyDecision( - decision="blocked", - matched_rule_id=matched_rule_id, - reasons_ar=matched_reasons, - suggested_next_action_ar="معالجة سبب الحظر قبل المحاولة مرة أخرى.", - ) - if decision == "approval_required": - final_decision = "approval_required" - next_action = "operator_approves_then_execute" - # 'allow' rules just confirm — keep looking for stricter rule + if action in ("draft_only", "draft_message", "draft_email"): + state = "approved" + reason_ar = "مسودة داخلية — مسموح للعرض فقط." - return PolicyDecision( - decision=final_decision, - matched_rule_id=matched_rule_id, - reasons_ar=matched_reasons or ["لا قاعدة مطابقة — الإجراء آمن افتراضياً."], - suggested_next_action_ar=next_action, - ) - - -# ── Helpers ────────────────────────────────────────────────────── -_SECRET_MARKERS = ("api_key", "secret_key", "private_key", "password", "ghp_", "sk-ant-", "moyasar_secret") - - -def _has_secret_marker(payload: dict[str, Any]) -> bool: - """Cheap heuristic check — production pairs this with a stronger scanner.""" - if not isinstance(payload, dict): - return False - flat = str(payload).lower() - return any(marker in flat for marker in _SECRET_MARKERS) + return {"state": state, "reason_ar": reason_ar or "قرار سياسة افتراضي.", "action": action, "channel_id": channel_id} diff --git a/dealix/auto_client_acquisition/platform_services/channel_registry.py b/dealix/auto_client_acquisition/platform_services/channel_registry.py index 6396fa4c..43ed5366 100644 --- a/dealix/auto_client_acquisition/platform_services/channel_registry.py +++ b/dealix/auto_client_acquisition/platform_services/channel_registry.py @@ -1,213 +1,69 @@ -""" -Channel Registry — 11 supported channels with capabilities + risk profile. - -Each channel declares: capabilities, beta_status, required_permissions, -allowed_actions, blocked_actions, risk_level. Used by the action policy -engine and the unified inbox. -""" +"""Channel capabilities — registered-only social channels, no OAuth in MVP.""" from __future__ import annotations -from dataclasses import dataclass, field from typing import Any - -@dataclass(frozen=True) -class Channel: - """A connected channel + what it can / cannot do.""" - - key: str - label_ar: str - label_en: str - capabilities: tuple[str, ...] - beta_status: str # ga / beta / experimental / planned - required_permissions: tuple[str, ...] - allowed_actions: tuple[str, ...] - blocked_actions: tuple[str, ...] - risk_level: str # low / medium / high - notes_ar: str = "" +_CHANNEL_DEFS: list[dict[str, Any]] = [ + { + "id": "whatsapp", + "label_ar": "واتساب للأعمال", + "beta_status": "pilot", + "risk_level": "high", + "allowed_actions": ["draft_message", "template_preview"], + "blocked_actions": ["cold_outreach_auto", "bulk_send_without_approval"], + }, + { + "id": "email", + "label_ar": "البريد", + "beta_status": "ga_ready", + "risk_level": "medium", + "allowed_actions": ["draft_email", "schedule_internal"], + "blocked_actions": ["smtp_live_without_approval"], + }, + { + "id": "linkedin_lead_form", + "label_ar": "نماذج عملاء LinkedIn", + "beta_status": "mvp", + "risk_level": "low", + "allowed_actions": ["ingest_webhook_simulation", "normalize_lead"], + "blocked_actions": ["scrape_profile"], + }, + { + "id": "website_form", + "label_ar": "نموذج موقع", + "beta_status": "mvp", + "risk_level": "low", + "allowed_actions": ["ingest_webhook_simulation", "normalize_lead"], + "blocked_actions": [], + }, + # Wave 5 — registered-only (ingest / auto-reply deferred) + { + "id": "google_business", + "label_ar": "ملف Google Business", + "beta_status": "registered_only", + "risk_level": "medium", + "allowed_actions": [], + "blocked_actions": ["auto_reply", "oauth_connect", "public_api_call"], + }, + { + "id": "x_twitter", + "label_ar": "X (تويتر)", + "beta_status": "registered_only", + "risk_level": "medium", + "allowed_actions": [], + "blocked_actions": ["auto_reply", "oauth_connect", "public_api_call"], + }, + { + "id": "instagram", + "label_ar": "إنستغرام", + "beta_status": "registered_only", + "risk_level": "medium", + "allowed_actions": [], + "blocked_actions": ["auto_reply", "oauth_connect", "public_api_call"], + }, +] -# ── The 11 channels we model ──────────────────────────────────── -ALL_CHANNELS: tuple[Channel, ...] = ( - Channel( - key="whatsapp", - label_ar="واتساب", - label_en="WhatsApp Business / Cloud", - capabilities=( - "inbound_messages", "outbound_template_messages", - "interactive_buttons_max_3", "media_send", "opt_out_handling", - ), - beta_status="ga", - required_permissions=( - "waba_account_id", "phone_number_id", "verified_business", - ), - allowed_actions=("draft_message", "send_with_approval", "track_reply"), - blocked_actions=("cold_send_without_consent", "bulk_unsolicited_send"), - risk_level="medium", - notes_ar="حد 3 buttons تفاعلية. الإرسال البارد محظور بدون lawful basis.", - ), - Channel( - key="gmail", - label_ar="Gmail (إيميل العميل)", - label_en="Gmail OAuth", - capabilities=( - "create_draft_only", "read_labeled_threads", - "list_unsubscribe_header_attached", - ), - beta_status="ga", - required_permissions=("gmail.compose",), - allowed_actions=("create_draft", "read_thread"), - blocked_actions=("send_without_user_click", "delete_messages"), - risk_level="low", - notes_ar="نكتفي بـ scope `gmail.compose`. المستخدم يضغط Send بنفسه.", - ), - Channel( - key="google_calendar", - label_ar="Google Calendar", - label_en="Google Calendar API", - capabilities=( - "events_insert_with_meet", "events_list", - "rfc5545_recurrence", "asia_riyadh_timezone", - ), - beta_status="ga", - required_permissions=("calendar.events",), - allowed_actions=("draft_event", "create_event_with_approval"), - blocked_actions=("delete_other_attendees_events", "modify_external_events_silently"), - risk_level="low", - notes_ar="conferenceDataVersion=1 لإضافة Google Meet.", - ), - Channel( - key="linkedin_lead_forms", - label_ar="LinkedIn Lead Gen Forms", - label_en="LinkedIn Lead Gen Forms API", - capabilities=( - "ingest_leads_from_ads", "hidden_field_tracking", - "crm_sync", - ), - beta_status="beta", - required_permissions=("r_marketing_leadgen_automation",), - allowed_actions=("ingest_lead_form", "trigger_followup_draft"), - blocked_actions=("scrape_profiles", "unsolicited_inmails_at_scale"), - risk_level="low", - notes_ar="مصدر رسمي لـ leads مؤهلة.", - ), - Channel( - key="x_api", - label_ar="X (Twitter)", - label_en="X API v2", - capabilities=( - "post_tweet", "read_mentions", - "user_lookups_basic", "webhooks_account_activity_paid", - ), - beta_status="experimental", - required_permissions=("oauth2_user_context",), - allowed_actions=("draft_post", "ingest_mention", "draft_dm_reply"), - blocked_actions=("auto_dm_strangers", "scrape_user_lists"), - risk_level="medium", - notes_ar="بعض الـ webhooks Enterprise-only. نقتصر على ما تتيحه الخطة الحالية.", - ), - Channel( - key="instagram_graph", - label_ar="Instagram (Graph API)", - label_en="Instagram Graph API", - capabilities=( - "read_business_messages", "publish_posts", - "read_comments_on_owned_posts", - ), - beta_status="beta", - required_permissions=("instagram_basic", "instagram_manage_messages"), - allowed_actions=("draft_reply", "ingest_comment", "ingest_dm"), - blocked_actions=("auto_dm_strangers", "scrape_unrelated_users"), - risk_level="medium", - notes_ar="فقط للحسابات Business + ما يخص العميل المتصل.", - ), - Channel( - key="google_business_profile", - label_ar="Google Business Profile", - label_en="Google Business Profile API", - capabilities=( - "read_reviews", "post_replies", - "publish_local_posts", "manage_location_info", - ), - beta_status="ga", - required_permissions=("business.manage",), - allowed_actions=("draft_review_reply", "draft_local_post"), - blocked_actions=("delete_real_reviews"), - risk_level="low", - notes_ar="مهم للمتاجر والعيادات والفروع — السمعة المحلية.", - ), - Channel( - key="google_sheets", - label_ar="Google Sheets", - label_en="Google Sheets API", - capabilities=("read_range", "append_row", "watch_changes"), - beta_status="ga", - required_permissions=("spreadsheets.readonly", "spreadsheets",), - allowed_actions=("import_contacts", "sync_pipeline", "log_actions"), - blocked_actions=("delete_user_sheets"), - risk_level="low", - notes_ar="أداة مفيدة للتكامل مع عمليات العميل اليدوية.", - ), - Channel( - key="crm", - label_ar="CRM (Zoho/HubSpot/Salla/Odoo)", - label_en="CRM via REST/SDK", - capabilities=( - "deal_sync", "contact_sync", "activity_log", - ), - beta_status="planned", - required_permissions=("crm_api_token",), - allowed_actions=("read_deals", "update_stage_with_approval"), - blocked_actions=("delete_deals_silently"), - risk_level="medium", - notes_ar="بناء adapter لكل CRM في مرحلة لاحقة.", - ), - Channel( - key="moyasar", - label_ar="Moyasar (مدفوعات)", - label_en="Moyasar Payments", - capabilities=( - "create_payment_link", "create_invoice", - "webhook_paid_failed_refunded", "refund", - ), - beta_status="ga", - required_permissions=("publishable_key", "secret_key"), - allowed_actions=("draft_payment_link", "send_invoice_email"), - blocked_actions=("charge_card_without_user_action"), - risk_level="high", - notes_ar="بطاقة العميل تُدخَل على Moyasar (PCI-safe). لا تخزين خانات.", - ), - Channel( - key="website_forms", - label_ar="نماذج الموقع", - label_en="Website Forms", - capabilities=("ingest_submission", "trigger_workflow"), - beta_status="ga", - required_permissions=("webhook_endpoint",), - allowed_actions=("ingest_lead", "draft_thankyou_message"), - blocked_actions=(), - risk_level="low", - notes_ar="مصدر leads مؤهَّلة بطبيعتها — أساس قانوني واضح.", - ), -) - - -def get_channel(key: str) -> Channel | None: - for c in ALL_CHANNELS: - if c.key == key: - return c - return None - - -def channels_summary() -> dict[str, Any]: - by_status: dict[str, int] = {} - by_risk: dict[str, int] = {} - for c in ALL_CHANNELS: - by_status[c.beta_status] = by_status.get(c.beta_status, 0) + 1 - by_risk[c.risk_level] = by_risk.get(c.risk_level, 0) + 1 - return { - "total": len(ALL_CHANNELS), - "by_beta_status": by_status, - "by_risk_level": by_risk, - } +def list_channels() -> dict[str, Any]: + return {"channels": list(_CHANNEL_DEFS), "demo": True} diff --git a/dealix/auto_client_acquisition/platform_services/contact_import_preview.py b/dealix/auto_client_acquisition/platform_services/contact_import_preview.py new file mode 100644 index 00000000..08604b29 --- /dev/null +++ b/dealix/auto_client_acquisition/platform_services/contact_import_preview.py @@ -0,0 +1,152 @@ +"""CSV / row-list import preview — classification only, no send.""" + +from __future__ import annotations + +import csv +import io +import re +from typing import Any + +from auto_client_acquisition.compliance_os.consent_ledger import LawfulBasis, record_consent +from auto_client_acquisition.compliance_os.contactability import check_contactability + +_TRUSTED_SOURCES = frozenset( + { + "inbound_form", + "website_form", + "prior_customer", + "event_meeting", + "explicit_consent", + "form_submission", + "linkedin_lead_form", + } +) + + +def _norm_phone(raw: str | None) -> str: + if not raw: + return "" + s = re.sub(r"\s+", "", str(raw)) + if s.startswith("00"): + s = "+" + s[2:] + if s.startswith("0") and len(s) >= 9: + s = "+966" + s[1:] + if s.isdigit() and len(s) == 9: + s = "+966" + s + return s + + +def _parse_rows(body: dict[str, Any]) -> list[dict[str, Any]]: + if isinstance(body.get("rows"), list): + return [dict(x) for x in body["rows"] if isinstance(x, dict)] + csv_text = body.get("csv_text") + if isinstance(csv_text, str) and csv_text.strip(): + reader = csv.DictReader(io.StringIO(csv_text)) + return [dict(row) for row in reader] + return [] + + +def _bucket_for_row(row: dict[str, Any], *, customer_id: str) -> tuple[str, str, dict[str, Any]]: + """ + Returns (bucket, reason_code, extra) where bucket is safe | needs_review | blocked. + """ + if row.get("opted_out") in (True, "true", "1", 1): + return "blocked", "opted_out", {} + + src = str(row.get("source") or "").strip().lower() or "unknown" + if row.get("cold_whatsapp") in (True, "true", "1", 1): + return "blocked", "cold_whatsapp", {} + + if src in ("purchased_list", "scraped", "unknown_list"): + return "blocked", "purchased_list", {} + + phone = _norm_phone(row.get("phone") or row.get("mobile") or row.get("tel")) + email = str(row.get("email") or "").strip().lower() + if not phone and not email: + return "needs_review", "missing_identifier", {} + + contact_id = phone or email + + if src == "unknown" or src not in _TRUSTED_SOURCES: + return "needs_review", "unknown_source", {"contact_id": contact_id} + + # Trusted: synthetic consent so contactability can pass (import-time snapshot). + if src in ("inbound_form", "website_form", "form_submission", "linkedin_lead_form"): + basis = LawfulBasis.CONSENT + rec_src = "form_submission" + else: + basis = LawfulBasis.LEGITIMATE_INTEREST + rec_src = "public_directory" + + rec = record_consent( + customer_id=customer_id, + contact_id=contact_id, + lawful_basis=basis, + purpose="import_preview", + channel="all", + source=rec_src, + ) + status = check_contactability( + contact_id=contact_id, + consent_records=[rec], + messages_sent_this_week=0, + weekly_cap=10, + current_riyadh_hour=12, + ) + if not status.can_contact: + if status.reason_code == "opted_out": + return "blocked", status.reason_code, status.to_dict() + return "needs_review", status.reason_code, status.to_dict() + + return "safe", "trusted_with_consent_snapshot", status.to_dict() + + +def build_import_preview(body: dict[str, Any]) -> dict[str, Any]: + """ + Summarizes contacts into safe / needs_review / blocked / invalid_duplicate. + + No external I/O; does not persist. + """ + customer_id = str(body.get("customer_id") or "default") + rows = _parse_rows(body) + if not rows: + return { + "ok": False, + "error": "no_rows", + "detail_ar": "مرّر ``rows`` كقائمة أو ``csv_text`` مع رؤوس أعمدة.", + } + + seen: set[str] = set() + counts = {"safe": 0, "needs_review": 0, "blocked": 0, "invalid_duplicate": 0} + samples: dict[str, list[dict[str, Any]]] = {"safe": [], "needs_review": [], "blocked": []} + + for raw in rows: + phone = _norm_phone(raw.get("phone") or raw.get("mobile") or raw.get("tel")) + email = str(raw.get("email") or "").strip().lower() + dedupe_key = phone or email or "" + if dedupe_key and dedupe_key in seen: + counts["invalid_duplicate"] += 1 + continue + if dedupe_key: + seen.add(dedupe_key) + + bucket, reason, extra = _bucket_for_row(raw, customer_id=customer_id) + counts[bucket] += 1 + entry = { + "phone": phone, + "email": email or None, + "source": raw.get("source"), + "bucket": bucket, + "reason": reason, + "contactability": extra, + } + if bucket in samples and len(samples[bucket]) < 5: + samples[bucket].append(entry) + + return { + "ok": True, + "approval_required": True, + "counts": counts, + "samples": samples, + "note_ar": "معاينة فقط — لا يُرسل أي تواصل ولا يُخزّن دفعة الاستيراد في MVP.", + } diff --git a/dealix/auto_client_acquisition/platform_services/event_bus.py b/dealix/auto_client_acquisition/platform_services/event_bus.py index 6ceebaab..8dcd63db 100644 --- a/dealix/auto_client_acquisition/platform_services/event_bus.py +++ b/dealix/auto_client_acquisition/platform_services/event_bus.py @@ -1,110 +1,74 @@ -""" -Omni-Channel Event Bus — every channel emits typed events here. - -Pure structures + helpers; the actual transport (Redis/Kafka) lives in a -production adapter. This module is testable in isolation. -""" +"""Unified event types and field validation — no transport.""" from __future__ import annotations -import uuid -from dataclasses import dataclass, field -from datetime import datetime, timezone +from enum import Enum from typing import Any -# ── Event taxonomy ──────────────────────────────────────────────── -EVENT_TYPES: tuple[str, ...] = ( - # WhatsApp - "whatsapp.message_received", - "whatsapp.message_sent", - "whatsapp.opt_out", - # Email (Gmail or company SMTP) - "email.received", - "email.draft_created", - "email.sent", - # Calendar - "calendar.meeting_scheduled", - "calendar.meeting_held", - "calendar.no_show", - # Social (X / LinkedIn / Instagram / Facebook) - "social.comment_received", - "social.dm_received", - "social.mention_received", - "social.lead_form_submitted", - # Website + CRM - "lead.form_submitted", - "lead.crm_imported", - # Payments (Moyasar) - "payment.initiated", - "payment.paid", - "payment.failed", - "payment.refunded", - # Reviews / reputation (Google Business Profile) - "review.created", - "review.replied", - # Partners - "partner.suggested", - "partner.intro_made", - # Internal lifecycle - "action.requested", - "action.approved", - "action.rejected", - "action.executed", - "action.blocked", - # Sheets / CRM sync - "sheet.row_added", - "crm.deal_updated", -) +class EventType(str, Enum): + """Stable event type names for platform ingest and internal cards.""" + + LEAD_RECEIVED = "lead_received" + EXTERNAL_SEND_REQUESTED = "external_send_requested" + PAYMENT_INTENT = "payment_intent" + WHATSAPP_MESSAGE_REQUESTED = "whatsapp_message_requested" + REVIEW_REQUIRED = "review_required" + DRAFT_CREATED = "draft_created" + # Omni-channel extensions (dotted names) — backward compatible with existing types. + EMAIL_RECEIVED = "email.received" + CALENDAR_MEETING_SCHEDULED = "calendar.meeting_scheduled" + SOCIAL_COMMENT_RECEIVED = "social.comment_received" + SOCIAL_DM_RECEIVED = "social.dm_received" + LEAD_FORM_SUBMITTED = "lead.form_submitted" + PAYMENT_PAID = "payment.paid" + PAYMENT_FAILED = "payment.failed" + REVIEW_CREATED = "review.created" + PARTNER_SUGGESTED = "partner.suggested" + ACTION_APPROVED = "action.approved" + ACTION_BLOCKED = "action.blocked" -# ── Event envelope ──────────────────────────────────────────────── -@dataclass(frozen=True) -class PlatformEvent: - """Immutable platform event.""" - - event_id: str - event_type: str - channel: str # whatsapp / gmail / google_calendar / x / ... - customer_id: str - occurred_at: datetime - payload: dict[str, Any] = field(default_factory=dict) - correlation_id: str | None = None - actor: str = "system" - - def to_dict(self) -> dict[str, Any]: - return { - "event_id": self.event_id, - "event_type": self.event_type, - "channel": self.channel, - "customer_id": self.customer_id, - "occurred_at": self.occurred_at.isoformat(), - "payload": self.payload, - "correlation_id": self.correlation_id, - "actor": self.actor, - } +_REQUIRED: dict[EventType, tuple[str, ...]] = { + EventType.LEAD_RECEIVED: ("source", "channel_id"), + EventType.EXTERNAL_SEND_REQUESTED: ("channel_id", "action"), + EventType.PAYMENT_INTENT: ("amount_halalas", "currency"), + EventType.WHATSAPP_MESSAGE_REQUESTED: ("intent", "audience"), + EventType.REVIEW_REQUIRED: ("reason_code",), + EventType.DRAFT_CREATED: ("draft_kind",), + EventType.EMAIL_RECEIVED: ("channel_id", "subject_ar"), + EventType.CALENDAR_MEETING_SCHEDULED: ("channel_id", "title_ar"), + EventType.SOCIAL_COMMENT_RECEIVED: ("channel_id", "snippet_ar"), + EventType.SOCIAL_DM_RECEIVED: ("channel_id", "sender_hint"), + EventType.LEAD_FORM_SUBMITTED: ("source", "channel_id"), + EventType.PAYMENT_PAID: ("amount_halalas", "currency"), + EventType.PAYMENT_FAILED: ("amount_halalas", "reason_code"), + EventType.REVIEW_CREATED: ("channel_id", "rating"), + EventType.PARTNER_SUGGESTED: ("partner_name_ar", "sector"), + EventType.ACTION_APPROVED: ("action_id", "actor"), + EventType.ACTION_BLOCKED: ("action_id", "reason_code"), +} -def make_event( - *, - event_type: str, - channel: str, - customer_id: str, - payload: dict[str, Any] | None = None, - correlation_id: str | None = None, - actor: str = "system", - occurred_at: datetime | None = None, -) -> PlatformEvent: - """Construct a validated event.""" - if event_type not in EVENT_TYPES: - raise ValueError(f"unknown event_type: {event_type}") - return PlatformEvent( - event_id=f"pevt_{uuid.uuid4().hex[:24]}", - event_type=event_type, - channel=channel, - customer_id=customer_id, - occurred_at=occurred_at or datetime.now(timezone.utc).replace(tzinfo=None), - payload=payload or {}, - correlation_id=correlation_id, - actor=actor, - ) +def validate_event(payload: dict[str, Any]) -> dict[str, Any]: + """ + Validate ``event_type`` and required keys. Unknown types are rejected + (forces explicit extension rather than silent typos). + """ + errors: list[str] = [] + raw_type = payload.get("event_type") + if not isinstance(raw_type, str) or not raw_type.strip(): + return {"valid": False, "errors": ["event_type_required"], "normalized": None} + + try: + et = EventType(raw_type.strip()) + except ValueError: + return {"valid": False, "errors": [f"unknown_event_type:{raw_type}"], "normalized": None} + + for key in _REQUIRED[et]: + if key not in payload or payload[key] in (None, ""): + errors.append(f"missing_field:{key}") + + normalized = {"event_type": et.value, **{k: v for k, v in payload.items() if k != "event_type"}} + normalized["event_type"] = et.value + return {"valid": len(errors) == 0, "errors": errors, "normalized": normalized if not errors else None} diff --git a/dealix/auto_client_acquisition/platform_services/identity_resolution.py b/dealix/auto_client_acquisition/platform_services/identity_resolution.py index ed8e5c34..e32c70b8 100644 --- a/dealix/auto_client_acquisition/platform_services/identity_resolution.py +++ b/dealix/auto_client_acquisition/platform_services/identity_resolution.py @@ -1,91 +1,22 @@ -""" -Identity Resolution — reconcile signals from many channels into one Identity. - -Inputs: phone, email, company, social handles, CRM ids. -Output: a single Identity record with confidence per matched signal. - -Pure deterministic — production version would hit a graph DB. -""" +"""Deterministic identity merge demo — no external graph DB.""" from __future__ import annotations import hashlib -from dataclasses import dataclass, field from typing import Any -@dataclass -class Identity: - """A reconciled identity across channels.""" - - identity_id: str - primary_phone: str | None = None - primary_email: str | None = None - company: str | None = None - crm_id: str | None = None - social_handles: dict[str, str] = field(default_factory=dict) - confidence: float = 0.0 # 0..1 - sources: list[str] = field(default_factory=list) - - -def _hash_id(*parts: str) -> str: - """Deterministic ID from any combination of stable identifiers.""" - seed = "|".join(p.lower().strip() for p in parts if p) - if not seed: - return "" - h = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16] - return f"id_{h}" - - -def resolve_identity(*, signals: list[dict[str, Any]]) -> Identity: - """ - Merge a list of signals (from different channels) into one Identity. - - Each signal can be: {phone, email, company, crm_id, social_handles, source}. - """ - phones: dict[str, int] = {} - emails: dict[str, int] = {} - companies: dict[str, int] = {} - crm_ids: list[str] = [] - socials: dict[str, str] = {} - sources: list[str] = [] - - for s in signals: - ph = (s.get("phone") or "").strip() - em = (s.get("email") or "").strip().lower() - co = (s.get("company") or "").strip() - crm = (s.get("crm_id") or "").strip() - if ph: - phones[ph] = phones.get(ph, 0) + 1 - if em: - emails[em] = emails.get(em, 0) + 1 - if co: - companies[co] = companies.get(co, 0) + 1 - if crm: - crm_ids.append(crm) - for k, v in (s.get("social_handles") or {}).items(): - if k not in socials and v: - socials[k] = v - if s.get("source"): - sources.append(str(s["source"])) - - # Pick most-frequent canonical values - primary_phone = max(phones, key=phones.get) if phones else None - primary_email = max(emails, key=emails.get) if emails else None - company = max(companies, key=companies.get) if companies else None - crm_id = crm_ids[0] if crm_ids else None - - # Confidence: proportional to number of independent strong signals - strong_signals = sum(1 for x in (primary_phone, primary_email, crm_id) if x) - confidence = min(1.0, 0.30 * strong_signals + 0.10 * (1 if socials else 0)) - - return Identity( - identity_id=_hash_id(primary_phone or "", primary_email or "", crm_id or ""), - primary_phone=primary_phone, - primary_email=primary_email, - company=company, - crm_id=crm_id, - social_handles=dict(socials), - confidence=round(confidence, 3), - sources=list(dict.fromkeys(sources)), # dedupe preserve order - ) +def resolve_identity_demo( + *, + phone: str | None = None, + email: str | None = None, + company_hint: str | None = None, +) -> dict[str, Any]: + parts = "|".join([p or "" for p in (phone, email, company_hint)]) + hid = hashlib.sha256(parts.encode("utf-8")).hexdigest()[:16] + return { + "identity_key": f"id_{hid}", + "signals": {"phone": phone, "email": email, "company_hint": company_hint}, + "note_ar": "دمج تجريبي — ربط CRM وsocial handles لاحقاً.", + "demo": True, + } diff --git a/dealix/auto_client_acquisition/platform_services/inbox_feed.py b/dealix/auto_client_acquisition/platform_services/inbox_feed.py new file mode 100644 index 00000000..051b5e93 --- /dev/null +++ b/dealix/auto_client_acquisition/platform_services/inbox_feed.py @@ -0,0 +1,42 @@ +"""Deterministic unified inbox feed for demos — merges intel cards + platform inbox card.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.intelligence_layer.intel_command_feed import build_intel_command_feed +from auto_client_acquisition.platform_services.event_bus import EventType +from auto_client_acquisition.platform_services.unified_inbox import event_to_inbox_card + + +def build_inbox_feed() -> dict[str, Any]: + intel = build_intel_command_feed() + items: list[dict[str, Any]] = [] + for c in intel.get("cards") if isinstance(intel.get("cards"), list) else []: + items.append({"source_layer": "intelligence", "format": "command_card", "payload": c}) + lead_card = event_to_inbox_card( + { + "event_type": EventType.LEAD_RECEIVED.value, + "source": "trusted_simulation", + "channel_id": "website_form", + "lead_name": "عميل تجريبي", + } + ) + items.append({"source_layer": "platform", "format": "inbox_card", "payload": lead_card}) + email_card = event_to_inbox_card( + { + "event_type": EventType.EMAIL_RECEIVED.value, + "channel_id": "gmail", + "subject_ar": "استفسار عن الباقات", + } + ) + items.append({"source_layer": "platform", "format": "inbox_card", "payload": email_card}) + review_card = event_to_inbox_card( + { + "event_type": EventType.REVIEW_CREATED.value, + "channel_id": "google_business_profile", + "rating": 2, + } + ) + items.append({"source_layer": "platform", "format": "inbox_card", "payload": review_card}) + return {"items": items, "count": len(items), "demo": True, "approval_required": False} diff --git a/dealix/auto_client_acquisition/platform_services/lead_form_ingest.py b/dealix/auto_client_acquisition/platform_services/lead_form_ingest.py new file mode 100644 index 00000000..9a941721 --- /dev/null +++ b/dealix/auto_client_acquisition/platform_services/lead_form_ingest.py @@ -0,0 +1,43 @@ +"""Lead form webhook MVP — trusted simulation only, no signature crypto yet.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.platform_services.event_bus import EventType, validate_event +from auto_client_acquisition.platform_services.unified_inbox import event_to_inbox_card + +_ALLOWED_SOURCES = frozenset({"trusted_simulation"}) + + +def ingest_lead_form(body: dict[str, Any]) -> dict[str, Any]: + """ + Accepts ``source``, ``channel_id`` (linkedin_lead_form | website_form), and lead fields. + Documented contract for later HMAC verification. + """ + source = str(body.get("source") or "") + if source not in _ALLOWED_SOURCES: + return { + "ok": False, + "error": "invalid_source", + "detail_ar": "المصدر غير مسموح في MVP — استخدم trusted_simulation حتى تفعيل التوقيع.", + } + + channel_id = str(body.get("channel_id") or "website_form") + if channel_id not in ("linkedin_lead_form", "website_form"): + return {"ok": False, "error": "invalid_channel", "detail_ar": "القناة غير مدعومة في مسار الـ ingest هذا."} + + event = { + "event_type": EventType.LEAD_RECEIVED.value, + "source": source, + "channel_id": channel_id, + "lead_name": body.get("lead_name") or body.get("name") or "", + "lead_email": body.get("lead_email") or body.get("email"), + "meta": body.get("meta") if isinstance(body.get("meta"), dict) else {}, + } + v = validate_event(event) + if not v["valid"]: + return {"ok": False, "error": "validation_failed", "errors": v["errors"]} + + card = event_to_inbox_card(v["normalized"] or event) + return {"ok": True, "event": v["normalized"], "inbox_card": card, "approval_required": True} diff --git a/dealix/auto_client_acquisition/platform_services/proof_overview.py b/dealix/auto_client_acquisition/platform_services/proof_overview.py new file mode 100644 index 00000000..7ea7b19a --- /dev/null +++ b/dealix/auto_client_acquisition/platform_services/proof_overview.py @@ -0,0 +1,32 @@ +"""Single JSON view combining innovation proof summary + business proof pack demo.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.business.proof_pack import build_demo_proof_pack +from auto_client_acquisition.platform_services.proof_summary import build_proof_summary + + +def build_proof_overview() -> dict[str, Any]: + summary = build_proof_summary() + pack = build_demo_proof_pack() + return { + "demo": True, + "approval_required": False, + "innovation_ledger_summary": summary, + "business_proof_pack_excerpt": { + "executive_summary_ar": pack.get("executive_summary_ar"), + "qualified_leads": pack.get("qualified_leads"), + "meetings_booked": pack.get("meetings_booked"), + "revenue_influenced_sar": pack.get("revenue_influenced_sar"), + "next_month_plan_ar": pack.get("next_month_plan_ar"), + }, + "related_routes": { + "innovation_proof_ledger_demo": "GET /api/v1/innovation/proof-ledger/demo", + "innovation_proof_events": "GET /api/v1/innovation/proof-ledger/events", + "innovation_proof_report_week": "GET /api/v1/innovation/proof-ledger/report/week", + "business_proof_pack_demo": "GET /api/v1/business/proof-pack/demo", + "platform_proof_summary": "GET /api/v1/platform/proof/summary", + }, + } diff --git a/dealix/auto_client_acquisition/platform_services/proof_summary.py b/dealix/auto_client_acquisition/platform_services/proof_summary.py new file mode 100644 index 00000000..25d6ce18 --- /dev/null +++ b/dealix/auto_client_acquisition/platform_services/proof_summary.py @@ -0,0 +1,30 @@ +"""Summarize innovation demo proof ledger — single source for demo numbers.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.innovation.proof_ledger import build_demo_proof_ledger + + +def build_proof_summary() -> dict[str, Any]: + demo = build_demo_proof_ledger() + events = demo.get("events") if isinstance(demo.get("events"), list) else [] + total_rev = 0.0 + types: dict[str, int] = {} + for ev in events: + if not isinstance(ev, dict): + continue + et = str(ev.get("event_type") or "unknown") + types[et] = types.get(et, 0) + 1 + try: + total_rev += float(ev.get("revenue_influenced_sar_estimate") or 0) + except (TypeError, ValueError): + pass + return { + "demo": True, + "source": "innovation.proof_ledger.build_demo_proof_ledger", + "event_count": len(events), + "event_types": types, + "revenue_influenced_sar_estimate_sum": total_rev, + } diff --git a/dealix/auto_client_acquisition/platform_services/service_catalog.py b/dealix/auto_client_acquisition/platform_services/service_catalog.py index 217eb967..3cfa85f2 100644 --- a/dealix/auto_client_acquisition/platform_services/service_catalog.py +++ b/dealix/auto_client_acquisition/platform_services/service_catalog.py @@ -1,219 +1,109 @@ -""" -Service Catalog — 12 sellable services on top of the platform. - -Each service has: target_customer, outcome, deliverables, pricing_model, -required_integrations, proof_metric. -""" +"""Sellable platform services — static catalog metadata.""" from __future__ import annotations -from dataclasses import dataclass from typing import Any - -@dataclass(frozen=True) -class ServiceOffering: - """A sellable service offering.""" - - key: str - label_ar: str - label_en: str - target_customer_ar: str - outcome_ar: str - deliverables_ar: tuple[str, ...] - pricing_model_ar: str - required_integrations: tuple[str, ...] - proof_metric_ar: str - - def to_dict(self) -> dict[str, Any]: - return { - "key": self.key, - "label_ar": self.label_ar, - "label_en": self.label_en, - "target_customer_ar": self.target_customer_ar, - "outcome_ar": self.outcome_ar, - "deliverables_ar": list(self.deliverables_ar), - "pricing_model_ar": self.pricing_model_ar, - "required_integrations": list(self.required_integrations), - "proof_metric_ar": self.proof_metric_ar, - } +_SERVICES: list[dict[str, Any]] = [ + { + "id": "unified_inbox", + "name_ar": "صندوق وارد موحّد", + "tier": "platform", + "description_ar": "بطاقات قرار عربية من أحداث موحّدة مع حد ثلاثة إجراءات.", + }, + { + "id": "action_policy_engine", + "name_ar": "محرك سياسة الإجراءات", + "tier": "platform", + "description_ar": "قواعد موافقة واتساب بارد ودفع — deterministic.", + }, + { + "id": "tool_gateway_safe", + "name_ar": "بوابة أدوات آمنة", + "tier": "platform", + "description_ar": "لا إرسال حي — مسودات وحالات موافقة فقط.", + }, + { + "id": "growth_intelligence_mvp", + "name_ar": "ذكاء نمو MVP", + "tier": "intelligence", + "description_ar": "Trust Score وRevenue DNA وموجز مجلس — JSON جاهز للعرض.", + }, + { + "id": "integrations_draft_pack", + "name_ar": "حزمة مسودات تكامل", + "tier": "integrations", + "description_ar": "Gmail / Calendar / Moyasar — payloads تحقق فقط بدون OAuth.", + }, + { + "id": "growth_operator_subscription", + "name_ar": "اشتراك Growth Operator", + "tier": "subscription", + "pricing_model": "monthly_sar", + "target_customer_ar": "B2B سعودي", + "outcome_ar": "Daily brief + command feed + موافقات + Proof Pack أسبوعي.", + "proof_metric_ar": "عدد الموافقات والمسودات والأثر المقدّر.", + }, + { + "id": "channel_setup_service", + "name_ar": "خدمة إعداد القنوات", + "tier": "services", + "pricing_model": "setup_fee", + "target_customer_ar": "فرق مبيعات وعمليات", + "outcome_ar": "ربط واتساب/بريد/تقويم ضمن سياسات آمنة.", + "required_integrations": ["whatsapp", "gmail", "google_calendar", "moyasar"], + }, + { + "id": "lead_intelligence_service", + "name_ar": "ذكاء قوائم العملاء", + "tier": "services", + "pricing_model": "per_project", + "outcome_ar": "تطبيع، إزالة تكرار، تصنيف contactability.", + }, + { + "id": "partnership_sprint", + "name_ar": "Partner Sprint — ١٤ يوم", + "tier": "services", + "pricing_model": "fixed_sprint", + "outcome_ar": "قائمة شركاء + رسائل + اجتماعات مقترحة.", + }, + { + "id": "email_revenue_rescue", + "name_ar": "إنقاذ إيراد البريد", + "tier": "services", + "pricing_model": "pilot_then_monthly", + "outcome_ar": "فرص ضائعة + مسودات متابعة — بدون إرسال حتى موافقة.", + }, + { + "id": "social_growth_os", + "name_ar": "نمو اجتماعي (رسمي فقط)", + "tier": "services", + "pricing_model": "monthly_sar", + "outcome_ar": "تحويل تعليقات/نماذج رسمية إلى كروت قرار.", + }, + { + "id": "local_business_growth", + "name_ar": "نمو محلي (عيادات/متاجر)", + "tier": "vertical", + "pricing_model": "monthly_sar", + "outcome_ar": "تقييمات Google + واتساب inbound + روابط دفع draft.", + }, + { + "id": "aeo_sprint", + "name_ar": "AI Visibility / AEO Sprint", + "tier": "services", + "pricing_model": "fixed_sprint", + "outcome_ar": "فجوات ظهور وأسئلة مقترحة — بدون وعود زائفة.", + }, + { + "id": "customer_success_operator", + "name_ar": "مشغّل نجاح العملاء", + "tier": "subscription", + "pricing_model": "add_on", + "outcome_ar": "تنبيه at-risk + QBR draft — بدون إرسال تلقائي.", + }, +] -SELLABLE_SERVICES: tuple[ServiceOffering, ...] = ( - ServiceOffering( - key="growth_operator_subscription", - label_ar="Growth Operator — اشتراك شهري", - label_en="Growth Operator Subscription", - target_customer_ar="شركات B2B سعودية تبحث عن نمو منظم", - outcome_ar="فرص يومية + رسائل عربية + موافقات + Proof Pack شهري", - deliverables_ar=( - "Daily brief", "Command Feed", "Top opportunities", - "Message drafts", "Approvals", "Weekly Proof Pack", - ), - pricing_model_ar="شهري (299 / 2,999 / 7,999 ريال حسب الحجم)", - required_integrations=("whatsapp",), - proof_metric_ar="Pipeline added × monthly cost multiple", - ), - ServiceOffering( - key="channel_setup_service", - label_ar="إعداد القنوات", - label_en="Channel Setup Service", - target_customer_ar="عملاء جدد لم يربطوا قنواتهم بعد", - outcome_ar="ربط آمن لكل قنوات نمو الشركة (PDPL-compliant)", - deliverables_ar=( - "ربط WhatsApp", "ربط Gmail", "ربط Calendar", - "ربط Sheets / CRM", "ربط Moyasar", "ربط social accounts", - ), - pricing_model_ar="رسوم setup (3,000-15,000 ريال) لمرة واحدة", - required_integrations=("whatsapp", "gmail", "google_calendar", "moyasar"), - proof_metric_ar="عدد القنوات المربوطة + uptime أسبوعي", - ), - ServiceOffering( - key="lead_intelligence_service", - label_ar="Lead Intelligence — تنظيف وتصنيف القوائم", - label_en="Lead Intelligence Service", - target_customer_ar="عملاء عندهم قوائم أرقام ضخمة غير منظمة", - outcome_ar="قائمة آمنة + مصنّفة + Top-10 مرشحة للإطلاق", - deliverables_ar=( - "normalize_phone", "dedupe", "classify source", - "contactability scoring", "segmentation", "Top-10 + why_now", - ), - pricing_model_ar="رسوم لمرة + per-1000-contact pricing", - required_integrations=("website_forms", "google_sheets"), - proof_metric_ar="نسبة contacts safe + Top-10 conversion", - ), - ServiceOffering( - key="outreach_approval_service", - label_ar="Outreach بموافقة كاملة", - label_en="Outreach Approval Service", - target_customer_ar="شركات تخاف من الإرسال العشوائي", - outcome_ar="حملات outreach آمنة عبر approval-first flow", - deliverables_ar=( - "Drafts عربية", "PDPL gates", "Approval queue", - "Tracking", "Follow-up", "Proof", - ), - pricing_model_ar="مدمج مع subscription + add-on per-campaign", - required_integrations=("whatsapp", "gmail"), - proof_metric_ar="معدل الرد + meeting rate + opt-out rate", - ), - ServiceOffering( - key="partnership_sprint", - label_ar="Partnership Sprint — 14 يوم", - label_en="Partnership Sprint", - target_customer_ar="شركات تريد قناة شراكات منظمة", - outcome_ar="20 شريك محتمل + 10 رسائل + 5 اجتماعات + 1 partner offer", - deliverables_ar=( - "Target list", "Outreach drafts", "Meeting drafts", - "Partner scorecard", "Revenue share template", - ), - pricing_model_ar="رسوم ثابتة (10,000 ريال للـ sprint)", - required_integrations=("gmail", "google_calendar"), - proof_metric_ar="Partner intros replied + first deal influenced", - ), - ServiceOffering( - key="email_revenue_rescue", - label_ar="Email Revenue Rescue — استخراج فرص ضائعة", - label_en="Email Revenue Rescue", - target_customer_ar="شركات عندها inbox مزدحم وفرص ضائعة", - outcome_ar="استخراج leads + فرص + drafts من إيميل الشركة", - deliverables_ar=( - "Inbox audit", "Lost leads list", "Drafts", - "Meeting prep", "Pipeline update", - ), - pricing_model_ar="رسوم لمرة + ongoing add-on", - required_integrations=("gmail", "google_calendar"), - proof_metric_ar="عدد الفرص المُستخرجة + pipeline rescued", - ), - ServiceOffering( - key="social_growth_os", - label_ar="Social Growth OS — تعليقات + DMs + leads", - label_en="Social Growth OS", - target_customer_ar="شركات نشطة على LinkedIn / X / Instagram", - outcome_ar="تحويل التعليقات والـ mentions إلى فرص", - deliverables_ar=( - "Listening", "Reply drafts", "Lead extraction", - "DM drafts (with permission)", "Reputation tasks", - ), - pricing_model_ar="add-on شهري على Growth/Scale", - required_integrations=("x_api", "instagram_graph", "linkedin_lead_forms"), - proof_metric_ar="Social-sourced leads + replied mentions", - ), - ServiceOffering( - key="local_business_growth", - label_ar="Local Business Growth — للمتاجر والعيادات", - label_en="Local Business Growth", - target_customer_ar="عيادات + مطاعم + متاجر + فروع", - outcome_ar="إدارة Google Business + reviews + WhatsApp inbound + booking", - deliverables_ar=( - "Reviews response", "GBP posts", "Branch info sync", - "WhatsApp booking flow", "Payment links", - ), - pricing_model_ar="شهري (999-2,999 ريال) + per-location", - required_integrations=("google_business_profile", "whatsapp", "moyasar"), - proof_metric_ar="Booking rate + average review rating + revenue per location", - ), - ServiceOffering( - key="ai_visibility_aeo_sprint", - label_ar="AI Visibility / AEO Sprint", - label_en="AI Visibility / AEO Sprint", - target_customer_ar="شركات تريد تظهر في إجابات ChatGPT / Gemini / Perplexity", - outcome_ar="زيادة ظهور الشركة في answer engines + خطة محتوى 30 يوم", - deliverables_ar=( - "AEO audit", "Question-gap analysis", "Content plan", - "FAQ pages", "Comparison pages", "Local posts", - ), - pricing_model_ar="رسوم لمرة (15,000 ريال) أو monthly retainer", - required_integrations=("google_business_profile",), - proof_metric_ar="عدد الأسئلة التي تظهر فيها الشركة + competitor delta", - ), - ServiceOffering( - key="revenue_proof_pack_service", - label_ar="Revenue Proof Pack — شهري للإدارة", - label_en="Revenue Proof Pack Service", - target_customer_ar="مدراء يحتاجون إثبات قيمة Dealix شهرياً", - outcome_ar="تقرير شهري بـ ROI + grading + خطة الشهر القادم", - deliverables_ar=( - "Activity report", "Money report", "Quality + Risk report", - "Best-of insights", "Next-month plan", - ), - pricing_model_ar="مدمج مع subscription Growth/Scale", - required_integrations=(), - proof_metric_ar="Customer NPS + renewal rate", - ), - ServiceOffering( - key="customer_success_operator", - label_ar="Customer Success Operator — منع churn", - label_en="Customer Success Operator", - target_customer_ar="شركات SaaS / subscription business", - outcome_ar="health score + churn prediction + upsell signals", - deliverables_ar=( - "Health score 4-dim", "Churn prediction", - "Expansion signals", "QBR auto-drafts", - ), - pricing_model_ar="add-on على Scale tier (1,500 ريال/شهر)", - required_integrations=("crm",), - proof_metric_ar="Customer churn rate + NRR (Net Revenue Retention)", - ), - ServiceOffering( - key="payments_collections_operator", - label_ar="Payments & Collections Operator", - label_en="Payments & Collections Operator", - target_customer_ar="شركات عندها فواتير متأخرة أو payments ضائعة", - outcome_ar="quote + invoice drafts + reminders + recovery", - deliverables_ar=( - "Payment links (Moyasar)", "Invoice drafts", - "Failed-payment recovery", "Renewal reminders", - ), - pricing_model_ar="شهري + 1-3% success fee على recovered revenue", - required_integrations=("moyasar", "whatsapp", "gmail"), - proof_metric_ar="Recovered SAR + on-time payment rate", - ), -) - - -def list_services() -> dict[str, Any]: - """Catalog the platform's sellable services.""" - return { - "total": len(SELLABLE_SERVICES), - "services": [s.to_dict() for s in SELLABLE_SERVICES], - } +def get_service_catalog() -> dict[str, Any]: + return {"services": list(_SERVICES), "version": 2} diff --git a/dealix/auto_client_acquisition/platform_services/tool_gateway.py b/dealix/auto_client_acquisition/platform_services/tool_gateway.py index 12bd86f6..b5dad530 100644 --- a/dealix/auto_client_acquisition/platform_services/tool_gateway.py +++ b/dealix/auto_client_acquisition/platform_services/tool_gateway.py @@ -1,193 +1,138 @@ -""" -Safe Tool Gateway — single chokepoint for every external action. - -Returns one of: draft_created / approval_required / blocked / -ready_for_adapter / unsupported. Never executes a live action here; -the actual API call (Gmail/Calendar/WhatsApp/Moyasar/...) happens in -the dedicated adapter that's gated by an explicit env flag. -""" +"""Tool execution facade — never performs live external I/O.""" from __future__ import annotations -import os -from dataclasses import dataclass, field from typing import Any +from auto_client_acquisition.platform_services.action_ledger import get_action_ledger from auto_client_acquisition.platform_services.action_policy import evaluate_action -from auto_client_acquisition.platform_services.channel_registry import get_channel - -SUPPORTED_TOOLS: tuple[str, ...] = ( - # Gmail / Email - "gmail.create_draft", - "gmail.read_thread", - # Calendar - "calendar.draft_event", - "calendar.insert_event", - # WhatsApp - "whatsapp.send_message", - "whatsapp.draft_message", - # Moyasar - "moyasar.create_payment_link", - "moyasar.create_invoice", - "moyasar.refund", - # Social - "social.post", - "social.send_dm", - # Sheets / CRM - "sheets.append_row", - "crm.update_deal_stage", - # Reviews - "gbp.reply_review", - "gbp.publish_post", +_SUPPORTED = frozenset( + { + "send_message", + "create_payment_draft", + "moyasar_charge", + "moyasar_payment_link", + "ingest_lead", + "render_whatsapp_template_preview", + "gmail_draft", + "gmail_send", + "calendar_draft", + "calendar_insert", + "google_meet_transcript_read", + "social_reply", + } ) -@dataclass -class GatewayResult: - """Outcome of a tool invocation through the gateway.""" - - status: str # draft_created / approval_required / blocked - # / ready_for_adapter / unsupported - tool: str - matched_policy_rule: str | None = None - reasons_ar: list[str] = field(default_factory=list) - next_action_ar: str = "" - payload_passthrough: dict[str, Any] | None = None - - -# ── Live-execution flag — defaults to OFF ─────────────────────── -def _live_send_allowed(channel: str) -> bool: - """Each channel has its own env flag; OFF by default everywhere.""" - flag_map = { - "whatsapp": "WHATSAPP_ALLOW_LIVE_SEND", - "gmail": "GMAIL_ALLOW_LIVE_SEND", - "google_calendar": "CALENDAR_ALLOW_LIVE_INSERT", - "moyasar": "MOYASAR_ALLOW_LIVE_CHARGE", - "social": "SOCIAL_ALLOW_LIVE_POST", - "x_api": "SOCIAL_ALLOW_LIVE_POST", - "instagram_graph": "SOCIAL_ALLOW_LIVE_POST", - "google_business_profile": "GBP_ALLOW_LIVE_REPLY", - } - flag = flag_map.get(channel) - if not flag: - return False - return os.environ.get(flag, "false").lower() in ("1", "true", "yes") - - -# ── Public API ────────────────────────────────────────────────── -def invoke_tool( - *, - tool: str, - payload: dict[str, Any] | None = None, - context: dict[str, Any] | None = None, -) -> GatewayResult: +def execute_tool(tool_name: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: """ - Single entry point for every tool action. - - Flow: validate tool name → map to policy action → evaluate policy - → check live-send flag → return GatewayResult (never throws on - business-logic failures). + Returns one of: ``draft_created``, ``blocked``, ``approval_required``, ``unsupported``. + Never calls HTTP, SMTP, or WhatsApp APIs. """ - if tool not in SUPPORTED_TOOLS: - return GatewayResult( - status="unsupported", - tool=tool, - reasons_ar=[f"الأداة غير مدعومة: {tool}"], + body = payload or {} + ledger = get_action_ledger() + + if tool_name not in _SUPPORTED: + ledger.append_decision(tool=tool_name, outcome="unsupported", detail=body) + return {"status": "unsupported", "tool": tool_name, "approval_required": False} + + if tool_name == "send_message": + channel = str(body.get("channel_id") or "email") + action = str(body.get("action") or "external_send") + pol = evaluate_action( + action=action, + channel_id=channel, + context=body.get("context") if isinstance(body.get("context"), dict) else {}, ) + if pol["state"] == "blocked": + ledger.append_decision(tool=tool_name, outcome="blocked", detail={"policy": pol}) + return {"status": "blocked", "tool": tool_name, "policy": pol, "approval_required": False} + if pol["state"] in ("approval_required", "review"): + ledger.append_decision(tool=tool_name, outcome="approval_required", detail={"policy": pol}) + return { + "status": "approval_required", + "tool": tool_name, + "policy": pol, + "approval_required": True, + } + ledger.append_decision(tool=tool_name, outcome="draft_created", detail={"policy": pol}) + return { + "status": "draft_created", + "tool": tool_name, + "draft": {"channel_id": channel, "preview_ar": "مسودة داخلية — لا إرسال."}, + "approval_required": False, + } - channel_key = tool.split(".", 1)[0] - channel = get_channel(_normalize_channel(channel_key)) - payload = payload or {} - ctx = dict(context or {}) - if "payload" not in ctx: - ctx["payload"] = payload - - # Map tool → policy action (the granular labels the policy understands) - action_map: dict[str, str] = { - "gmail.create_draft": "create_draft", - "gmail.read_thread": "read_data", - "calendar.draft_event": "create_draft", - "calendar.insert_event": "calendar_insert_event", - "whatsapp.send_message": "send_whatsapp", - "whatsapp.draft_message": "create_draft", - "moyasar.create_payment_link": "create_draft", - "moyasar.create_invoice": "create_draft", - "moyasar.refund": "charge_payment", - "social.post": "post_social", - "social.send_dm": "send_social_dm", - "sheets.append_row": "create_draft", - "crm.update_deal_stage": "create_draft", - "gbp.reply_review": "post_social", - "gbp.publish_post": "post_social", - } - policy_action = action_map.get(tool, "create_draft") - - decision = evaluate_action(action=policy_action, context=ctx) - - if decision.decision == "blocked": - return GatewayResult( - status="blocked", - tool=tool, - matched_policy_rule=decision.matched_rule_id, - reasons_ar=decision.reasons_ar, - next_action_ar=decision.suggested_next_action_ar, - ) - if decision.decision == "approval_required": - return GatewayResult( - status="approval_required", - tool=tool, - matched_policy_rule=decision.matched_rule_id, - reasons_ar=decision.reasons_ar, - next_action_ar=decision.suggested_next_action_ar, - payload_passthrough=payload, + if tool_name in ("create_payment_draft", "moyasar_charge"): + pol = evaluate_action( + action="moyasar_charge", + channel_id="moyasar", + context={"user_confirmed": body.get("user_confirmed"), "amount_halalas": body.get("amount_halalas")}, ) + ledger.append_decision(tool=tool_name, outcome=pol["state"], detail={"policy": pol}) + return { + "status": "approval_required" if pol["state"] != "blocked" else "blocked", + "tool": tool_name, + "policy": pol, + "approval_required": pol["state"] != "blocked", + } - # decision == "allow" → check live-send flag for the channel - if _is_external_send(tool): - if _live_send_allowed(_normalize_channel(channel_key)): - return GatewayResult( - status="ready_for_adapter", - tool=tool, - reasons_ar=["السياسة موافقة + LIVE flag مفعل — جاهز لـ adapter."], - payload_passthrough=payload, - ) - # Default: keep as draft - return GatewayResult( - status="draft_created", - tool=tool, - reasons_ar=["السياسة موافقة لكن LIVE flag غير مفعل — تم حفظه draft."], - payload_passthrough=payload, - ) + if tool_name == "moyasar_payment_link": + ledger.append_decision(tool=tool_name, outcome="draft_created", detail=body) + return { + "status": "draft_created", + "tool": tool_name, + "draft": {"type": "payment_link_placeholder", "approval_required": True}, + "approval_required": False, + } - return GatewayResult( - status="draft_created", - tool=tool, - reasons_ar=["إجراء داخلي / draft — لا تفاعل خارجي."], - payload_passthrough=payload, - ) + if tool_name == "gmail_draft": + ledger.append_decision(tool=tool_name, outcome="draft_created", detail=body) + return {"status": "draft_created", "tool": tool_name, "approval_required": False} + if tool_name == "gmail_send": + ledger.append_decision(tool=tool_name, outcome="blocked", detail={"reason": "gmail_send_blocked_by_default"}) + return { + "status": "blocked", + "tool": tool_name, + "policy": {"state": "blocked", "reason_ar": "إرسال Gmail معطّل افتراضياً — استخدم مسودة + موافقة لاحقاً."}, + "approval_required": False, + } -# ── Helpers ────────────────────────────────────────────────────── -def _normalize_channel(prefix: str) -> str: - """Channel registry uses dotted keys; tool prefixes use snake.""" - return { - "calendar": "google_calendar", - "gbp": "google_business_profile", - "social": "x_api", # used as an umbrella prefix - "sheets": "google_sheets", - }.get(prefix, prefix) + if tool_name == "calendar_draft": + ledger.append_decision(tool=tool_name, outcome="draft_created", detail=body) + return {"status": "draft_created", "tool": tool_name, "approval_required": False} + if tool_name == "calendar_insert": + ledger.append_decision(tool=tool_name, outcome="approval_required", detail=body) + return { + "status": "approval_required", + "tool": tool_name, + "policy": {"state": "approval_required", "reason_ar": "إدراج حدث تقويم يحتاج موافقة صريحة."}, + "approval_required": True, + } -def _is_external_send(tool: str) -> bool: - return tool in { - "whatsapp.send_message", - "calendar.insert_event", - "moyasar.create_payment_link", - "moyasar.create_invoice", - "moyasar.refund", - "social.post", - "social.send_dm", - "gbp.reply_review", - "gbp.publish_post", - } + if tool_name == "google_meet_transcript_read": + ledger.append_decision(tool=tool_name, outcome="approval_required", detail=body) + return { + "status": "approval_required", + "tool": tool_name, + "policy": {"state": "approval_required", "reason_ar": "قراءة transcript تتطلب OAuth ونطاقات صريحة."}, + "approval_required": True, + } + + if tool_name == "social_reply": + ledger.append_decision(tool=tool_name, outcome="approval_required", detail=body) + return { + "status": "approval_required", + "tool": tool_name, + "policy": {"state": "approval_required", "reason_ar": "رد السوشيال يتطلب صلاحية قناة وموافقة."}, + "approval_required": True, + } + + if tool_name in ("ingest_lead", "render_whatsapp_template_preview"): + ledger.append_decision(tool=tool_name, outcome="draft_created", detail=body) + return {"status": "draft_created", "tool": tool_name, "approval_required": False} + + return {"status": "unsupported", "tool": tool_name, "approval_required": False} diff --git a/dealix/auto_client_acquisition/platform_services/unified_inbox.py b/dealix/auto_client_acquisition/platform_services/unified_inbox.py index 6f152236..7b7f5566 100644 --- a/dealix/auto_client_acquisition/platform_services/unified_inbox.py +++ b/dealix/auto_client_acquisition/platform_services/unified_inbox.py @@ -1,250 +1,189 @@ -""" -Unified Growth Inbox — turn platform events into Arabic action cards. - -8 card types: opportunity / email_lead / whatsapp_reply / social_comment / -payment / meeting_prep / review_response / partner_suggestion. - -Every card: title_ar, summary_ar, why_it_matters_ar, recommended_action_ar, -risk_level, expected_impact_sar, ≤3 buttons, approval_required. -""" +"""Event → Arabic inbox card (≤3 actions).""" from __future__ import annotations -from dataclasses import dataclass, field from typing import Any -from auto_client_acquisition.platform_services.event_bus import PlatformEvent +from auto_client_acquisition.innovation.command_feed import build_demo_command_feed +from auto_client_acquisition.platform_services.event_bus import EventType -CARD_TYPES: tuple[str, ...] = ( - "opportunity", - "email_lead", - "whatsapp_reply", - "social_comment", - "payment", - "meeting_prep", - "review_response", - "partner_suggestion", -) +def _trim_actions(actions: list[dict[str, str]], max_n: int = 3) -> list[dict[str, str]]: + return actions[:max_n] -@dataclass -class InboxCard: - """One card in the unified inbox.""" - - card_id: str - type: str - channel: str - title_ar: str - summary_ar: str - why_it_matters_ar: str - recommended_action_ar: str - risk_level: str # low / medium / high - expected_impact_sar: float = 0.0 - buttons_ar: tuple[str, ...] = () # ≤3 per WhatsApp limit - approval_required: bool = True - - def __post_init__(self): - if len(self.buttons_ar) > 3: - raise ValueError("buttons_ar must have ≤3 items (WhatsApp limit)") - if self.type not in CARD_TYPES: - raise ValueError(f"unknown card type: {self.type}") - if self.risk_level not in ("low", "medium", "high"): - raise ValueError(f"invalid risk_level: {self.risk_level}") - - def to_dict(self) -> dict[str, Any]: +def event_to_inbox_card(event: dict[str, Any], *, merge_demo_hint: bool = False) -> dict[str, Any]: + """Build ``title_ar``, ``summary_ar``, and up to three action buttons.""" + et_raw = event.get("event_type") + try: + et = EventType(str(et_raw)) + except (ValueError, TypeError): return { - "card_id": self.card_id, - "type": self.type, - "channel": self.channel, - "title_ar": self.title_ar, - "summary_ar": self.summary_ar, - "why_it_matters_ar": self.why_it_matters_ar, - "recommended_action_ar": self.recommended_action_ar, - "risk_level": self.risk_level, - "expected_impact_sar": self.expected_impact_sar, - "buttons_ar": list(self.buttons_ar), - "approval_required": self.approval_required, + "title_ar": "حدث غير صالح", + "summary_ar": "تعذر بناء البطاقة — نوع الحدث غير معروف.", + "actions": _trim_actions([{"action_id": "dismiss", "label_ar": "إغلاق"}]), } + actions: list[dict[str, str]] = [] + title_ar = "" + summary_ar = "" -# ── Per-event-type renderers ───────────────────────────────────── -def build_card_from_event(event: PlatformEvent) -> InboxCard | None: - """Render an event into a card. Returns None for non-actionable events.""" - et = event.event_type - p = event.payload + if et == EventType.LEAD_RECEIVED: + src = str(event.get("source") or "") + name = str(event.get("lead_name") or "جهة جديدة") + title_ar = "عميل محتمل جديد" + summary_ar = f"مصدر: {src}. الاسم: {name}." + actions = [ + {"action_id": "qualify", "label_ar": "تأهيل سريع"}, + {"action_id": "assign_owner", "label_ar": "تعيين مالك"}, + {"action_id": "archive", "label_ar": "أرشفة"}, + ] + elif et == EventType.EXTERNAL_SEND_REQUESTED: + title_ar = "طلب إرسال خارجي" + summary_ar = f"القناة: {event.get('channel_id')}. الإجراء: {event.get('action')}." + actions = [ + {"action_id": "approve_send", "label_ar": "موافقة مشروطة"}, + {"action_id": "edit_draft", "label_ar": "تعديل المسودة"}, + {"action_id": "reject", "label_ar": "رفض"}, + ] + elif et == EventType.PAYMENT_INTENT: + title_ar = "نية دفع" + summary_ar = f"المبلغ (هللات): {event.get('amount_halalas')} {event.get('currency', 'SAR')}." + actions = [ + {"action_id": "confirm_payment", "label_ar": "تأكيد المشغّل"}, + {"action_id": "adjust_amount", "label_ar": "تعديل المبلغ"}, + {"action_id": "cancel", "label_ar": "إلغاء"}, + ] + elif et == EventType.WHATSAPP_MESSAGE_REQUESTED: + title_ar = "طلب رسالة واتساب" + summary_ar = f"النية: {event.get('intent')} — الجمهور: {event.get('audience')}." + actions = [ + {"action_id": "preview_template", "label_ar": "معاينة القالب"}, + {"action_id": "require_optin_proof", "label_ar": "طلب إثبات opt-in"}, + {"action_id": "block", "label_ar": "إيقاف"}, + ] + elif et == EventType.REVIEW_REQUIRED: + title_ar = "مراجعة يدوية" + summary_ar = f"السبب: {event.get('reason_code')}." + actions = [ + {"action_id": "open_queue", "label_ar": "فتح الطابور"}, + {"action_id": "assign", "label_ar": "إسناد"}, + {"action_id": "snooze", "label_ar": "تأجيل"}, + ] + elif et == EventType.EMAIL_RECEIVED: + title_ar = "إيميل شركة جديد" + summary_ar = f"الموضوع: {event.get('subject_ar')} — القناة: {event.get('channel_id')}." + actions = [ + {"action_id": "gmail_draft_reply", "label_ar": "جهّز مسودة رد"}, + {"action_id": "classify_lead", "label_ar": "صنّف كفرصة"}, + {"action_id": "snooze_email", "label_ar": "تأجيل"}, + ] + elif et == EventType.CALENDAR_MEETING_SCHEDULED: + title_ar = "اجتماع في التقويم" + summary_ar = f"{event.get('title_ar')} — {event.get('channel_id')}." + actions = [ + {"action_id": "meeting_prep", "label_ar": "تحضير"}, + {"action_id": "calendar_draft", "label_ar": "مسودة تعديل"}, + {"action_id": "ignore_meeting", "label_ar": "تخطي"}, + ] + elif et == EventType.SOCIAL_COMMENT_RECEIVED: + title_ar = "تعليق على منشور" + summary_ar = str(event.get("snippet_ar") or "")[:200] + actions = [ + {"action_id": "draft_reply", "label_ar": "رد مسودة"}, + {"action_id": "escalate", "label_ar": "تصعيد"}, + {"action_id": "dismiss_social", "label_ar": "تجاهل"}, + ] + elif et == EventType.SOCIAL_DM_RECEIVED: + title_ar = "رسالة خاصة (سوشيال)" + summary_ar = f"من: {event.get('sender_hint')} — {event.get('channel_id')}." + actions = [ + {"action_id": "policy_check", "label_ar": "فحص سياسة"}, + {"action_id": "draft_dm", "label_ar": "مسودة رد"}, + {"action_id": "block_channel", "label_ar": "إيقاف القناة"}, + ] + elif et == EventType.LEAD_FORM_SUBMITTED: + title_ar = "نموذج ليد جديد" + summary_ar = f"مصدر: {event.get('source')} — قناة: {event.get('channel_id')}." + actions = [ + {"action_id": "qualify", "label_ar": "تأهيل"}, + {"action_id": "import_crm", "label_ar": "مسودة CRM"}, + {"action_id": "archive", "label_ar": "أرشفة"}, + ] + elif et == EventType.PAYMENT_PAID: + title_ar = "دفعة مؤكدة" + summary_ar = f"المبلغ (هللات): {event.get('amount_halalas')} {event.get('currency', 'SAR')}." + actions = [ + {"action_id": "proof_ledger", "label_ar": "سجّل في Proof"}, + {"action_id": "thank_you_draft", "label_ar": "شكر مسودة"}, + {"action_id": "upsell_draft", "label_ar": "عرض ترقية"}, + ] + elif et == EventType.PAYMENT_FAILED: + title_ar = "دفعة فاشلة" + summary_ar = f"السبب: {event.get('reason_code')} — المبلغ: {event.get('amount_halalas')}." + actions = [ + {"action_id": "retry_draft", "label_ar": "مسودة متابعة"}, + {"action_id": "support_ticket", "label_ar": "تذكرة دعم"}, + {"action_id": "close_payment", "label_ar": "إغلاق"}, + ] + elif et == EventType.REVIEW_CREATED: + title_ar = "تقييم جديد (سمعة محلية)" + summary_ar = f"التقييم: {event.get('rating')} — {event.get('channel_id')}." + actions = [ + {"action_id": "draft_review_reply", "label_ar": "رد مسودة"}, + {"action_id": "escalate_mgr", "label_ar": "تصعيد مدير"}, + {"action_id": "monitor", "label_ar": "مراقبة"}, + ] + elif et == EventType.PARTNER_SUGGESTED: + title_ar = "اقتراح شريك" + summary_ar = f"{event.get('partner_name_ar')} — قطاع {event.get('sector')}." + actions = [ + {"action_id": "partner_draft", "label_ar": "رسالة شريك"}, + {"action_id": "schedule_call", "label_ar": "مسودة اجتماع"}, + {"action_id": "skip_partner", "label_ar": "تخطي"}, + ] + elif et == EventType.ACTION_APPROVED: + title_ar = "تمت الموافقة على إجراء" + summary_ar = f"{event.get('action_id')} — بواسطة {event.get('actor')}." + actions = [ + {"action_id": "view_ledger", "label_ar": "عرض السجل"}, + {"action_id": "notify_team", "label_ar": "إشعار داخلي"}, + {"action_id": "done", "label_ar": "تم"}, + ] + elif et == EventType.ACTION_BLOCKED: + title_ar = "إجراء ممنوع" + summary_ar = f"{event.get('action_id')} — {event.get('reason_code')}." + actions = [ + {"action_id": "edit_policy", "label_ar": "مراجعة سياسة"}, + {"action_id": "appeal", "label_ar": "طلب استثناء"}, + {"action_id": "dismiss", "label_ar": "إغلاق"}, + ] + elif et == EventType.DRAFT_CREATED: + title_ar = "مسودة جاهزة" + summary_ar = f"النوع: {event.get('draft_kind')}." + actions = [ + {"action_id": "open_draft", "label_ar": "فتح المسودة"}, + {"action_id": "share", "label_ar": "مشاركة داخلية"}, + {"action_id": "discard", "label_ar": "تجاهل"}, + ] + else: + title_ar = "حدث داخلي" + summary_ar = "نوع مسجّل لكن بدون قالب عرض — راجع الإعدادات." + actions = [ + {"action_id": "dismiss", "label_ar": "إغلاق"}, + {"action_id": "log", "label_ar": "تسجيل"}, + {"action_id": "help", "label_ar": "مساعدة"}, + ] - if et == "whatsapp.message_received": - return InboxCard( - card_id=f"card_{event.event_id}", - type="whatsapp_reply", - channel="whatsapp", - title_ar=f"رد جديد من {p.get('from_name', '—')}", - summary_ar=str(p.get("text_preview", ""))[:160], - why_it_matters_ar="رد سريع خلال ٣٠ دقيقة يضاعف احتمال الحجز.", - recommended_action_ar="صنّف الرد + جهّز رد عربي مناسب", - risk_level="low", - expected_impact_sar=2_500, - buttons_ar=("اعتمد", "تخطّي", "عدّل"), - ) - - if et == "email.received": - return InboxCard( - card_id=f"card_{event.event_id}", - type="email_lead", - channel="gmail", - title_ar=f"إيميل جديد من {p.get('from', '—')}", - summary_ar=str(p.get("subject", ""))[:200], - why_it_matters_ar="إيميل من عميل محتمل — رد ≤4 ساعات يضاعف التحويل.", - recommended_action_ar="جهّز رد رسمي + عرض اجتماع 15 دقيقة", - risk_level="low", - expected_impact_sar=8_000, - buttons_ar=("جهّز مسودة", "احجز اجتماع", "تخطّي"), - ) - - if et == "calendar.meeting_scheduled": - return InboxCard( - card_id=f"card_{event.event_id}", - type="meeting_prep", - channel="google_calendar", - title_ar=f"اجتماع {p.get('when', 'قريباً')} مع {p.get('contact', '—')}", - summary_ar="جهّزت ملخص الشركة + 5 أسئلة + اعتراضات محتملة + عرض مناسب.", - why_it_matters_ar="الاجتماع المُحضَّر يرفع احتمال الإغلاق بنسبة 40%+.", - recommended_action_ar="افتح ملف التحضير + راجع الأجندة", - risk_level="low", - expected_impact_sar=p.get("expected_value_sar", 25_000), - buttons_ar=("افتح التحضير", "اكتب أجندة", "أرسل تأكيد"), - approval_required=False, - ) - - if et == "payment.failed": - return InboxCard( - card_id=f"card_{event.event_id}", - type="payment", - channel="moyasar", - title_ar="فشل دفعة", - summary_ar=f"العميل {p.get('customer_id', '—')} — مبلغ {p.get('amount_sar', 0):,.0f} ريال.", - why_it_matters_ar="فشل الدفع غالباً سببه فني — متابعة سريعة تنقذ الصفقة.", - recommended_action_ar="جهّز رسالة WhatsApp + رابط Moyasar جديد", - risk_level="medium", - expected_impact_sar=p.get("amount_sar", 2_999), - buttons_ar=("جهّز رسالة", "رابط جديد", "اتصل"), - ) - - if et == "review.created": - rating = float(p.get("rating", 5)) - risk = "high" if rating <= 2 else "medium" if rating <= 3 else "low" - return InboxCard( - card_id=f"card_{event.event_id}", - type="review_response", - channel="google_business_profile", - title_ar=f"تقييم Google جديد: {rating} نجوم", - summary_ar=str(p.get("text", ""))[:180], - why_it_matters_ar=( - "التقييم السلبي بدون رد خلال 24 ساعة يضرّ بالسمعة المحلية." - if rating <= 3 else "التقييم الإيجابي فرصة للشكر + طلب إحالة." - ), - recommended_action_ar=( - "اعتذار قصير + طلب تواصل + حل" if rating <= 3 - else "شكر دافئ + دعوة لطلب إحالة" - ), - risk_level=risk, - expected_impact_sar=1_000, - buttons_ar=("اعتمد الرد", "صعّد للمدير", "تخطّي") - if rating <= 3 - else ("اعتمد الرد", "اطلب إحالة", "تخطّي"), - ) - - if et == "social.comment_received": - return InboxCard( - card_id=f"card_{event.event_id}", - type="social_comment", - channel=event.channel, - title_ar=f"تعليق جديد على {event.channel}", - summary_ar=str(p.get("text", ""))[:150], - why_it_matters_ar="التعليقات الإيجابية = leads warmer من cold outreach.", - recommended_action_ar="جهّز رد عربي + اقترح DM لو فيه إشارة شراء", - risk_level="medium", - expected_impact_sar=1_500, - buttons_ar=("جهّز رد", "ابدأ DM", "تخطّي"), - ) - - if et == "lead.form_submitted": - return InboxCard( - card_id=f"card_{event.event_id}", - type="opportunity", - channel=event.channel, - title_ar=f"Lead جديد: {p.get('company', '—')}", - summary_ar=f"{p.get('name', '')} — {p.get('email', '')} — {p.get('city', '')}", - why_it_matters_ar="Lead تعبأ نموذج → أعلى احتمال تحويل بين كل المصادر.", - recommended_action_ar="رد ≤30 دقيقة + احجز مكالمة 15 دقيقة", - risk_level="low", - expected_impact_sar=p.get("expected_value_sar", 12_000), - buttons_ar=("جهّز رد فوري", "احجز مكالمة", "تخطّي"), - ) - - if et == "partner.suggested": - return InboxCard( - card_id=f"card_{event.event_id}", - type="partner_suggestion", - channel="internal", - title_ar=f"اقتراح شريك: {p.get('partner_name', '—')}", - summary_ar=str(p.get("rationale_ar", ""))[:200], - why_it_matters_ar="الشراكة الواحدة تفتح 3-5 leads warmer من cold.", - recommended_action_ar="جهّز رسالة warm + احجز مكالمة 20 دقيقة", - risk_level="low", - expected_impact_sar=p.get("expected_revenue_sar", 50_000), - buttons_ar=("اكتب رسالة", "احجز", "تخطّي"), - ) - - return None # non-actionable event - - -# ── Demo feed builder ──────────────────────────────────────────── -def build_demo_feed() -> dict[str, Any]: - """A deterministic demo feed for the dashboard preview.""" - from auto_client_acquisition.platform_services.event_bus import make_event - - events = [ - make_event( - event_type="lead.form_submitted", channel="website_forms", - customer_id="demo", - payload={"company": "شركة العقار الذهبي", "name": "خالد", - "email": "khalid@example.sa", "city": "الرياض", - "expected_value_sar": 18_000}, - ), - make_event( - event_type="email.received", channel="gmail", - customer_id="demo", - payload={"from": "ali@example.sa", "subject": "استفسار عن الباقات للشركات"}, - ), - make_event( - event_type="whatsapp.message_received", channel="whatsapp", - customer_id="demo", - payload={"from_name": "نورا — Saudi Logistics", - "text_preview": "ابغى أعرف وش الفرق بين Growth و Scale؟"}, - ), - make_event( - event_type="payment.failed", channel="moyasar", - customer_id="demo", - payload={"customer_id": "cust_123", "amount_sar": 2_999}, - ), - make_event( - event_type="review.created", channel="google_business_profile", - customer_id="demo", - payload={"rating": 2, "text": "تأخر الرد في عيادتنا"}, - ), - make_event( - event_type="partner.suggested", channel="internal", - customer_id="demo", - payload={"partner_name": "وكالة B2B في جدة", - "rationale_ar": "عملاؤها يحتاجون lead-gen — Dealix يكمل خدماتها.", - "expected_revenue_sar": 60_000}, - ), - ] - cards = [c.to_dict() for e in events if (c := build_card_from_event(e)) is not None] - return { - "feed_size": len(cards), - "cards": cards, - "policy_note_ar": "كل card عربي + ≤3 buttons + approval-aware.", + card: dict[str, Any] = { + "title_ar": title_ar, + "summary_ar": summary_ar, + "actions": _trim_actions(actions), + "event_type": et.value, } + if merge_demo_hint: + demo = build_demo_command_feed() + cards = demo.get("cards") if isinstance(demo.get("cards"), list) else [] + if cards and isinstance(cards[0], dict): + card["demo_hint_ar"] = str(cards[0].get("title_ar") or "") + return card diff --git a/dealix/auto_client_acquisition/revenue_company_os/__init__.py b/dealix/auto_client_acquisition/revenue_company_os/__init__.py index 40305bf1..1d522653 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/__init__.py +++ b/dealix/auto_client_acquisition/revenue_company_os/__init__.py @@ -1,67 +1,6 @@ -"""Revenue Company OS — multi-channel command feed + Revenue Work Units + self-improvement. +"""Revenue Company OS — events to cards, RWU, merged command feed.""" -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 auto_client_acquisition.revenue_company_os.command_feed_engine import build_company_os_command_feed +from auto_client_acquisition.revenue_company_os.event_to_card import event_to_card -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", -] +__all__ = ["build_company_os_command_feed", "event_to_card"] diff --git a/dealix/auto_client_acquisition/revenue_company_os/action_graph.py b/dealix/auto_client_acquisition/revenue_company_os/action_graph.py index fb555c0d..6c9d2eba 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/action_graph.py +++ b/dealix/auto_client_acquisition/revenue_company_os/action_graph.py @@ -1,123 +1,28 @@ -"""Revenue Action Graph — signal → action → outcome → proof relationships.""" +"""Signal → context → action graph (demo JSON for UI / intelligence).""" 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") +def demo_action_graph() -> dict[str, Any]: return { - "edges": list(g.edges), - "summary_a": g.what_works_for_customer("cust_A"), - "summary_b": g.what_works_for_customer("cust_B"), + "nodes": [ + {"id": "signal", "label_ar": "إشارة"}, + {"id": "context", "label_ar": "سياق"}, + {"id": "service", "label_ar": "خدمة"}, + {"id": "risk", "label_ar": "مخاطرة"}, + {"id": "draft", "label_ar": "مسودة"}, + {"id": "approval", "label_ar": "موافقة"}, + {"id": "proof", "label_ar": "Proof"}, + ], + "edges": [ + {"from": "signal", "to": "context"}, + {"from": "context", "to": "service"}, + {"from": "service", "to": "risk"}, + {"from": "risk", "to": "draft"}, + {"from": "draft", "to": "approval"}, + {"from": "approval", "to": "proof"}, + ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/revenue_company_os/channel_health.py b/dealix/auto_client_acquisition/revenue_company_os/channel_health.py index ccd4f0ba..94349f57 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/channel_health.py +++ b/dealix/auto_client_acquisition/revenue_company_os/channel_health.py @@ -1,58 +1,16 @@ -"""Channel health — cross-channel reputation snapshot for the customer.""" +"""Channel health snapshot (deterministic demo).""" 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"] +def demo_channel_health() -> dict[str, Any]: 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 - "جميع القنوات صحية الآن." - ), + "channels": [ + {"channel": "email", "health_score": 78, "notes_ar": "مسودات فقط — جيد"}, + {"channel": "whatsapp", "health_score": 62, "notes_ar": "يحتاج opt-in واضح"}, + {"channel": "linkedin", "health_score": 70, "notes_ar": "Lead Forms أولاً — لا auto-DM"}, ], + "demo": True, } 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 index b02fdeeb..98edda3c 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py +++ b/dealix/auto_client_acquisition/revenue_company_os/command_feed_engine.py @@ -1,61 +1,17 @@ -"""Command Feed engine — aggregates events across channels into a daily feed.""" +"""Merge innovation demo command feed with event-derived cards.""" from __future__ import annotations from typing import Any -from .event_to_card import build_card_from_event +from auto_client_acquisition.innovation.command_feed import build_demo_command_feed +from auto_client_acquisition.revenue_company_os.event_to_card import event_to_card -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, - ) +def build_company_os_command_feed(extra_events: list[dict[str, Any]] | None = None) -> dict[str, Any]: + base = build_demo_command_feed() + cards = list(base.get("cards") or []) + for ev in extra_events or []: + et = str(ev.get("type") or "form.submitted") + cards.append(event_to_card(et, ev.get("payload"))) + return {"cards": cards, "source": "company_os_merged", "demo": True} 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 index 4e17a4be..4812aa6d 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/event_to_card.py +++ b/dealix/auto_client_acquisition/revenue_company_os/event_to_card.py @@ -1,172 +1,72 @@ -"""Event → Card converter — every channel event becomes an Arabic decision card.""" +"""Map normalized event types to Arabic command cards (max 3 buttons).""" 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", +_EVENT_HANDLERS: dict[str, dict[str, Any]] = { + "email.received": { + "type": "inbox", + "title_ar": "بريد وارد — فرصة متابعة", + "risk_score": 22, + "recommended_action_ar": "صنّف الرد واقترح مسودة متابعة قصيرة.", + "buttons_ar": ["مسودة متابعة", "تجاهل", "إلى الصفقة"], + "proof_impact": "draft_created", + "action_mode": "draft_only", + }, + "whatsapp.reply_received": { + "type": "reply", + "title_ar": "رد واتساب — يحتاج قراراً", + "risk_score": 35, + "recommended_action_ar": "تحقق من opt-in ثم اقترح ردّاً مهنياً.", + "buttons_ar": ["مسودة رد", "تأجيل", "تصعيد"], + "proof_impact": "reply_handled", + "action_mode": "approval_required", + }, + "form.submitted": { + "type": "lead", + "title_ar": "نموذج جديد — جاهز للتأهيل", + "risk_score": 15, + "recommended_action_ar": "شغّل تشخيصاً قصيراً واربط بالخدمة المناسبة.", + "buttons_ar": ["تشخيص", "تخطي", "إسناد"], + "proof_impact": "opportunity_created", + "action_mode": "suggest_only", + }, + "payment.link_created": { + "type": "revenue", + "title_ar": "مسودة رابط دفع", + "risk_score": 10, + "recommended_action_ar": "راجع المبلغ والمرجع قبل الإرسال للعميل.", + "buttons_ar": ["موافقة مراجعة", "تعديل", "إلغاء"], + "proof_impact": "payment_link_drafted", + "action_mode": "draft_only", + }, + "risk.blocked": { + "type": "compliance_risk", + "title_ar": "إجراء مُحظور تلقائياً", + "risk_score": 95, + "recommended_action_ar": "سجّل السبب واقترح بديلاً آمناً.", + "buttons_ar": ["عرض السياسة", "مسودة بديلة", "إغلاق"], + "proof_impact": "risks_blocked", + "action_mode": "blocked", + }, + "proof.generated": { + "type": "proof_update", + "title_ar": "تحديث Proof Pack", + "risk_score": 5, + "recommended_action_ar": "أضف الحدث إلى تقرير الأسبوع.", + "buttons_ar": ["عرض الملخص", "مشاركة", "تجاهل"], + "proof_impact": "proof_generated", + "action_mode": "suggest_only", + }, } -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", - } +def event_to_card(event_type: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: + et = (event_type or "").strip().lower() + base = dict(_EVENT_HANDLERS.get(et) or _EVENT_HANDLERS["form.submitted"]) + base["event_type"] = et + base["payload_preview"] = {k: payload[k] for k in list((payload or {}).keys())[:5]} if payload else {} + if len(base.get("buttons_ar") or []) > 3: + base["buttons_ar"] = base["buttons_ar"][:3] + return {**base, "demo": True} diff --git a/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py b/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py index 51d30d0a..3d2214ee 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py +++ b/dealix/auto_client_acquisition/revenue_company_os/growth_memory.py @@ -1,108 +1,16 @@ -"""Growth memory — long-term cross-customer learning store (anonymized aggregates).""" +"""Short-term growth memory stub — replace with decision_memory / DB later.""" 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:], - } +_MEMORY: list[dict[str, Any]] = [] -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"), - } +def record_highlight(entry: dict[str, Any]) -> dict[str, Any]: + _MEMORY.append(dict(entry)) + return {"stored": True, "size": len(_MEMORY), "demo": True} + + +def recent_highlights(limit: int = 10) -> dict[str, Any]: + return {"highlights": list(_MEMORY[-limit:]), "demo": True} diff --git a/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py b/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py index d23bffb4..e1ca7a06 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py +++ b/dealix/auto_client_acquisition/revenue_company_os/opportunity_factory.py @@ -1,54 +1,19 @@ -"""Opportunity factory — turn signals into opportunity cards using Targeting OS.""" +"""Demo opportunities for Company OS feed.""" 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, - }) +def demo_opportunities() -> dict[str, Any]: return { - "sector": sector, - "city": city, - "count": len(enriched), - "opportunities": enriched, - "buying_committee_template": committee, - "do_not_do_ar": [ - "لا scraping LinkedIn ولا auto-DM.", - "لا cold WhatsApp.", - "لا تواصل بدون موافقة المالك.", + "opportunities": [ + { + "id": "opp_demo_1", + "company_ar": "شركة تدريب — الرياض", + "why_now_ar": "توسع في فريق المبيعات", + "suggested_service_id": "first_10_opportunities", + } ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py b/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py index 87894236..d19a3ed8 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py +++ b/dealix/auto_client_acquisition/revenue_company_os/proof_ledger.py @@ -1,130 +1,16 @@ -"""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. -""" +"""Proof ledger lines for Company OS (demo, in-process).""" 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) +def demo_proof_ledger() -> dict[str, Any]: + return { + "entries": [ + {"metric": "drafts_created", "delta": 3, "notes_ar": "بعد موافقة تجريبية"}, + {"metric": "risks_blocked", "delta": 1, "notes_ar": "منع إرسال جماعي مقترح"}, + {"metric": "approvals_logged", "delta": 2, "notes_ar": "سجل قرار داخلي"}, + ], + "demo": True, + } 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 index 0c82ad3b..d83694cf 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py +++ b/dealix/auto_client_acquisition/revenue_company_os/revenue_work_units.py @@ -1,95 +1,36 @@ -"""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". -""" +"""Revenue Work Units (RWU) — countable completed work items (demo).""" from __future__ import annotations -import time -import uuid from typing import Any -# Categories of Revenue Work Units. -REVENUE_WORK_UNIT_TYPES: tuple[str, ...] = ( +RWU_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)}" +def demo_work_units() -> dict[str, Any]: + units: list[dict[str, Any]] = [] + for i, ut in enumerate(RWU_TYPES[:6]): + units.append( + { + "unit_id": f"rwu_demo_{i}", + "unit_type": ut, + "service_id": "first_10_opportunities", + "customer_id": "demo_customer", + "risk_level": "low" if i % 2 == 0 else "medium", + "revenue_influenced_sar": 0, + "proof_event": ut, + "timestamp": "2026-05-01T12:00:00Z", + } ) - 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, - } + return {"work_units": units, "demo": True} 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 index ae027a76..02b5e608 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py +++ b/dealix/auto_client_acquisition/revenue_company_os/self_improvement_loop.py @@ -1,97 +1,25 @@ -"""Self-improvement loop — weekly review across services + recommendations.""" +"""Weekly self-improvement report (deterministic 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 - "ابدأ بأعلى توصية في القائمة قبل أي تجربة جديدة." - ) - +def weekly_growth_curator_report_ar() -> dict[str, Any]: 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 'لا شيء بارز'}", + "weekly_growth_curator_report_ar": ( + "ملخص أسبوعي (تجريبي): ارتفعت الموافقات على المسودات قليلاً؛ " + "يُنصح بزيادة رسائل المتابعة القصيرة بعد ٤٨ ساعة؛ " + "راجع خدمة واتساب الامتثال للعملاء عاليي المخاطر." + ), + "service_improvement_backlog": [ + "توحيد أزرار البطاقات إلى ثلاثة كحد أقصى في كل الشاشات", + "ربط Proof Pack تلقائياً بتقرير الأسبوع", ], - "recommendations_ar": recommendations, - "next_experiment_ar": next_experiment, - "best_service_id": best_service, - "approval_required": True, + "next_week_playbook_ar": [ + "١٠ لمسات يدوية موافَق عليها", + "منشور LinkedIn واحد للوكيل", + "مكالمة شريك محتمل", + ], + "demo": 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 index bf0739a4..d4b182a0 100644 --- a/dealix/auto_client_acquisition/revenue_company_os/service_factory.py +++ b/dealix/auto_client_acquisition/revenue_company_os/service_factory.py @@ -1,54 +1,12 @@ -"""Service factory — instantiate a service for a customer.""" +"""Bridge to Service Tower for Company OS.""" 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, -) +from auto_client_acquisition.service_tower.service_catalog import list_tower_services -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 - ], - } +def demo_service_snapshot() -> dict[str, Any]: + cat = list_tower_services() + return {"catalog": cat, "demo": True} diff --git a/dealix/auto_client_acquisition/revenue_launch/__init__.py b/dealix/auto_client_acquisition/revenue_launch/__init__.py index 7cf7c062..524b1cd6 100644 --- a/dealix/auto_client_acquisition/revenue_launch/__init__.py +++ b/dealix/auto_client_acquisition/revenue_launch/__init__.py @@ -1,86 +1,17 @@ -"""Revenue Launch — turn Dealix into actual paid pilots TODAY. +"""Revenue Today — offers, pipeline, pilot delivery, manual payment (no live charge).""" -Scope: - - offer_builder: build today's paid offers (499 Pilot, Growth OS Pilot, free case study) - - pipeline_tracker: deterministic pipeline schema + add/update/summarize - - outreach_sequence: build first-20 with day-by-day cadence - - demo_closer: 12-min demo wrapper + close script + objection bank - - pilot_delivery: 24-hour delivery template per service - - proof_pack_template: client-facing summary - - payment_manual_flow: Moyasar invoice/payment-link manual instructions -""" - -from __future__ import annotations - -from .demo_closer import ( - build_12_min_demo_flow as demo_12_min, - build_close_script as demo_close_script, - build_discovery_questions as demo_discovery, - build_objection_responses as demo_objections, -) -from .offer_builder import ( +from auto_client_acquisition.revenue_launch.offer_builder import ( build_499_pilot_offer, build_case_study_free_offer, build_growth_os_pilot_offer, build_private_beta_offer, recommend_offer_for_segment, ) -from .payment_manual_flow import ( - build_moyasar_invoice_instructions, - build_payment_confirmation_checklist, - build_payment_link_message, -) -from .pilot_delivery import ( - build_24h_delivery_plan, - build_client_intake_form, - build_first_10_opportunities_delivery, - build_growth_diagnostic_delivery, - build_list_intelligence_delivery, -) -from .pipeline_tracker import ( - PIPELINE_STAGES, - add_prospect, - build_pipeline_schema, - summarize_pipeline, - update_stage, -) -from .proof_pack_template import ( - build_client_summary, - build_next_step_recommendation, - build_private_beta_proof_pack, -) -from .outreach_sequence import ( - build_first_20_segments_v2, - build_followup_1, - build_followup_2, - build_outreach_message_v2, - build_reply_handlers_v2, -) __all__ = [ - # offer_builder - "build_499_pilot_offer", "build_case_study_free_offer", - "build_growth_os_pilot_offer", "build_private_beta_offer", + "build_private_beta_offer", + "build_499_pilot_offer", + "build_growth_os_pilot_offer", + "build_case_study_free_offer", "recommend_offer_for_segment", - # pipeline_tracker - "PIPELINE_STAGES", "add_prospect", "build_pipeline_schema", - "summarize_pipeline", "update_stage", - # outreach_sequence - "build_first_20_segments_v2", "build_followup_1", - "build_followup_2", "build_outreach_message_v2", - "build_reply_handlers_v2", - # demo_closer - "demo_12_min", "demo_close_script", "demo_discovery", "demo_objections", - # pilot_delivery - "build_24h_delivery_plan", "build_client_intake_form", - "build_first_10_opportunities_delivery", - "build_growth_diagnostic_delivery", - "build_list_intelligence_delivery", - # proof_pack_template - "build_client_summary", "build_next_step_recommendation", - "build_private_beta_proof_pack", - # payment_manual_flow - "build_moyasar_invoice_instructions", - "build_payment_confirmation_checklist", - "build_payment_link_message", ] diff --git a/dealix/auto_client_acquisition/revenue_launch/demo_closer.py b/dealix/auto_client_acquisition/revenue_launch/demo_closer.py index 78955341..a6de732a 100644 --- a/dealix/auto_client_acquisition/revenue_launch/demo_closer.py +++ b/dealix/auto_client_acquisition/revenue_launch/demo_closer.py @@ -1,17 +1,64 @@ -"""Demo closer — re-export single source of truth from launch_ops.""" +"""12-minute demo + discovery + close — Arabic scripts (reference).""" from __future__ import annotations -from auto_client_acquisition.launch_ops.demo_flow import ( - build_12_min_demo_flow, - build_close_script, - build_discovery_questions, - build_objection_responses, -) +from typing import Any -__all__ = [ - "build_12_min_demo_flow", - "build_close_script", - "build_discovery_questions", - "build_objection_responses", -] + +def build_12_min_demo_flow() -> dict[str, Any]: + return { + "duration_minutes": 12, + "steps_ar": [ + "٠–٢: المشكلة — تشتت قنوات وقرارات يومية غير واضحة.", + "٢–٤: Daily Brief — قرارات ومخاطر.", + "٤–٦: فرص ومهمات — مثال ١٠ فرص.", + "٦–٨: Contactability — آمن / يحتاج مراجعة / ممنوع.", + "٨–١٠: برج الخدمات — عرض ٤٩٩ و Pilot.", + "١٠–١٢: الخطوة التالية — تشخيص مجاني أو Pilot.", + ], + "closing_line_ar": "Dealix لا يرسل عشوائياً — يقرر، يكتب، يطلب موافقة، ثم يثبت النتائج.", + "demo": True, + } + + +def build_discovery_questions() -> dict[str, Any]: + return { + "questions_ar": [ + "من عميلكم المثالي اليوم؟", + "ما القناة التي تثقون بها أكثر (إيميل، واتساب opt-in، نماذج)؟", + "هل عندكم قائمة أرقام أو CRM؟", + "ما متوسط قيمة الصفقة؟", + "من يوافق على الرسائل داخل الشركة؟", + ], + "demo": True, + } + + +def build_close_script() -> dict[str, Any]: + return { + "script_ar": ( + "خلنا نجرب ٧ أيام بـ٤٩٩ ريال: نعطيكم ١٠ فرص، رسائل عربية، فحص مخاطر، خطة متابعة، وProof Pack. " + "بعدها تقررون Growth OS أو التوقف — بدون التزام تلقائي." + ), + "demo": True, + } + + +def build_objection_responses() -> dict[str, Any]: + return { + "objections_ar": [ + { + "objection": "نخاف من واتساب", + "response_ar": "نعم — واتساب فقط مع opt-in أو inbound؛ الباقي إيميل أو مهام يدوية معتمدة.", + }, + { + "objection": "هل تضمنون عملاء؟", + "response_ar": "لا نضمن نتائج — نضمن مسودات وموافقات وتقرير قياس واضح.", + }, + { + "objection": "نحتاج وقت للتفكير", + "response_ar": "تمام — أرسل لك تشخيصاً مجانياً صغيراً خلال ٢٤ ساعة لتشوف الأسلوب.", + }, + ], + "demo": True, + } diff --git a/dealix/auto_client_acquisition/revenue_launch/offer_builder.py b/dealix/auto_client_acquisition/revenue_launch/offer_builder.py index 239d9620..d58fc21d 100644 --- a/dealix/auto_client_acquisition/revenue_launch/offer_builder.py +++ b/dealix/auto_client_acquisition/revenue_launch/offer_builder.py @@ -1,131 +1,79 @@ -"""Today's paid offers — 499 Pilot, Growth OS Pilot, free case study.""" +"""Structured commercial offers — deterministic, no payment execution.""" from __future__ import annotations from typing import Any -def build_499_pilot_offer() -> dict[str, Any]: - """The headline 499 SAR Pilot — Dealix's revenue funnel entry.""" +def build_private_beta_offer() -> dict[str, Any]: return { - "offer_id": "pilot_499_7d", - "name_ar": "Pilot 7 أيام — 499 ريال", - "promise_ar": ( - "خلال 7 أيام: 10 فرص B2B + رسائل عربية + خطة متابعة + Proof Pack." - ), - "deliverables_ar": [ - "10 فرص مرتبة بـ fit_score", - "10 رسائل عربية بنبرة سعودية", - "تصنيف القنوات (safe / needs_review / blocked)", - "خطة متابعة 7 أيام", - "Proof Pack مختصر (PDF + JSON)", - "جلسة مراجعة 30 دقيقة في نهاية الأسبوع", - ], + "offer_id": "private_beta_shell", + "title_ar": "البيتا الخاصة — Dealix", + "summary_ar": "Pilot محدود: فرص، مسودات، موافقة، Proof — بدون إرسال حي افتراضياً.", + "price_sar": None, + "includes_ar": ["تشخيص أو سباق فرص", "كروت موافقة", "Proof Pack تجريبي"], + "no_live_send": True, + "demo": True, + } + + +def build_499_pilot_offer() -> dict[str, Any]: + return { + "offer_id": "pilot_7d_499", + "title_ar": "Pilot — ٧ أيام (٤٩٩ ريال)", "price_sar": 499, "duration_days": 7, - "approval_required": True, - "live_send_allowed": False, + "deliverables_ar": [ + "تشخيص نمو مختصر أو ٣ فرص عينة", + "١٠ فرص B2B مع لماذا الآن", + "١٠ رسائل عربية (مسودات)", + "فحص قابلية التواصل والمخاطر", + "خطة متابعة ٧ أيام", + "Proof Pack مختصر", + ], + "payment_ar": "فاتورة أو رابط دفع يدوي عبر Moyasar (لوحة التحكم) — لا charge من API داخل Dealix في هذه المرحلة.", "no_live_charge": True, - "payment_method": "moyasar_invoice_or_payment_link", - "delivery_starts": "next_sunday_after_payment", + "no_live_send": True, + "demo": True, } def build_growth_os_pilot_offer() -> dict[str, Any]: - """30-day Growth OS Pilot — for serious customers.""" return { "offer_id": "growth_os_pilot_30d", - "name_ar": "Growth OS Pilot — 30 يوم", - "promise_ar": ( - "تشغيل يومي للنمو لمدة شهر: command feed + drafts + اجتماعات + Proof Pack." - ), - "deliverables_ar": [ - "Daily growth brief عربي", - "First 10 Opportunities Sprint", - "List Intelligence على قائمة العميل", - "Email/WhatsApp drafts (بدون live send)", - "Meeting drafts على Calendar", - "Weekly Proof Pack", - "تحويل لـ Growth OS Monthly بعد الإثبات", - ], - "price_sar_min": 1500, - "price_sar_max": 3000, + "title_ar": "Growth OS Pilot — ٣٠ يوم", + "price_range_sar": {"min": 1500, "max": 3000}, "duration_days": 30, - "approval_required": True, + "deliverables_ar": [ + "موجز يومي تجريبي", + "١٠ فرص + ذكاء قوائم حسب الحالة", + "مسودات قنوات (بدون إرسال حي)", + "Proof Pack أسبوعي", + ], "no_live_charge": True, - "payment_method": "moyasar_invoice_or_payment_link", + "no_live_send": True, + "demo": True, } def build_case_study_free_offer() -> dict[str, Any]: - """Free Pilot in exchange for a case study + permission to publish.""" return { - "offer_id": "case_study_free_7d", - "name_ar": "Pilot مجاني مقابل case study", - "promise_ar": ( - "نسلّم Pilot 7 أيام مجاناً، وأنت تعطينا تصريحاً بنشر case study بدون " - "بيانات حساسة." - ), - "eligibility_ar": [ - "شركة سعودية أو خليجية", - "حجم متوسط (≥10 موظفين)", - "قرار سريع (مدير مفوّض على الرد)", - "موافقة كتابية على نشر النتائج بدون بيانات حساسة", - ], + "offer_id": "pilot_free_case_study", + "title_ar": "Pilot مجاني مقابل Case Study", "price_sar": 0, - "case_study_required": True, - "approval_required": True, - "no_live_charge": True, + "conditions_ar": [ + "موافقة على نشر نتائج معممة بدون بيانات حساسة", + "مقابلة قصيرة بعد الأسبوع", + ], + "no_live_send": True, + "demo": True, } -def build_private_beta_offer() -> dict[str, Any]: - """Re-export the Private Beta offer (single source of truth).""" - from auto_client_acquisition.launch_ops import PRIVATE_BETA_OFFER - return dict(PRIVATE_BETA_OFFER) - - -def recommend_offer_for_segment(segment_id: str) -> dict[str, Any]: - """Map outreach segment → best-fit paid offer.""" - s = (segment_id or "").lower().strip() - - if s == "agency_b2b": - return { - "primary_offer": "growth_os_pilot_30d", - "fallback_offer": "case_study_free_7d", - "reason_ar": ( - "وكالة → Growth OS Pilot يعطيها revenue share واضح. " - "إذا ترددت، اعرض free case study." - ), - } - if s == "training_consulting": - return { - "primary_offer": "pilot_499_7d", - "fallback_offer": "case_study_free_7d", - "reason_ar": ( - "تدريب/استشارات → Pilot 499 سريع. " - "free case study للأسماء البارزة." - ), - } - if s == "saas_tech_small": - return { - "primary_offer": "pilot_499_7d", - "fallback_offer": "growth_os_pilot_30d", - "reason_ar": ( - "SaaS صغيرة → Pilot 499 يكسر الجليد + ترقية لـ Growth OS Pilot." - ), - } - if s == "services_with_whatsapp": - return { - "primary_offer": "pilot_499_7d", - "fallback_offer": "case_study_free_7d", - "reason_ar": ( - "خدمات بقاعدة واتساب → Pilot 499 ثم WhatsApp Compliance Setup." - ), - } - - return { - "primary_offer": "pilot_499_7d", - "fallback_offer": "case_study_free_7d", - "reason_ar": "افتراضي: Pilot 499.", - } +def recommend_offer_for_segment(segment: str) -> dict[str, Any]: + s = (segment or "").strip().lower() + if s in ("agency", "وكالة"): + return {"recommended": "growth_os_pilot_30d", "reason_ar": "وكالات غالباً تحتاج نطاق أوسع وتقارير لعملاء.", "demo": True} + if s in ("founder", "مؤسس", "b2b"): + return {"recommended": "pilot_7d_499", "reason_ar": "أسرع إثبات قيمة لشركة واحدة.", "demo": True} + return {"recommended": "pilot_7d_499", "reason_ar": "افتراضي آمن للبيع السريع.", "demo": True} diff --git a/dealix/auto_client_acquisition/revenue_launch/offer_i18n.py b/dealix/auto_client_acquisition/revenue_launch/offer_i18n.py new file mode 100644 index 00000000..68b79448 --- /dev/null +++ b/dealix/auto_client_acquisition/revenue_launch/offer_i18n.py @@ -0,0 +1,75 @@ +"""Optional English labels for revenue offer JSON (?lang=en) — keeps all Arabic keys.""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from auto_client_acquisition.revenue_launch.offer_builder import ( + build_499_pilot_offer, + build_growth_os_pilot_offer, + build_private_beta_offer, +) + +_EN_BY_OFFER_ID: dict[str, dict[str, Any]] = { + "private_beta_shell": { + "title_en": "Private beta — Dealix", + "summary_en": "Limited pilot: opportunities, drafts, approvals, and proof — no live outbound by default.", + "includes_en": ["Diagnostic or opportunity sprint", "Approval cards", "Trial proof pack"], + }, + "pilot_7d_499": { + "title_en": "Pilot — 7 days (499 SAR)", + "summary_en": "Ten B2B opportunities with why-now, Arabic message drafts, contactability review, 7-day follow-up plan, short proof pack.", + "deliverables_en": [ + "Short growth diagnostic or 3 sample opportunities", + "10 B2B opportunities with why-now", + "10 Arabic message drafts", + "Contactability and risk review", + "7-day follow-up plan", + "Short proof pack", + ], + "payment_en": "Manual invoice or payment link via Moyasar dashboard — no in-app API charge at this stage.", + }, + "growth_os_pilot_30d": { + "title_en": "Growth OS pilot — 30 days", + "summary_en": "Wider rhythm: daily brief, opportunities, list intelligence as applicable, channel drafts (no live send), weekly proof.", + "deliverables_en": [ + "Trial daily brief", + "10 opportunities + list intelligence where applicable", + "Channel drafts (no live send)", + "Weekly proof pack", + ], + }, +} + + +def _merge_en(offer: dict[str, Any]) -> dict[str, Any]: + out = deepcopy(offer) + oid = str(out.get("offer_id") or "") + extra = _EN_BY_OFFER_ID.get(oid) + if extra: + out.update(extra) + return out + + +def build_revenue_offers_payload(lang: str) -> dict[str, Any]: + """Return bundle for GET /revenue-launch/offer; lang 'en' adds *_en fields alongside Arabic.""" + ln = (lang or "ar").lower() + if ln not in ("ar", "en"): + ln = "ar" + p = build_private_beta_offer() + q = build_499_pilot_offer() + g = build_growth_os_pilot_offer() + if ln == "en": + p = _merge_en(p) + q = _merge_en(q) + g = _merge_en(g) + return { + "locale": ln, + "private_beta_shell": p, + "pilot_499": q, + "growth_os_pilot": g, + "no_live_send": True, + "no_live_charge": True, + "demo": True, + } diff --git a/dealix/auto_client_acquisition/revenue_launch/outreach_sequence.py b/dealix/auto_client_acquisition/revenue_launch/outreach_sequence.py index ee268270..aa727973 100644 --- a/dealix/auto_client_acquisition/revenue_launch/outreach_sequence.py +++ b/dealix/auto_client_acquisition/revenue_launch/outreach_sequence.py @@ -1,36 +1,66 @@ -"""Outreach sequence — re-uses launch_ops with revenue-tier extensions.""" +"""First-20 segments and message templates — manual copy only.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.launch_ops.outreach_messages import ( - build_first_20_segments as _base_segments, - build_followup_message as _base_followup, - build_outreach_message as _base_msg, - build_reply_handlers as _base_handlers, -) + +def build_first_20_segments() -> dict[str, Any]: + return { + "segments": [ + {"id": "agency_b2b", "label_ar": "وكالات B2B", "count": 5}, + {"id": "training", "label_ar": "تدريب واستشارات", "count": 5}, + {"id": "saas", "label_ar": "SaaS / تقنية صغيرة", "count": 5}, + {"id": "services_whatsapp", "label_ar": "خدمات بواتساب نشط", "count": 5}, + ], + "total": 20, + "disclaimer_ar": "نسخ يدوي فقط — لا إرسال تلقائي من Dealix.", + "demo": True, + } -def build_first_20_segments_v2() -> dict[str, Any]: - """Re-export (single source of truth in launch_ops).""" - return _base_segments() +def build_outreach_message(segment: str) -> dict[str, Any]: + seg = (segment or "default").lower() + if seg == "agency_b2b": + body = ( + "هلا [الاسم]، عندنا Beta للوكالات: Dealix يساعدكم تطلعون فرص لعملائكم، " + "رسائل عربية، موافقات، وProof Pack. يناسبكم ديمو ١٥ دقيقة؟" + ) + elif seg == "training": + body = ( + "هلا [الاسم]، Dealix يطلع فرص B2B لقطاع التدريب مع سبب «لماذا الآن» ورسائل عربية — " + "أنت توافق قبل أي تواصل. نقدر نعطيكم تشخيصاً مجانياً مختصراً؟" + ) + else: + body = ( + "هلا [الاسم]، أطلقنا Beta محدودة لـ Dealix: ١٠ فرص، رسائل عربية، موافقة قبل التواصل، وProof Pack. " + "أفتح ٥ مقاعد Pilot هذا الأسبوع — يناسبكم؟" + ) + return {"segment": segment, "body_ar": body, "manual_only": True, "demo": True} -def build_outreach_message_v2( - segment_id: str, *, name: str = "[الاسم]", -) -> dict[str, Any]: - """Re-export from launch_ops.""" - return _base_msg(segment_id, name=name) +def build_followup_1(_segment: str) -> dict[str, Any]: + return { + "body_ar": "متابعة خفيفة: أقدر أرسل عينة ٣ فرص + رسالة واحدة خلال ٢٤ ساعة إذا أعطيتني رابط الموقع والقطاع والمدينة.", + "manual_only": True, + "demo": True, + } -def build_followup_1(segment_id: str, *, name: str = "[الاسم]") -> dict[str, Any]: - return _base_followup(segment_id, step=1, name=name) +def build_followup_2(_segment: str) -> dict[str, Any]: + return { + "body_ar": "إذا التوقيت مو مناسب، أقدر أرجع بعد أسبوعين — أو أغلق الملف برسالة «لا شكراً».", + "manual_only": True, + "demo": True, + } -def build_followup_2(segment_id: str, *, name: str = "[الاسم]") -> dict[str, Any]: - return _base_followup(segment_id, step=2, name=name) - - -def build_reply_handlers_v2() -> dict[str, dict[str, str]]: - return _base_handlers() +def build_reply_handlers() -> dict[str, Any]: + return { + "handlers_ar": [ + {"trigger": "مهتم", "action_ar": "أرسل رابط التشخيص أو جدول ديمو ١٥ دقيقة."}, + {"trigger": "كم السعر؟", "action_ar": "عرض ٤٩٩ لسبعة أيام أو ١٥٠٠–٣٠٠٠ لـ Growth OS Pilot ٣٠ يوم."}, + {"trigger": "لا شكراً", "action_ar": "شكراً — أغلق السجل بدون متابعة."}, + ], + "demo": True, + } diff --git a/dealix/auto_client_acquisition/revenue_launch/payment_manual_flow.py b/dealix/auto_client_acquisition/revenue_launch/payment_manual_flow.py index 7b145b47..4df255da 100644 --- a/dealix/auto_client_acquisition/revenue_launch/payment_manual_flow.py +++ b/dealix/auto_client_acquisition/revenue_launch/payment_manual_flow.py @@ -1,97 +1,43 @@ -"""Manual Moyasar invoice/payment-link flow — never charges live from API.""" +"""Manual Moyasar / invoice flow — no API charge inside Dealix.""" from __future__ import annotations from typing import Any -def build_moyasar_invoice_instructions( - *, - amount_sar: int = 499, - customer_name: str = "", - invoice_description: str = "Dealix Private Beta Pilot — 7 days", -) -> dict[str, Any]: - """ - Step-by-step instructions to create a Moyasar invoice from the dashboard. - - Never calls the API. Founder-driven only. - """ - amount_halalas = int(amount_sar) * 100 +def build_moyasar_invoice_instructions() -> dict[str, Any]: return { - "amount_sar": amount_sar, - "amount_halalas": amount_halalas, - "currency": "SAR", - "customer_name": customer_name, - "description": invoice_description, - "method": "manual_moyasar_dashboard", + "steps_ar": [ + "سجّل الدخول إلى لوحة Moyasar (بيئة sandbox أو live حسب سياسة شركتك).", + "أنشئ فاتورة أو رابط دفع بالمبلغ المتفق عليه (مثلاً ٤٩٩ ريال = ٤٩٩٠٠ هللة).", + "أرسل الرابط للعميل عبر قناة موثوقة (إيميل أو رسالة يدوية).", + "احتفظ بمرجع الدفع في pipeline_tracker يدوياً.", + ], + "amount_halalas_note_ar": "١ ريال = ١٠٠ هللة في واجهة Moyasar عادةً — راجع وثائق Moyasar الرسمية.", "no_live_charge": True, - "instructions_ar": [ - "1. افتح Moyasar dashboard.", - "2. اختر Invoices → Create Invoice.", - f"3. ضع المبلغ {amount_sar} ريال (الـ API يستخدم halalas = {amount_halalas}).", - f"4. اكتب الوصف: {invoice_description}.", - f"5. أضف اسم العميل: {customer_name or '(اسم العميل)'}.", - "6. فعّل خيار إرسال الفاتورة بالإيميل.", - "7. اضغط Send.", - "8. سجّل invoice ID + رابط الفاتورة في pipeline_tracker.", - ], - "do_not_do_ar": [ - "لا تخزّن بيانات بطاقة العميل.", - "لا تستخدم API live charge من Dealix.", - "لا ترسل دفعة بدون تأكيد العميل صراحة.", - ], + "manual_or_dashboard_only": True, + "demo": True, } -def build_payment_link_message( - *, - customer_name: str = "[الاسم]", - invoice_url: str = "[INVOICE_URL]", - amount_sar: int = 499, -) -> dict[str, Any]: - """Build the Arabic message to send to the customer with the payment link.""" - body_ar = ( - f"هلا {customer_name}،\n\n" - f"تمام، نبدأ Pilot 7 أيام بـ{amount_sar} ريال.\n\n" - "يشمل:\n" - "• 10 فرص مناسبة\n" - "• رسائل عربية جاهزة\n" - "• فحص مخاطر القنوات\n" - "• خطة متابعة 7 أيام\n" - "• Proof Pack مختصر\n\n" - f"رابط الدفع/الفاتورة: {invoice_url}\n\n" - "بعد الدفع أحتاج منك:\n" - "1. رابط موقعكم.\n" - "2. القطاع المستهدف.\n" - "3. المدينة.\n" - "4. العرض الرئيسي.\n\n" - "خلال 24 ساعة عمل بعد الدفع، أسلّمك أول دفعة من المخرجات.\n\nشاكر لك." - ) +def build_payment_link_message() -> dict[str, Any]: return { - "channel": "email_or_whatsapp", - "body_ar": body_ar, - "amount_sar": amount_sar, - "invoice_url": invoice_url, - "approval_required": True, - "live_send_allowed": False, + "template_ar": ( + "تمام، هذا رابط الدفع/الفاتورة لـ Pilot (٧ أيام — ٤٩٩ ريال). " + "بعد إتمام الدفع أرسل لي رابط الموقع + القطاع + المدينة + العرض الرئيسي." + ), + "no_live_charge": True, + "demo": True, } def build_payment_confirmation_checklist() -> dict[str, Any]: - """Checklist after the customer claims to have paid.""" return { - "title_ar": "تأكيد دفعة Moyasar", - "checks_ar": [ - "افتح Moyasar dashboard → Invoices.", - "تحقق أن invoice في حالة paid (وليس initiated أو failed).", - "تطابق amount/currency مع الفاتورة الأصلية.", - "سجّل في pipeline_tracker: stage=paid + price_sar.", - "ابعث للعميل: تأكيد + intake form + موعد الكيك-أوف.", - "ابدأ build_24h_delivery_plan.", + "checklist_ar": [ + "تأكيد استلام المبلغ في Moyasar", + "تسجيل paid في pipeline", + "إرسال نموذج intake للعميل", + "جدولة kickoff ٣٠ دقيقة", ], - "do_not_do_ar": [ - "لا تبدأ التسليم قبل تأكيد paid في Moyasar.", - "لا تشارك invoice ID في القنوات العامة.", - ], - "approval_required": True, + "demo": True, } diff --git a/dealix/auto_client_acquisition/revenue_launch/pilot_delivery.py b/dealix/auto_client_acquisition/revenue_launch/pilot_delivery.py index e14bfe87..bf0b342d 100644 --- a/dealix/auto_client_acquisition/revenue_launch/pilot_delivery.py +++ b/dealix/auto_client_acquisition/revenue_launch/pilot_delivery.py @@ -1,4 +1,4 @@ -"""24-hour pilot delivery templates per service.""" +"""Pilot delivery checklist — deterministic templates.""" from __future__ import annotations @@ -6,135 +6,66 @@ from typing import Any def build_client_intake_form() -> dict[str, Any]: - """The single intake form sent to a customer after they pay.""" return { "fields": [ - {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, - {"key": "website", "label_ar": "رابط الموقع", "required": True}, - {"key": "sector", "label_ar": "القطاع", "required": True}, - {"key": "city", "label_ar": "المدينة", "required": True}, - {"key": "primary_offer", "label_ar": "العرض الرئيسي", "required": True}, - {"key": "ideal_customer", "label_ar": "العميل المثالي", - "required": True}, - {"key": "avg_deal_value_sar", "label_ar": "متوسط قيمة الصفقة", - "required": False}, - {"key": "has_contact_list", "label_ar": "هل عندكم قائمة عملاء؟", - "required": True, "type": "boolean"}, - {"key": "channels_available", "label_ar": "القنوات المتاحة", - "required": True, "type": "multi"}, - {"key": "whatsapp_opt_in_status", - "label_ar": "حالة opt-in واتساب", "required": False}, - {"key": "approval_owner", - "label_ar": "من يوافق على الرسائل قبل الإرسال؟", - "required": True}, - {"key": "exclusions", - "label_ar": "شركات أو أشخاص لا نتواصل معهم", - "required": False, "type": "list"}, + "company_name", + "website_url", + "sector", + "city", + "main_offer", + "ideal_customer", + "avg_deal_value_sar", + "has_contact_list", + "available_channels", + "whatsapp_opt_in_status", + "approver_name", ], - "estimated_completion_minutes": 10, - "approval_required": True, + "note_ar": "لا تُخزَّن أسرار في هذا النموذج التجريبي — استخدم قنوات آمنة لجمع البيانات.", + "demo": True, } -def build_24h_delivery_plan(service_id: str) -> dict[str, Any]: - """Generic 24-hour delivery plan for any service.""" +def build_24h_delivery_plan() -> dict[str, Any]: return { - "service_id": service_id, - "phases": [ - {"phase": "T+0h", "label_ar": "كيك-أوف", - "actions_ar": ["مراجعة intake + تأكيد القناة الأساسية"]}, - {"phase": "T+1h", "label_ar": "Diagnosis", - "actions_ar": [ - "تشغيل targeting/contactability على القائمة أو القطاع", - "تحديد buying committee + why-now", - ]}, - {"phase": "T+6h", "label_ar": "Drafting", - "actions_ar": [ - "صياغة 10 رسائل عربية", - "تشغيل safety + Saudi tone evals على كل رسالة", - ]}, - {"phase": "T+18h", "label_ar": "Approval Pack", - "actions_ar": [ - "إرسال drafts للعميل في approval cards (≤3 أزرار لكل بطاقة)", - "تحديث Action Ledger", - ]}, - {"phase": "T+24h", "label_ar": "Proof Pack v1", - "actions_ar": [ - "تسليم Proof Pack المختصر", - "حجز جلسة مراجعة 30 دقيقة في نهاية الأسبوع", - ]}, + "hours": [ + {"h": "0-4", "task_ar": "جمع المدخلات والتحقق من القنوات المسموحة."}, + {"h": "4-12", "task_ar": "توليد فرص ومسودات (عرض داخلي للمراجعة)."}, + {"h": "12-20", "task_ar": "تشغيل contactability وتقرير مخاطر."}, + {"h": "20-24", "task_ar": "تسليم حزمة أولية + موعد مراجعة مع العميل."}, ], - "approval_required": True, - "live_send_allowed": False, + "demo": True, } -def build_first_10_opportunities_delivery(intake: dict[str, Any]) -> dict[str, Any]: - """Service-specific delivery for First 10 Opportunities Sprint.""" +def build_first_10_opportunities_delivery() -> dict[str, Any]: return { - "service_id": "first_10_opportunities_sprint", - "intake_received": bool(intake), - "delivery_steps_ar": [ - "تشغيل account_finder على (sector, city) + offer.", - "buyer_role_mapper لكل شركة → 1 DM + 2 influencers.", - "explain_why_now لكل شركة (Arabic).", - "draft_b2b_email و/أو draft_whatsapp_message حسب القناة.", - "safety_eval + saudi_tone_eval على كل رسالة قبل التسليم.", - "بناء follow-up sequence لـ7 أيام.", - "Proof Pack v1 (PDF + JSON).", - ], - "deliverables": [ - "10 opportunity cards", - "10 Arabic messages", - "follow-up plan", - "Proof Pack v1", + "deliverables_ar": [ + "١٠ فرص مع لماذا الآن", + "١٠ رسائل عربية (مسودات)", + "توصية قناة لكل فرصة", + "خطة متابعة ٧ أيام", ], "approval_required": True, + "demo": True, } -def build_list_intelligence_delivery(intake: dict[str, Any]) -> dict[str, Any]: - """Service-specific delivery for List Intelligence.""" +def build_list_intelligence_delivery() -> dict[str, Any]: return { - "service_id": "list_intelligence", - "intake_received": bool(intake), - "delivery_steps_ar": [ - "تنظيف الـ CSV + dedupe.", - "classify_source لكل صف.", - "evaluate_contactability + allowed_channels لكل contact.", - "تقسيم القائمة: safe / needs_review / blocked.", - "اختيار أفضل 50 target.", - "كتابة رسائل عربية للقطاع المهيمن.", - "Risk report + retention recommendation.", - ], - "deliverables": [ - "Cleaned CSV", - "Top 50 targets", - "Arabic messages per segment", - "Risk report", - "Channel mix recommendation", + "deliverables_ar": [ + "تقرير تنظيف وتصنيف مصدر", + "أفضل ٥٠ هدفاً (تجريبي)", + "مسودات رسائل للآمن فقط", + "تقرير مخاطر", ], "approval_required": True, + "demo": True, } -def build_growth_diagnostic_delivery(intake: dict[str, Any]) -> dict[str, Any]: - """Free 24-hour growth diagnostic delivery.""" +def build_growth_diagnostic_delivery() -> dict[str, Any]: return { - "service_id": "free_growth_diagnostic", - "intake_received": bool(intake), - "delivery_steps_ar": [ - "تشغيل recommend_accounts(sector, city) → 3 فرص.", - "كتابة رسالة عربية واحدة جاهزة.", - "تقرير risk سريع (واتساب opt-in / domain reputation / channel mix).", - "توصية بالخدمة المدفوعة الأنسب (Pilot 499 / Growth OS Pilot).", - ], - "deliverables": [ - "3 opportunities", - "1 Arabic message", - "Risk note", - "Paid pilot recommendation", - ], - "delivery_time": "خلال 24 ساعة عمل", - "approval_required": True, + "deliverables_ar": ["٣ فرص", "١ رسالة", "١ ملاحظة مخاطر", "١ توصية خدمة مدفوعة"], + "approval_required": False, + "demo": True, } diff --git a/dealix/auto_client_acquisition/revenue_launch/pipeline_tracker.py b/dealix/auto_client_acquisition/revenue_launch/pipeline_tracker.py index 0b4ec07e..647dc65c 100644 --- a/dealix/auto_client_acquisition/revenue_launch/pipeline_tracker.py +++ b/dealix/auto_client_acquisition/revenue_launch/pipeline_tracker.py @@ -1,10 +1,10 @@ -"""Deterministic pipeline tracker — schema, add, update, summarize.""" +"""Simple pipeline schema for founder-led beta — in-memory demo only.""" from __future__ import annotations from typing import Any -PIPELINE_STAGES: tuple[str, ...] = ( +STAGES = [ "identified", "contacted", "replied", @@ -13,143 +13,37 @@ PIPELINE_STAGES: tuple[str, ...] = ( "pilot_offered", "paid", "lost", -) - -# Default Sheet/CSV columns the pipeline tracker emits. -PIPELINE_COLUMNS: tuple[str, ...] = ( - "company", "person", "segment", "source", "channel", - "message_sent_at", "reply_status", "stage", - "demo_booked", "service_offered", "price_sar", - "paid", "next_step", "notes", -) +] def build_pipeline_schema() -> dict[str, Any]: - """Return the canonical pipeline schema (deterministic).""" return { - "stages": list(PIPELINE_STAGES), - "columns": list(PIPELINE_COLUMNS), - "stage_progression": [ - {"from": "identified", "to": "contacted", "trigger": "outreach_sent"}, - {"from": "contacted", "to": "replied", "trigger": "reply_received"}, - {"from": "replied", "to": "demo_booked", "trigger": "demo_scheduled"}, - {"from": "demo_booked", "to": "diagnostic_sent", "trigger": "diagnostic_delivered"}, - {"from": "diagnostic_sent", "to": "pilot_offered", "trigger": "offer_sent"}, - {"from": "pilot_offered", "to": "paid", "trigger": "moyasar_invoice_paid"}, - ], - "loss_reasons_ar": [ - "السعر", - "التوقيت", - "بديل قائم", - "صانع القرار غير متاح", - "PDPL/أمان", - "لا حاجة الآن", - ], - "notes_ar": ( - "هذا المخطط deterministic. كل صفقة تتقدم بـ trigger صريح فقط، " - "ولا يحدث تغيير stage بدون event موثّق." - ), + "stages": STAGES, + "fields_ar": ["company", "person", "segment", "channel", "stage", "next_step_ar", "notes"], + "demo": True, } -def add_prospect( - *, - pipeline: list[dict[str, Any]] | None = None, - company: str, - person: str = "", - segment: str = "", - source: str = "manual", - channel: str = "email", - notes: str = "", -) -> dict[str, Any]: - """Add a new prospect to the in-memory pipeline. Stage starts at identified.""" - entry: dict[str, Any] = { +def add_prospect(company: str, person: str, segment: str, channel: str) -> dict[str, Any]: + return { + "id": f"prospect_{hash(company + person) % 10_000_000}", "company": company, "person": person, "segment": segment, - "source": source, "channel": channel, - "message_sent_at": None, - "reply_status": "none", "stage": "identified", - "demo_booked": False, - "service_offered": "", - "price_sar": 0, - "paid": False, - "next_step": "send_first_outreach", - "notes": notes[:300], + "demo": True, } - if pipeline is not None: - pipeline.append(entry) - return entry -def update_stage( - *, - prospect: dict[str, Any], - new_stage: str, - notes: str = "", -) -> dict[str, Any]: - """Move a prospect to a new stage. Validates the new stage is known.""" - if new_stage not in PIPELINE_STAGES: - raise ValueError( - f"Unknown stage: {new_stage}. " - f"Valid: {', '.join(PIPELINE_STAGES)}" - ) - prospect["stage"] = new_stage - if notes: - existing = str(prospect.get("notes", "")) - sep = " | " if existing else "" - prospect["notes"] = (existing + sep + notes)[:300] - if new_stage == "paid": - prospect["paid"] = True - prospect["next_step"] = "deliver_24h" - elif new_stage == "lost": - prospect["next_step"] = "archive" - return prospect +def update_stage(prospect_id: str, new_stage: str) -> dict[str, Any]: + st = new_stage if new_stage in STAGES else "identified" + return {"id": prospect_id, "stage": st, "ok": True, "demo": True} -def summarize_pipeline( - pipeline: list[dict[str, Any]] | None, -) -> dict[str, Any]: - """Aggregate pipeline counts + revenue.""" - pipeline = pipeline or [] - by_stage: dict[str, int] = {s: 0 for s in PIPELINE_STAGES} - by_segment: dict[str, int] = {} - revenue_paid_sar = 0.0 - revenue_offered_sar = 0.0 - - for p in pipeline: - stage = str(p.get("stage", "identified")) - if stage in by_stage: - by_stage[stage] += 1 - seg = str(p.get("segment", "unknown")) - by_segment[seg] = by_segment.get(seg, 0) + 1 - price = float(p.get("price_sar", 0) or 0) - if p.get("paid"): - revenue_paid_sar += price - if stage in ("pilot_offered", "paid"): - revenue_offered_sar += price - - total = len(pipeline) - won = by_stage["paid"] - lost = by_stage["lost"] - closed = won + lost - win_rate = round(won / closed, 3) if closed else 0.0 - - return { - "total_prospects": total, - "by_stage": by_stage, - "by_segment": by_segment, - "revenue_paid_sar": round(revenue_paid_sar, 2), - "revenue_offered_sar": round(revenue_offered_sar, 2), - "win_rate": win_rate, - "summary_ar": [ - f"إجمالي الـ prospects: {total}", - f"اتصالات: {by_stage['contacted']} | ردود: {by_stage['replied']}", - f"ديموهات: {by_stage['demo_booked']} | عروض: {by_stage['pilot_offered']}", - f"مدفوعة: {by_stage['paid']} | خسرت: {by_stage['lost']}", - f"إيراد محصّل: {revenue_paid_sar:.0f} ريال", - f"win rate: {win_rate * 100:.1f}%", - ], - } +def summarize_pipeline(prospects: list[dict[str, Any]] | None = None) -> dict[str, Any]: + rows = prospects or [] + counts: dict[str, int] = {s: 0 for s in STAGES} + for r in rows: + counts[r.get("stage", "identified")] = counts.get(r.get("stage", "identified"), 0) + 1 + return {"counts_by_stage": counts, "total": len(rows), "demo": True} diff --git a/dealix/auto_client_acquisition/revenue_launch/proof_pack_template.py b/dealix/auto_client_acquisition/revenue_launch/proof_pack_template.py index 55c2b02f..65c332ce 100644 --- a/dealix/auto_client_acquisition/revenue_launch/proof_pack_template.py +++ b/dealix/auto_client_acquisition/revenue_launch/proof_pack_template.py @@ -1,100 +1,44 @@ -"""Proof Pack template — client-facing summary at end of Pilot.""" +"""Proof Pack template for private beta pilots.""" from __future__ import annotations from typing import Any -def build_private_beta_proof_pack( - *, - company_name: str = "", - metrics: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build the private-beta Proof Pack template (Arabic).""" - metrics = metrics or {} +def build_private_beta_proof_pack() -> dict[str, Any]: return { - "title_ar": f"Proof Pack — {company_name or 'Pilot 7 أيام'}", "sections_ar": [ - "ملخص تنفيذي (5 أسطر)", - "ما عمله Dealix هذا الأسبوع", - "النتائج بالأرقام (vs أهداف الأسبوع)", - "أبرز الردود والاعتراضات", - "المخاطر التي تم منعها (PDPL/سمعة القناة)", - "أفضل 3 رسائل (مع safety+tone scores)", - "Action Ledger (كل فعل + مَن اعتمده)", - "التوصية بالخطوة التالية", + "ملخص الأسبوع", + "الفرص المقترحة والمعتمدة/المتخطاة", + "المسودات التي أُنشئت", + "المخاطر التي تم كشفها أو منعها", + "الاجتماعات المقترحة (مسودات فقط إن وُجدت)", + "الخطوة التالية والترقية المقترحة", ], - "metrics_to_include": [ - "opportunities_generated", - "drafts_approved", - "positive_replies", - "meetings_drafted", - "pipeline_influenced_sar", - "risks_blocked", - "time_saved_hours", + "metrics_keys": [ + "opportunities_count", + "drafts_created", + "approvals_pending", + "risks_flagged", + "meetings_suggested", ], - "captured_metrics": metrics, - "approval_required": True, - "delivery_format": ["pdf", "json", "whatsapp_summary"], + "demo": True, } -def build_client_summary( - *, - company_name: str = "", - opportunities_count: int = 0, - approved_drafts: int = 0, - meetings: int = 0, - pipeline_sar: float = 0.0, - risks_blocked: int = 0, -) -> dict[str, Any]: - """5-line Arabic executive summary for the client.""" - lines = [ - f"خلال 7 أيام، شغّل Dealix Pilot لشركة {company_name or '(العميل)'}.", - f"تم توليد {opportunities_count} فرصة B2B + اعتماد {approved_drafts} رسالة.", - f"نتج عن ذلك {meetings} اجتماع و pipeline متأثر بقيمة {pipeline_sar:.0f} ريال.", - f"تم منع {risks_blocked} مخاطر تواصل تلقائياً (PDPL/cold WhatsApp/سمعة).", - "التوصية: الترقية لـ Growth OS Pilot 30 يوم لتثبيت العائد المتكرر.", - ] +def build_client_summary(metrics: dict[str, Any] | None = None) -> dict[str, Any]: + m = metrics or {} return { - "company_name": company_name, - "summary_ar": lines, - "approval_required": True, - "deliverable_format": "5_line_executive_summary", + "one_line_ar": ( + f"تمت معالجة {m.get('opportunities_count', 0)} فرصة تقريباً مع " + f"{m.get('drafts_created', 0)} مسودة — المخاطر المسجلة: {m.get('risks_flagged', 0)}." + ), + "demo": True, } -def build_next_step_recommendation( - *, - pilot_metrics: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Recommend next step based on pilot outcome metrics.""" - m = pilot_metrics or {} - pipeline_sar = float(m.get("pipeline_sar", 0)) - meetings = int(m.get("meetings", 0)) - csat = int(m.get("csat", 0)) # 0..10 - - if csat >= 8 and (pipeline_sar >= 25_000 or meetings >= 2): - action = "upsell_growth_os_monthly" - msg = ( - "Pilot قوي — اعرض Growth OS Monthly بـ2,999 ريال شهرياً مع " - "خصم 15% على الاشتراك السنوي." - ) - elif pipeline_sar < 5_000 and meetings == 0: - action = "iterate_or_archive" - msg = ( - "النتائج ضعيفة هذه الجولة. اقترح زاوية مختلفة (قطاع/عرض) " - "أو أرشف العميل بدون ضغط." - ) - else: - action = "extend_pilot" - msg = ( - "Pilot واعد. مدّد الأسبوع لأسبوعين بـ500 ريال إضافي، " - "أو أضف قناة (Email + LinkedIn Lead Form)." - ) - +def build_next_step_recommendation() -> dict[str, Any]: return { - "next_action": action, - "recommendation_ar": msg, - "approval_required": True, + "next_step_ar": "إذا ارتفعت جودة القائمة: انتقل إلى Growth OS Pilot أو ذكاء قوائم أوسع.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/security_curator/__init__.py b/dealix/auto_client_acquisition/security_curator/__init__.py index c99e6eb6..f3929a5a 100644 --- a/dealix/auto_client_acquisition/security_curator/__init__.py +++ b/dealix/auto_client_acquisition/security_curator/__init__.py @@ -1,46 +1,13 @@ -"""Security Curator — secret redaction + patch firewall + trace sanitization. +"""Security curator — redaction and diff inspection before agents touch repos.""" -Inspired by Hermes Agent's Curator pattern, but specialized for Dealix's -external-action surface (WhatsApp, Gmail, Calendar, Moyasar, Social). - -Goals: -- Never let an API key, token, or PAT escape into a log/trace/embedding/patch. -- Block any diff that adds .env files or secret-shaped strings. -- Sanitize tool outputs before they go into the Action Ledger or Proof Pack. -""" - -from __future__ import annotations - -from .patch_firewall import ( - PatchFirewallResult, - inspect_diff, - is_safe_diff, -) -from .secret_redactor import ( - DEFAULT_PATTERNS, - SecretFinding, - detect_secret_patterns, - redact_secrets, - scan_payload, -) -from .tool_output_sanitizer import ( - sanitize_tool_output, - sanitize_trace_event, -) -from .trace_redactor import ( - redact_trace, -) +from auto_client_acquisition.security_curator.patch_firewall import inspect_diff +from auto_client_acquisition.security_curator.secret_redactor import redact_secrets, scan_payload +from auto_client_acquisition.security_curator.trace_redactor import redact_span_metadata, redact_trace_payload __all__ = [ - "DEFAULT_PATTERNS", - "PatchFirewallResult", - "SecretFinding", - "detect_secret_patterns", "inspect_diff", - "is_safe_diff", "redact_secrets", - "redact_trace", - "sanitize_tool_output", - "sanitize_trace_event", + "redact_span_metadata", + "redact_trace_payload", "scan_payload", ] diff --git a/dealix/auto_client_acquisition/security_curator/patch_firewall.py b/dealix/auto_client_acquisition/security_curator/patch_firewall.py index df7e485e..3233e3e6 100644 --- a/dealix/auto_client_acquisition/security_curator/patch_firewall.py +++ b/dealix/auto_client_acquisition/security_curator/patch_firewall.py @@ -1,99 +1,35 @@ -"""Patch Firewall — block unsafe diffs before they enter the repo.""" +"""Block risky diffs before they reach git — text inspection only.""" from __future__ import annotations import re -from dataclasses import dataclass, field - -from .secret_redactor import detect_secret_patterns - -# Files that should never be added to the repo via patch. -DANGEROUS_FILE_PATTERNS: tuple[str, ...] = ( - r"^\+\+\+ b/.*\.env$", - r"^\+\+\+ b/.*\.env\.local$", - r"^\+\+\+ b/.*\.env\.staging$", - r"^\+\+\+ b/.*\.env\.production$", - r"^\+\+\+ b/.*credentials\.json$", - r"^\+\+\+ b/.*service[-_]account.*\.json$", - r"^\+\+\+ b/.*id_rsa$", - r"^\+\+\+ b/.*\.pem$", - r"^\+\+\+ b/.*\.p12$", - r"^\+\+\+ b/.*\.pfx$", -) +from typing import Any -@dataclass(frozen=True) -class PatchFirewallResult: - safe: bool - reasons_ar: list[str] = field(default_factory=list) - blocked_files: list[str] = field(default_factory=list) - secret_findings: list[dict[str, str]] = field(default_factory=list) - - def to_dict(self) -> dict[str, object]: - return { - "safe": self.safe, - "reasons_ar": self.reasons_ar, - "blocked_files": self.blocked_files, - "secret_findings": self.secret_findings, - } - - -def _added_lines(diff_text: str) -> str: - """Concatenate only the *added* lines from a unified diff.""" - out: list[str] = [] - for line in diff_text.splitlines(): - if line.startswith("+++") or line.startswith("---"): - continue - if line.startswith("+"): - out.append(line[1:]) - return "\n".join(out) - - -def _blocked_files_in_diff(diff_text: str) -> list[str]: - blocked: list[str] = [] - for line in diff_text.splitlines(): - for pat in DANGEROUS_FILE_PATTERNS: - if re.match(pat, line): - blocked.append(line.replace("+++ b/", "")) - break - return blocked - - -def inspect_diff(diff_text: str) -> PatchFirewallResult: +def inspect_diff(diff_text: str) -> dict[str, Any]: """ - Inspect a unified-diff blob. - - Returns PatchFirewallResult.safe = False if: - - The diff adds a file from DANGEROUS_FILE_PATTERNS, OR - - Any added line contains a known secret pattern. + Returns ``allowed`` bool and ``reasons_ar`` list. + MVP heuristics only — not a full patch parser. """ - if not diff_text: - return PatchFirewallResult(safe=True) - reasons: list[str] = [] - blocked = _blocked_files_in_diff(diff_text) - if blocked: - reasons.append(f"الملفات المحظورة: {', '.join(blocked)}") + if not diff_text or not diff_text.strip(): + return {"allowed": True, "reasons_ar": [], "detail": "empty_diff"} - added = _added_lines(diff_text) - findings = detect_secret_patterns(added) - finding_dicts = [ - {"label": f.label, "sample_redacted": f.sample_redacted} - for f in findings - ] - if findings: - labels = sorted({f.label for f in findings}) - reasons.append(f"تم اكتشاف أسرار محتملة: {', '.join(labels)}") + if re.search(r"^\+.*\.env", diff_text, re.MULTILINE) or re.search(r"^\+.*\.env\.", diff_text, re.MULTILINE): + reasons.append("يحتوي على إضافة ملف بيئة (.env) — مرفوض في المسار الآلي.") - safe = not reasons - return PatchFirewallResult( - safe=safe, - reasons_ar=reasons, - blocked_files=blocked, - secret_findings=finding_dicts, - ) + if "ghp_" in diff_text or "github_pat_" in diff_text: + reasons.append("فرق يحتوي على رمز GitHub — مرفوض.") + if re.search(r"(?i)(supabase_service_role|openai_api_key|anthropic_api_key)\s*=", diff_text): + reasons.append("فرق يحتوي على تعيين مفتاح حساس — راجع يدوياً.") -def is_safe_diff(diff_text: str) -> bool: - """Convenience boolean wrapper around inspect_diff().""" - return inspect_diff(diff_text).safe + lower = diff_text.lower() + if ".pem" in lower and "begin" in lower and "private" in lower: + reasons.append("مفتاح خاص (PEM) في الفرق — مرفوض.") + + return { + "allowed": len(reasons) == 0, + "reasons_ar": reasons, + "detail": "heuristic_scan", + } diff --git a/dealix/auto_client_acquisition/security_curator/secret_redactor.py b/dealix/auto_client_acquisition/security_curator/secret_redactor.py index cfb62e70..90b3fc0e 100644 --- a/dealix/auto_client_acquisition/security_curator/secret_redactor.py +++ b/dealix/auto_client_acquisition/security_curator/secret_redactor.py @@ -1,113 +1,66 @@ -"""Secret Redactor — detect + redact secret-shaped strings before they leak.""" +"""Detect and redact common secret patterns from text and nested payloads.""" from __future__ import annotations +import copy +import json import re -from dataclasses import dataclass from typing import Any -# Patterns are intentionally specific to avoid false positives. -# Each entry: (label, regex, redaction_template). -DEFAULT_PATTERNS: tuple[tuple[str, str, str], ...] = ( - ("github_pat", r"ghp_[A-Za-z0-9]{20,}", "ghp_***"), - ("github_pat_legacy", r"github_pat_[A-Za-z0-9_]{20,}", "github_pat_***"), - ("openai_key", r"sk-[A-Za-z0-9]{20,}", "sk-***"), - ("anthropic_key", r"sk-ant-[A-Za-z0-9_\-]{20,}", "sk-ant-***"), - ("supabase_service_role", r"eyJ[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{20,}", "eyJ.***.***"), - ("whatsapp_token", r"EAA[A-Za-z0-9]{30,}", "EAA***"), - ("moyasar_secret", r"sk_(?:test|live)_[A-Za-z0-9]{20,}", "sk_***_***"), - ("langfuse_secret", r"lf_sk_[A-Za-z0-9]{20,}", "lf_sk_***"), - ("sentry_dsn", r"https://[A-Za-z0-9]{20,}@[A-Za-z0-9.\-]+/\d+", "https://***@***/***"), - ("aws_access_key", r"AKIA[A-Z0-9]{16}", "AKIA***"), - ("google_api_key", r"AIza[A-Za-z0-9_\-]{30,}", "AIza***"), - ("private_key_block", r"-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----", "-----BEGIN PRIVATE KEY *** REDACTED ***-----"), -) - -# Sensitive keys for dict-shaped payloads (case-insensitive substring match). -SENSITIVE_PAYLOAD_KEYS: tuple[str, ...] = ( - "api_key", "apikey", "secret", "token", "password", "passwd", - "authorization", "auth_token", "access_token", "refresh_token", - "client_secret", "private_key", "ssn", "credit_card", "card_number", - "cvv", "iban", "moyasar_secret", -) - - -@dataclass(frozen=True) -class SecretFinding: - """A single secret detected in input.""" - label: str - span: tuple[int, int] - sample_redacted: str # the *redacted* form, never the raw secret - - -def detect_secret_patterns(text: str) -> list[SecretFinding]: - """Find secret-shaped substrings. Never returns the raw secret.""" - if not text: - return [] - findings: list[SecretFinding] = [] - for label, pattern, redaction in DEFAULT_PATTERNS: - for m in re.finditer(pattern, text): - findings.append(SecretFinding( - label=label, - span=(m.start(), m.end()), - sample_redacted=redaction, - )) - return findings +_PATTERNS: list[tuple[re.Pattern[str], str]] = [ + (re.compile(r"ghp_[A-Za-z0-9]{20,}"), "ghp_"), + (re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), "github_pat_"), + (re.compile(r"sk_live_[A-Za-z0-9]{20,}"), "sk_live_"), + ( + re.compile(r"(?i)(OPENAI_API_KEY|ANTHROPIC_API_KEY|DEEPSEEK_API_KEY|GROQ_API_KEY)\s*[=:]\s*[\w\-]{8,}"), + r"\1=", + ), + (re.compile(r"(?i)SUPABASE_SERVICE_ROLE_KEY\s*[=:]\s*[\w\-.]{10,}"), "SUPABASE_SERVICE_ROLE_KEY="), + (re.compile(r"(?i)WHATSAPP_ACCESS_TOKEN\s*[=:]\s*[\w\-.]{10,}"), "WHATSAPP_ACCESS_TOKEN="), + (re.compile(r"(?i)MOYASAR_SECRET\s*[=:]\s*[\w\-.]{6,}"), "MOYASAR_SECRET="), + (re.compile(r"(?i)LANGFUSE_SECRET_KEY\s*[=:]\s*[\w\-.]{6,}"), "LANGFUSE_SECRET_KEY="), + ( + re.compile(r"https://[a-f0-9]+@[a-z0-9.-]+\.ingest\.[a-z0-9.-]+\.sentry\.io/\d+"), + "https://@sentry.io/", + ), +] def redact_secrets(text: str) -> str: - """Replace every detected secret with a label-typed redaction marker.""" if not text: return text out = text - for _label, pattern, redaction in DEFAULT_PATTERNS: - out = re.sub(pattern, redaction, out) + for pat, repl in _PATTERNS: + out = pat.sub(repl, out) return out -def _is_sensitive_key(key: str) -> bool: - k = key.lower() - return any(s in k for s in SENSITIVE_PAYLOAD_KEYS) +def scan_payload(payload: Any) -> list[str]: + """Return list of human-readable findings (empty if clean).""" + findings: list[str] = [] + raw = json.dumps(payload, ensure_ascii=False, default=str) if not isinstance(payload, str) else payload + if "ghp_" in raw or "github_pat_" in raw: + findings.append("possible_github_token") + if re.search(r"sk_live_", raw): + findings.append("possible_stripe_live") + if re.search(r"(?i)(OPENAI_API_KEY|ANTHROPIC_API_KEY)\s*[=:]", raw): + findings.append("possible_llm_key_assignment") + if ".env" in raw and ("=" in raw or ":" in raw): + findings.append("possible_env_dump") + return findings -def scan_payload(payload: Any) -> dict[str, Any]: - """ - Scan a JSON-shaped payload for secret-typed keys + secret-shaped values. +def sanitize_for_trace(payload: dict[str, Any]) -> dict[str, Any]: + """Deep-copy and redact string leaves (MVP).""" + data = copy.deepcopy(payload) - Returns: - { - "has_secrets": bool, - "findings": [{"label", "path"}], - "redacted": , - } - """ - findings: list[dict[str, str]] = [] + def _walk(obj: Any) -> Any: + if isinstance(obj, dict): + return {k: _walk(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_walk(v) for v in obj] + if isinstance(obj, str): + return redact_secrets(obj) + return obj - def _walk(node: Any, path: str) -> Any: - if isinstance(node, dict): - out: dict[str, Any] = {} - for k, v in node.items(): - p = f"{path}.{k}" if path else str(k) - if _is_sensitive_key(str(k)): - findings.append({"label": "sensitive_key", "path": p}) - out[k] = "***" - else: - out[k] = _walk(v, p) - return out - if isinstance(node, list): - return [_walk(item, f"{path}[{i}]") for i, item in enumerate(node)] - if isinstance(node, str): - secrets = detect_secret_patterns(node) - if secrets: - for s in secrets: - findings.append({"label": s.label, "path": path}) - return redact_secrets(node) - return node - return node - - redacted = _walk(payload, "") - return { - "has_secrets": bool(findings), - "findings": findings, - "redacted": redacted, - } + return _walk(data) diff --git a/dealix/auto_client_acquisition/security_curator/tool_output_sanitizer.py b/dealix/auto_client_acquisition/security_curator/tool_output_sanitizer.py index 4b48a2e0..f1e17037 100644 --- a/dealix/auto_client_acquisition/security_curator/tool_output_sanitizer.py +++ b/dealix/auto_client_acquisition/security_curator/tool_output_sanitizer.py @@ -1,68 +1,15 @@ -"""Sanitize tool/agent outputs before they reach the user, ledger, or Proof Pack.""" +"""Sanitize agent/tool outputs before logging or returning to clients.""" from __future__ import annotations from typing import Any -from .secret_redactor import scan_payload -from .trace_redactor import redact_trace +from auto_client_acquisition.security_curator.secret_redactor import redact_secrets, sanitize_for_trace -def sanitize_tool_output(output: Any, *, mask_pii: bool = True) -> dict[str, Any]: - """ - Sanitize a tool's output before showing it to a human or persisting it. - - Returns: - { - "safe": bool (True iff no secrets and no payload PII at risk), - "redacted": , - "notes_ar": list[str] of human-readable notes, - } - """ - notes: list[str] = [] - secret_scan = scan_payload(output) - redacted = secret_scan["redacted"] - - if secret_scan["has_secrets"]: - labels = sorted({f["label"] for f in secret_scan["findings"]}) - notes.append(f"تمت إزالة قيم حساسة من المخرج: {', '.join(labels)}") - - if mask_pii: - trace_scan = redact_trace(redacted, mask_pii=True) - redacted = trace_scan["redacted"] - if trace_scan["had_pii"]: - notes.append("تم إخفاء أرقام/إيميلات في المخرج لأغراض الخصوصية.") - - safe = not secret_scan["has_secrets"] - return {"safe": safe, "redacted": redacted, "notes_ar": notes} - - -def sanitize_trace_event(event: dict[str, Any]) -> dict[str, Any]: - """ - Sanitize a single trace event for Langfuse/Sentry. - - Always preserves: event_type, agent_name, status, latency_ms, cost_estimate. - Always masks: payload, output, input. - """ - safe_keys = { - "event_type", "agent_name", "status", "latency_ms", - "cost_estimate", "approval_status", "tool", "policy_result", - "risk_level", "user_id_hash", "company_id_hash", - "workflow_name", "trace_id", "span_id", "ts", - } - risky_keys = {"payload", "output", "input", "context", "raw"} - - out: dict[str, Any] = {} - for k, v in event.items(): - if k in safe_keys: - out[k] = v - elif k in risky_keys: - scan = redact_trace(v, mask_pii=True) - out[k] = scan["redacted"] - if scan["had_secrets"] or scan["had_pii"]: - out.setdefault("_sanitized", []).append(k) - else: - # Unknown keys default to redaction, just in case. - scan = redact_trace(v, mask_pii=True) - out[k] = scan["redacted"] - return out +def sanitize_tool_output(obj: Any) -> Any: + if isinstance(obj, str): + return redact_secrets(obj) + if isinstance(obj, dict): + return sanitize_for_trace(obj) + return obj diff --git a/dealix/auto_client_acquisition/security_curator/trace_redactor.py b/dealix/auto_client_acquisition/security_curator/trace_redactor.py index 020c37f0..2c2c0628 100644 --- a/dealix/auto_client_acquisition/security_curator/trace_redactor.py +++ b/dealix/auto_client_acquisition/security_curator/trace_redactor.py @@ -1,76 +1,17 @@ -"""Trace Redactor — strip secrets/PII from traces before sending to Langfuse/Sentry.""" +"""Redact nested structures before sending traces to external observability.""" from __future__ import annotations -import re from typing import Any -from .secret_redactor import scan_payload - -# Phone-number-ish patterns we'll mask. Saudi: +966 5xxxxxxxx; international. -_PHONE_RE = re.compile(r"\+?\d[\d\s\-]{7,}\d") -# Generic email. -_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}") +from auto_client_acquisition.security_curator.secret_redactor import sanitize_for_trace -def _mask_phone(s: str) -> str: - def _mask(m: re.Match[str]) -> str: - raw = m.group(0) - digits_only = re.sub(r"\D", "", raw) - if len(digits_only) < 7: - return raw - return digits_only[:3] + "*" * (len(digits_only) - 6) + digits_only[-3:] - return _PHONE_RE.sub(_mask, s) +def redact_trace_payload(payload: dict[str, Any]) -> dict[str, Any]: + """Deep-redact string leaves; safe for Langfuse/OpenAI-style span metadata.""" + return sanitize_for_trace(payload) -def _mask_email(s: str) -> str: - def _mask(m: re.Match[str]) -> str: - local, _, domain = m.group(0).partition("@") - if not local or not domain: - return m.group(0) - keep = local[0] if local else "" - return f"{keep}***@{domain}" - return _EMAIL_RE.sub(_mask, s) - - -def redact_trace(payload: Any, *, mask_pii: bool = True) -> dict[str, Any]: - """ - Redact a trace payload for safe storage in observability tools. - - - Always strips secret patterns + sensitive keys (api_key/token/etc.). - - When mask_pii=True (default), also masks phone numbers and emails inside - string values. - - Returns: - { - "had_secrets": bool, - "had_pii": bool, - "redacted": , - } - """ - secret_scan = scan_payload(payload) - redacted = secret_scan["redacted"] - had_pii = False - - if mask_pii: - had_pii_box: list[bool] = [False] - - def _walk(node: Any) -> Any: - if isinstance(node, dict): - return {k: _walk(v) for k, v in node.items()} - if isinstance(node, list): - return [_walk(item) for item in node] - if isinstance(node, str): - if _PHONE_RE.search(node) or _EMAIL_RE.search(node): - had_pii_box[0] = True - return _mask_email(_mask_phone(node)) - return node - - redacted = _walk(redacted) - had_pii = had_pii_box[0] - - return { - "had_secrets": secret_scan["has_secrets"], - "had_pii": had_pii, - "redacted": redacted, - } +def redact_span_metadata(metadata: dict[str, Any] | None) -> dict[str, Any]: + """Alias for observability adapters.""" + return redact_trace_payload(metadata or {}) diff --git a/dealix/auto_client_acquisition/service_excellence/__init__.py b/dealix/auto_client_acquisition/service_excellence/__init__.py index ca90f12b..22dc1281 100644 --- a/dealix/auto_client_acquisition/service_excellence/__init__.py +++ b/dealix/auto_client_acquisition/service_excellence/__init__.py @@ -1,85 +1,5 @@ -"""Service Excellence OS — يضمن أن كل خدمة هي الأفضل قبل الإطلاق. +"""Service Excellence OS — scoring, feature matrix, workflows, launch readiness.""" -Feature matrix + scoring + workflow validation + competitor gap + -proof metrics + quality review + improvement backlog + launch package. -""" +from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score -from __future__ import annotations - -from .competitor_gap import compare_against_categories -from .feature_matrix import ( - build_feature_matrix, - classify_features, - prioritize_features, - recommend_missing_features, -) -from .launch_package import ( - build_demo_script, - build_landing_page_outline, - build_onboarding_checklist, - build_sales_script, - build_service_launch_package, -) -from .proof_metrics import ( - build_proof_pack_template_excellence, - calculate_service_roi_estimate, - required_proof_metrics, - summarize_proof_ar, -) -from .quality_review import ( - block_if_missing_approval_policy, - block_if_missing_proof, - block_if_unclear_pricing, - block_if_unsafe_channel, - review_service_before_launch, -) -from .research_lab import ( - build_monthly_service_review, - build_service_research_brief, - generate_feature_hypotheses, - recommend_next_experiments, -) -from .service_improvement_backlog import ( - build_backlog, - convert_feedback_to_backlog, - prioritize_backlog_items, - recommend_weekly_improvements, -) -from .service_scoring import ( - calculate_service_excellence_score, - score_automation, - score_clarity, - score_compliance, - score_proof, - score_speed_to_value, - score_upsell, -) - -__all__ = [ - # competitor_gap - "compare_against_categories", - # feature_matrix - "build_feature_matrix", "classify_features", - "prioritize_features", "recommend_missing_features", - # launch_package - "build_demo_script", "build_landing_page_outline", - "build_onboarding_checklist", "build_sales_script", - "build_service_launch_package", - # proof_metrics - "build_proof_pack_template_excellence", "calculate_service_roi_estimate", - "required_proof_metrics", "summarize_proof_ar", - # quality_review - "block_if_missing_approval_policy", "block_if_missing_proof", - "block_if_unclear_pricing", "block_if_unsafe_channel", - "review_service_before_launch", - # research_lab - "build_monthly_service_review", "build_service_research_brief", - "generate_feature_hypotheses", "recommend_next_experiments", - # service_improvement_backlog - "build_backlog", "convert_feedback_to_backlog", - "prioritize_backlog_items", "recommend_weekly_improvements", - # service_scoring - "calculate_service_excellence_score", "score_automation", - "score_clarity", "score_compliance", "score_proof", - "score_speed_to_value", "score_upsell", -] +__all__ = ["calculate_service_excellence_score"] diff --git a/dealix/auto_client_acquisition/service_excellence/competitor_gap.py b/dealix/auto_client_acquisition/service_excellence/competitor_gap.py index 9e6dfb80..a93dcd4b 100644 --- a/dealix/auto_client_acquisition/service_excellence/competitor_gap.py +++ b/dealix/auto_client_acquisition/service_excellence/competitor_gap.py @@ -1,79 +1,22 @@ -"""Competitor gap analysis — لا scraping، فقط مقارنة structural بفئات معروفة.""" +"""Static competitor gap framing — do_not_copy list.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service - -# Categories Dealix competes against. Strengths/limits are public knowledge. -COMPETITOR_CATEGORIES: dict[str, dict[str, list[str]]] = { - "crm": { - "strengths": ["تخزين بيانات", "pipeline tracking", "تكاملات واسعة"], - "limits": ["ينتظر إدخال يدوي", "لا يقرر ما تفعل اليوم", - "غير مصمم للسوق العربي"], - }, - "whatsapp_tools": { - "strengths": ["إرسال جماعي", "templates", "broadcast"], - "limits": ["لا approval-first", "لا proof", "خطر PDPL"], - }, - "email_assistant": { - "strengths": ["كتابة أسرع", "تكامل Gmail/Outlook"], - "limits": ["لا يحول الإيميل لـ pipeline", "لا proof", "عام غير عربي"], - }, - "linkedin_tools": { - "strengths": ["إيجاد leads"], - "limits": ["كثير منها يخالف ToS", "auto-DM يوقف الحسابات", - "لا يحترم PDPL"], - }, - "agency": { - "strengths": ["خبرة بشرية", "علاقات سوق"], - "limits": ["لا تتوسع", "غير قابلة للتكرار", "تعتمد على الفريق"], - }, - "revenue_intelligence": { - "strengths": ["تحليل المكالمات", "deal scoring"], - "limits": ["تبدأ بعد الـcall", "لا يصنع pipeline من الصفر"], - }, - "generic_ai_agent": { - "strengths": ["مرن", "يكتب أي شيء"], - "limits": ["بدون سياق شركة", "بدون proof", "بدون امتثال محلي"], - }, -} - def compare_against_categories(service_id: str) -> dict[str, Any]: - """Compare a Dealix service against generic competitor categories.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - dealix_advantages = [ - "موجّه للسوق السعودي بالعربية الطبيعية.", - "Approval-first — لا يضرّ سمعة العميل.", - "Proof Pack شهري قابل للقياس.", - "Multi-channel orchestration بـ سياسة موحدة.", - "Self-improving Curator يحسّن الرسائل أسبوعياً.", - "PDPL-aware من اليوم الأول.", - ] - - gaps_to_close: list[str] = [] - if "growth_os" not in service_id: - gaps_to_close.append("Daily autopilot كامل (متاح في Growth OS).") - if service_id == "free_growth_diagnostic": - gaps_to_close.append("Proof Pack حقيقي بعد 30 يوم.") - - do_not_copy = [ - "auto-DM على LinkedIn (مخالف).", - "scraping ضد ToS.", - "وعود بنتائج مضمونة.", - "مفاتيح API غير محمية في الواجهة.", - ] - return { "service_id": service_id, - "service_name_ar": s.name_ar, - "competitor_categories": COMPETITOR_CATEGORIES, - "dealix_advantages_ar": dealix_advantages, - "gaps_to_close_ar": gaps_to_close, - "do_not_copy_ar": do_not_copy, + "competitor_strengths_ar": [ + "أدوات CRM: بيانات غنية لكن بدون قرار يومي.", + "أدوات واتساب: إرسال سريع لكن بدون سياسة.", + ], + "dealix_advantages_ar": [ + "كروت قرار عربية + موافقة + Proof.", + "تعدد قنوات مع بوابة أمان.", + ], + "gaps_to_close_ar": ["تكاملات OAuth حقيقية", "تتبع تكلفة LLM"], + "do_not_copy": ["spam_automation", "scraping_linkedin", "cold_whatsapp_blast"], + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/feature_matrix.py b/dealix/auto_client_acquisition/service_excellence/feature_matrix.py index c745e144..80c8f772 100644 --- a/dealix/auto_client_acquisition/service_excellence/feature_matrix.py +++ b/dealix/auto_client_acquisition/service_excellence/feature_matrix.py @@ -1,120 +1,109 @@ -"""Feature matrix per service — must_have / advanced / premium / future.""" +"""Per-service feature tiers — deterministic.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -# 12 must-have features every premium Dealix service should ship with. -DEFAULT_MUST_HAVE: tuple[dict[str, object], ...] = ( - {"name_ar": "Self-Serve Intake", "value_ar": "العميل يبدأ بدون مكالمة.", - "complexity": 2, "risk": 1, "proof_metric": "intake_completion_rate"}, - {"name_ar": "AI Recommendation", - "value_ar": "النظام يوصي بالخدمة المناسبة من إجابات بسيطة.", - "complexity": 3, "risk": 2, "proof_metric": "wizard_acceptance_rate"}, - {"name_ar": "Data Quality Check", - "value_ar": "لا يستخدم بيانات سيئة.", - "complexity": 3, "risk": 4, "proof_metric": "data_quality_score"}, - {"name_ar": "Contactability / Risk Gate", - "value_ar": "يمنع التواصل الخطر تلقائياً.", - "complexity": 4, "risk": 8, "proof_metric": "risks_blocked"}, - {"name_ar": "Channel Strategy", - "value_ar": "يختار القناة الأفضل لكل contact.", - "complexity": 4, "risk": 5, "proof_metric": "channel_success_rate"}, - {"name_ar": "Arabic Contextual Drafting", - "value_ar": "رسائل سعودية، ليست ترجمة.", - "complexity": 5, "risk": 3, "proof_metric": "saudi_tone_score"}, - {"name_ar": "Approval Cards", - "value_ar": "CEO/Growth Manager يوافق من واتساب.", - "complexity": 3, "risk": 2, "proof_metric": "approval_rate"}, - {"name_ar": "Execution Mode", - "value_ar": "draft/export/approved فقط — لا live بدون env flag.", - "complexity": 3, "risk": 9, "proof_metric": "live_send_violations"}, - {"name_ar": "Proof Pack", - "value_ar": "تقرير قيمة محسوب.", - "complexity": 4, "risk": 1, "proof_metric": "proof_pack_delivered"}, - {"name_ar": "Learning Loop", - "value_ar": "يتعلم من Accept/Skip/Edit.", - "complexity": 5, "risk": 2, "proof_metric": "accept_rate_30d"}, - {"name_ar": "Upsell Path", - "value_ar": "يقود للخدمة الأعلى.", - "complexity": 2, "risk": 1, "proof_metric": "upsell_conversion_rate"}, - {"name_ar": "Service Score", - "value_ar": "يقيس نجاح الخدمة نفسها.", - "complexity": 3, "risk": 1, "proof_metric": "service_excellence_score"}, -) - -# Service-specific premium features. -_PREMIUM_BY_SERVICE: dict[str, list[dict[str, object]]] = { - "growth_os_monthly": [ - {"name_ar": "Daily Autopilot", "value_ar": "تشغيل ذاتي يومي.", - "complexity": 6, "risk": 4, "proof_metric": "daily_decisions_made"}, - {"name_ar": "Revenue Leak Detector", - "value_ar": "كشف التسريبات تلقائياً.", - "complexity": 5, "risk": 2, "proof_metric": "leaks_detected"}, - {"name_ar": "Founder Shadow Board", - "value_ar": "موجز أسبوعي مركّب.", - "complexity": 4, "risk": 1, "proof_metric": "weekly_briefs_delivered"}, - ], - "agency_partner_program": [ - {"name_ar": "Co-Branded Proof Pack", "value_ar": "Proof بعلامة الوكالة.", - "complexity": 4, "risk": 2, "proof_metric": "co_branded_proofs"}, - {"name_ar": "Revenue Share Dashboard", - "value_ar": "لوحة مشاركة الإيرادات.", - "complexity": 5, "risk": 3, "proof_metric": "agency_revenue_sar"}, - ], -} +_BASE_FEATURES: list[dict[str, Any]] = [ + { + "id": "intake_self_serve", + "name_ar": "استقبال ذاتي", + "tier": "must_have", + "value_ar": "يبدأ العميل بدون احتكاك.", + "complexity": "low", + "risk": "low", + "proof_metric": "completion_rate", + "launch_priority": 1, + }, + { + "id": "contactability_gate", + "name_ar": "بوابة contactability", + "tier": "must_have", + "value_ar": "يمنع التواصل الخطر.", + "complexity": "medium", + "risk": "low", + "proof_metric": "blocked_risk_count", + "launch_priority": 1, + }, + { + "id": "approval_cards", + "name_ar": "كروت موافقة", + "tier": "must_have", + "value_ar": "لا إرسال خارجي بدون قرار بشري.", + "complexity": "low", + "risk": "low", + "proof_metric": "approval_rate", + "launch_priority": 1, + }, + { + "id": "proof_pack", + "name_ar": "Proof Pack", + "tier": "must_have", + "value_ar": "يثبت العائد أسبوعياً.", + "complexity": "medium", + "risk": "low", + "proof_metric": "revenue_influenced_sar", + "launch_priority": 2, + }, + { + "id": "channel_mix", + "name_ar": "مزج قنوات آمن", + "tier": "advanced", + "value_ar": "إيميل أولاً، واتساب بموافقة/opt-in.", + "complexity": "high", + "risk": "medium", + "proof_metric": "meetings_booked", + "launch_priority": 3, + }, + { + "id": "research_lab_hook", + "name_ar": "ربط مختبر تحسين", + "tier": "premium", + "value_ar": "backlog تحسين أسبوعي.", + "complexity": "medium", + "risk": "low", + "proof_metric": "experiment_win_rate", + "launch_priority": 4, + }, +] def build_feature_matrix(service_id: str) -> dict[str, Any]: - """Build the full feature matrix for a service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - must_have = [dict(f) for f in DEFAULT_MUST_HAVE] - premium = list(_PREMIUM_BY_SERVICE.get(service_id, [])) - return { - "service_id": service_id, - "service_name_ar": s.name_ar, - "must_have": must_have, - "advanced": premium, - "premium": premium, - "future": [], - "total_features": len(must_have) + len(premium), - } + svc = get_service_by_id(service_id) + feats = list(_BASE_FEATURES) + if svc and svc.get("risk_level") == "high": + feats.append( + { + "id": "extra_compliance_review", + "name_ar": "مراجعة امتثال إضافية", + "tier": "must_have", + "value_ar": "خدمة عالية المخاطر.", + "complexity": "medium", + "risk": "high", + "proof_metric": "compliance_checks", + "launch_priority": 1, + } + ) + return {"service_id": service_id, "features": feats, "demo": True} def classify_features(service_id: str) -> dict[str, list[str]]: - """Classify a service's features into tiers.""" - matrix = build_feature_matrix(service_id) - if "error" in matrix: - return {} - return { - "must_have": [str(f["name_ar"]) for f in matrix["must_have"]], - "advanced": [str(f["name_ar"]) for f in matrix["advanced"]], - "premium": [str(f["name_ar"]) for f in matrix["premium"]], - } + fm = build_feature_matrix(service_id) + buckets: dict[str, list[str]] = {"must_have": [], "advanced": [], "premium": [], "future": []} + for f in fm.get("features") or []: + tier = str(f.get("tier") or "must_have") + if tier not in buckets: + tier = "must_have" + buckets[tier].append(str(f.get("id"))) + return buckets -def recommend_missing_features(service_id: str) -> list[dict[str, Any]]: - """Recommend features the service may be missing.""" - matrix = build_feature_matrix(service_id) - if "error" in matrix: - return [] - # If the service has fewer than 12 must-haves, suggest the rest. - if len(matrix["must_have"]) >= 12: - return [] - return [{"name_ar": "Add to advanced tier", - "rationale_ar": "خدمة قوية تستفيد من ميزات advanced."}] +def recommend_missing_features(service_id: str) -> list[str]: + """Stub: suggest future items.""" + return ["connector_webhooks", "durable_workflows"] if service_id == "growth_os" else [] def prioritize_features(features: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Order features by (lower complexity, lower risk, higher impact).""" - return sorted( - features, - key=lambda f: ( - int(f.get("complexity", 9)), - int(f.get("risk", 9)), - ), - ) + return sorted(features or [], key=lambda f: int(f.get("launch_priority", 99))) diff --git a/dealix/auto_client_acquisition/service_excellence/launch_package.py b/dealix/auto_client_acquisition/service_excellence/launch_package.py index 3d728fb5..b8c6adc5 100644 --- a/dealix/auto_client_acquisition/service_excellence/launch_package.py +++ b/dealix/auto_client_acquisition/service_excellence/launch_package.py @@ -1,125 +1,46 @@ -"""Launch package — لكل خدمة: landing page outline + sales script + demo + onboarding.""" +"""Sales/demo/onboarding outlines per service.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -def build_landing_page_outline(service_id: str) -> dict[str, Any]: - """Outline of a landing page for the service (Arabic, RTL).""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} +def build_service_launch_package(service_id: str) -> dict[str, Any]: + svc = get_service_by_id(service_id) or {} return { "service_id": service_id, - "title_ar": s.name_ar, - "sections_ar": [ - "Hero: العرض في جملة + CTA", - "وعد المنتج: ماذا سيحصل العميل عليه؟", - "كيف تعمل الخدمة (3 خطوات)", - "Deliverables — قائمة بالمخرجات", - "Pricing — السعر بوضوح", - "Proof — ما الذي نقيسه", - "Safety — لا live send، Approval-first", - "Trust — للوكالات / B2B سعودي", - "FAQ", - "CTA النهائي", - ], - "cta_ar": "ابدأ الآن" if s.pricing_max_sar > 0 else "احجز التشخيص المجاني", - "must_include_ar": [ - "Approval-first.", - "لا cold WhatsApp.", - "PDPL-aware.", - "لا وعود بنتائج مضمونة.", - ], + "name_ar": svc.get("name_ar"), + "landing_outline_ar": ["الوعد", "لمن؟", "ماذا تحصل؟", "CTA", "تحذير: لا نتائج مضمونة"], + "demo_script_ar": ["افتح التشخيص", "اعرض الكروت", "أظهر الموافقة", "أغلق بـ Proof"], + "onboarding_checklist_ar": ["جمع المدخلات", "تشغيل wizard", "تفعيل مسودات فقط"], + "demo": True, } +def build_landing_page_outline(service_id: str) -> dict[str, Any]: + return {"service_id": service_id, "sections": build_service_launch_package(service_id).get("landing_outline_ar"), "demo": True} + + def build_sales_script(service_id: str) -> dict[str, Any]: - """Sales script (Arabic) — discovery → pitch → close.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} return { "service_id": service_id, - "discovery_questions_ar": [ - "وش أكبر تحدي نمو لديكم اليوم؟", - "كيف تستهدفون اليوم؟ ما الذي يعمل؟", - "ما الذي يأخذ وقتاً يومياً ولا يثبت قيمة؟", - "هل عندكم قائمة عملاء قدامى لم تتم متابعتهم؟", - "من يوافق على الرسائل قبل الإرسال؟", - ], - "pitch_ar": ( - f"بناءً على ما شاركته، {s.name_ar} مناسبة لكم. " - f"خلال {('7 أيام' if s.pricing_model == 'sprint' else 'الشهر الأول')}، " - f"سنطلع لكم: {', '.join(s.deliverables_ar)}." - ), - "objection_handling_ar": { - "price": "نقدم Free Diagnostic أولاً — تشوفون النتائج قبل الدفع.", - "timing": "Pilot 7 أيام لا يحتاج التزام طويل — جرّبوه ثم قرروا.", - "trust": "Approval-first: لا نرسل أي شيء بدون موافقتكم.", - "complexity": "نتولى الإعداد كاملاً في 3 أيام عمل.", - }, - "close_ar": ( - "إذا الفكرة منطقية، أحدد لكم Pilot يبدأ يوم الأحد. " - "أرسل لي تأكيد + اسم منسّق Approvals." + "script_ar": ( + f"نقدّم {service_id}: نعمل مسودات وموافقات، " + "ولا نرسل أو نخصم دون قرارك. نثبت القيمة عبر Proof Pack." ), + "demo": True, } def build_demo_script(service_id: str) -> dict[str, Any]: - """12-minute Arabic demo script.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - return { - "service_id": service_id, - "duration_minutes": 12, - "minute_by_minute_ar": [ - "0–2: الفكرة الكبرى — Dealix ليس CRM ولا أداة واتساب.", - f"2–4: عرض {s.name_ar} — Daily Brief / Command Feed.", - "4–6: مثال حي — 10 فرص في 10 دقائق.", - "6–8: Trust Score + Simulator + Proof Pack.", - "8–10: الأمان والتكاملات (security + connectors).", - "10–12: العرض والـ CTA.", - ], - "do_not_do_ar": [ - "لا تكشف API keys على الشاشة.", - "لا تشغّل live WhatsApp في الـdemo.", - "لا تعد بأرقام لم تُحقَّق.", - ], - } + return build_sales_script(service_id) def build_onboarding_checklist(service_id: str) -> dict[str, Any]: - """Onboarding checklist for the customer (first 5 days).""" - 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, - "first_5_days_ar": [ - "يوم 1: kick-off + جمع الـ intake + توقيع DPA draft.", - "يوم 2: ربط القنوات الآمنة (Gmail drafts / Sheets / website forms).", - "يوم 3: توليد أول Proof Pack template + تدريب على Approval Center.", - "يوم 4: إطلاق أول mission (10 فرص في 10 دقائق).", - "يوم 5: مراجعة النتائج + تخطيط الأسبوع الثاني.", - ], - "approval_required": True, - "live_send_allowed": False, - } - - -def build_service_launch_package(service_id: str) -> dict[str, Any]: - """Full launch package = landing + sales + demo + onboarding.""" - return { - "service_id": service_id, - "landing": build_landing_page_outline(service_id), - "sales_script": build_sales_script(service_id), - "demo_script": build_demo_script(service_id), - "onboarding": build_onboarding_checklist(service_id), - "approval_required": True, + "checklist_ar": build_service_launch_package(service_id).get("onboarding_checklist_ar"), + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/proof_metrics.py b/dealix/auto_client_acquisition/service_excellence/proof_metrics.py index d1aa459b..c6eaa4a9 100644 --- a/dealix/auto_client_acquisition/service_excellence/proof_metrics.py +++ b/dealix/auto_client_acquisition/service_excellence/proof_metrics.py @@ -1,72 +1,38 @@ -"""Proof metrics — كل خدمة لازم تثبت العائد بأرقام محددة.""" +"""Required proof metrics and ROI estimate stubs.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id def required_proof_metrics(service_id: str) -> list[str]: - """Return the proof metrics every run of the service must produce.""" - s = get_service(service_id) - if s is None: - return [] - return list(s.proof_metrics) + svc = get_service_by_id(service_id) or {} + return list(svc.get("proof_metrics") or ["drafts_created", "approvals"]) -def build_proof_pack_template_excellence(service_id: str) -> dict[str, Any]: - """Build a polished Proof Pack template for an excellence-tier service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} +def build_proof_pack_template(service_id: str) -> dict[str, Any]: return { "service_id": service_id, - "service_name_ar": s.name_ar, - "executive_summary_ar": ( - "ملخص تنفيذي من 10 أسطر يعرض النتائج، الأثر المالي، " - "والمخاطر التي تم منعها." - ), - "metrics": list(s.proof_metrics), - "report_format": ["pdf", "json", "whatsapp_summary"], - "signature_required": True, - "approval_required": True, + "metrics": required_proof_metrics(service_id), + "template_ar": "ملخص + قرارات + مخاطر منعت + أثر مقدّر", + "demo": True, } -def calculate_service_roi_estimate( - service_id: str, - metrics: dict[str, Any], -) -> dict[str, Any]: - """Estimate ROI = pipeline_influenced / service_price.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - price = max(1, float(metrics.get("price_paid_sar", s.pricing_min_sar or 1))) - pipeline = float(metrics.get("pipeline_sar", 0)) - closed_won = float(metrics.get("closed_won_sar", 0)) - - roi_pipeline_x = round(pipeline / price, 2) - roi_closed_x = round(closed_won / price, 2) - +def calculate_service_roi_estimate(service_id: str, metrics: dict[str, Any]) -> dict[str, Any]: + influenced = int(metrics.get("revenue_influenced_sar", 0)) + if influenced <= 0: + influenced = int(metrics.get("pipeline_sar", 12000)) return { "service_id": service_id, - "price_paid_sar": price, - "pipeline_sar": pipeline, - "closed_won_sar": closed_won, - "roi_pipeline_x": roi_pipeline_x, - "roi_closed_x": roi_closed_x, - "summary_ar": ( - f"كل ريال أنفقه العميل على {s.name_ar} أنتج " - f"{roi_pipeline_x}× pipeline و {roi_closed_x}× closed-won." - ), + "revenue_influenced_sar_estimate": influenced, + "note_ar": "تقدير عرضي — ليس وعداً.", + "demo": True, } def summarize_proof_ar(service_id: str, metrics: dict[str, Any]) -> str: - """Build a one-paragraph Arabic proof summary.""" - roi = calculate_service_roi_estimate(service_id, metrics) - if "error" in roi: - return roi["error"] - return roi["summary_ar"] + r = calculate_service_roi_estimate(service_id, metrics) + return f"خدمة {service_id}: أثر مقدّر {r.get('revenue_influenced_sar_estimate')} ريال (عرضي)." diff --git a/dealix/auto_client_acquisition/service_excellence/quality_review.py b/dealix/auto_client_acquisition/service_excellence/quality_review.py index 7de12194..70d3c841 100644 --- a/dealix/auto_client_acquisition/service_excellence/quality_review.py +++ b/dealix/auto_client_acquisition/service_excellence/quality_review.py @@ -1,82 +1,56 @@ -"""Quality review — يمنع الخدمات الضعيفة من الإطلاق.""" +"""Launch gate checks for services.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service - -from .service_scoring import calculate_service_excellence_score - - -def block_if_missing_proof(service_id: str) -> dict[str, Any]: - s = get_service(service_id) - if s is None: - return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} - if not s.proof_metrics: - return {"blocked": True, "reason_ar": "لا توجد proof metrics."} - return {"blocked": False} - - -def block_if_missing_approval_policy(service_id: str) -> dict[str, Any]: - s = get_service(service_id) - if s is None: - return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} - if not s.approval_policy: - return {"blocked": True, "reason_ar": "سياسة الاعتماد غير محددة."} - return {"blocked": False} - - -def block_if_unclear_pricing(service_id: str) -> dict[str, Any]: - s = get_service(service_id) - if s is None: - return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} - if s.pricing_max_sar < 0: - return {"blocked": True, "reason_ar": "تسعير غير صحيح."} - if s.pricing_max_sar > 0 and s.pricing_max_sar < s.pricing_min_sar: - return {"blocked": True, "reason_ar": "نطاق التسعير غير منطقي."} - return {"blocked": False} - - -def block_if_unsafe_channel(service_id: str) -> dict[str, Any]: - """Block if a service depends on an unsafe channel (e.g., scraping).""" - s = get_service(service_id) - if s is None: - return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} - unsafe = {"scraping", "auto_dm", "auto_connect", "browser_extension"} - for ch in s.required_integrations: - if ch.lower() in unsafe: - return {"blocked": True, - "reason_ar": f"تكامل غير آمن: {ch}."} - return {"blocked": False} +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id def review_service_before_launch(service_id: str) -> dict[str, Any]: - """Run all gates + scoring before allowing a service to ship.""" - gates = { - "proof": block_if_missing_proof(service_id), - "approval": block_if_missing_approval_policy(service_id), - "pricing": block_if_unclear_pricing(service_id), - "channels": block_if_unsafe_channel(service_id), - } - blocked = [k for k, v in gates.items() if v.get("blocked")] - score = calculate_service_excellence_score(service_id) + svc = get_service_by_id(service_id) or {} + issues: list[str] = [] + if not svc.get("pricing_range_sar"): + issues.append("missing_pricing") + if not svc.get("approval_policy"): + issues.append("missing_approval_policy") + if not svc.get("proof_metrics"): + issues.append("missing_proof_metrics") + ok = len(issues) == 0 + return {"service_id": service_id, "ok": ok, "issues": issues, "demo": True} - if blocked: - verdict = "blocked_at_gate" - elif score.get("status") == "launch_ready": - verdict = "launch_ready" - elif score.get("status") == "beta_only": - verdict = "beta_only" - else: - verdict = "needs_work" +def block_if_missing_proof(service_id: str) -> bool: + return not (get_service_by_id(service_id) or {}).get("proof_metrics") + + +def block_if_missing_approval_policy(service_id: str) -> bool: + return not (get_service_by_id(service_id) or {}).get("approval_policy") + + +def block_if_unclear_pricing(service_id: str) -> bool: + if service_id == "free_growth_diagnostic": + return False + pr = (get_service_by_id(service_id) or {}).get("pricing_range_sar") or {} + return pr.get("max") in (None, 0) + + +def block_if_unsafe_channel(service_id: str) -> bool: + """Block launch if policy suggests unguarded external send.""" + pol = ((get_service_by_id(service_id) or {}).get("approval_policy") or "").lower() + return pol in ("", "none", "auto_send") + + +def review_all_services() -> dict[str, Any]: + from auto_client_acquisition.service_tower.service_catalog import list_service_ids + + results: list[dict[str, Any]] = [] + for sid in list_service_ids(): + results.append(review_service_before_launch(sid)) + ok_count = sum(1 for r in results if r.get("ok")) return { - "service_id": service_id, - "verdict": verdict, - "score": score, - "gates": gates, - "blocked_reasons_ar": [ - gates[k].get("reason_ar", "") for k in blocked - ], + "count": len(results), + "ok_count": ok_count, + "results": results, + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/research_lab.py b/dealix/auto_client_acquisition/service_excellence/research_lab.py index 8754f482..c8cf2d9b 100644 --- a/dealix/auto_client_acquisition/service_excellence/research_lab.py +++ b/dealix/auto_client_acquisition/service_excellence/research_lab.py @@ -1,109 +1,37 @@ -"""Service Research Lab — تحسين شهري لكل خدمة (deterministic).""" +"""Deterministic research brief — no web calls.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import get_service - -from .competitor_gap import compare_against_categories -from .service_scoring import calculate_service_excellence_score - def build_service_research_brief(service_id: str) -> dict[str, Any]: - """Research brief: questions to answer about a service this month.""" - 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, - "questions_to_answer_ar": [ - "من أكثر فئة عميل اشترت هذه الخدمة آخر 30 يوم؟", - "ما متوسط الـ time-to-value الفعلي؟", - "ما أعلى اعتراض ظهر في الـonboarding؟", - "ما أكثر deliverable يطلبه العميل بالاسم؟", - "ما أضعف proof_metric لم يُحقَّق هذا الشهر؟", - "ما أكثر سعر يقبله العميل بدون تردد؟", + "hypotheses_ar": [ + "تحسين رسالة الـ CTA يزيد الردود.", + "تقليل المتابعات يقلل الشكاوى.", ], - "data_sources_ar": [ - "Action Ledger.", - "Proof Ledger.", - "Approval Center.", - "Decision Memory.", - "Customer feedback.", - ], - "approval_required": True, + "experiments_ar": ["A/B لنبرة سعودية قصيرة", "تغيير ترتيب القنوات في الخطة"], + "demo": True, } -def generate_feature_hypotheses(service_id: str) -> list[dict[str, Any]]: - """Generate hypotheses for feature additions/improvements.""" - s = get_service(service_id) - if s is None: - return [] - base = [ - { - "hypothesis_ar": "إضافة exit survey بعد كل deliverable يرفع NPS بـ20%.", - "effort": "low", "impact": "medium", - }, - { - "hypothesis_ar": "اقتراح 3 رسائل بدل 1 في الـapproval card يرفع approval rate 30%.", - "effort": "medium", "impact": "high", - }, - { - "hypothesis_ar": "إضافة Saudi-tone-score مرئية في الواجهة يقلل الرسائل المرفوضة 40%.", - "effort": "medium", "impact": "high", - }, - { - "hypothesis_ar": "ربط Proof Pack بـ Moyasar invoice draft يرفع conversion 25%.", - "effort": "medium", "impact": "high", - }, - ] - if s.pricing_model == "monthly": - base.append({ - "hypothesis_ar": "تقرير شهري بصيغة فيديو 60 ثانية يرفع retention 15%.", - "effort": "high", "impact": "medium", - }) - return base +def generate_feature_hypotheses(service_id: str) -> list[str]: + return [f"{service_id}: إضافة checklist امتثال", f"{service_id}: تقرير مقارنة منافسين"] -def recommend_next_experiments(service_id: str) -> dict[str, Any]: - """Recommend the next 3 experiments to run on a service.""" - hypotheses = generate_feature_hypotheses(service_id) - # Pick top-3 by impact desc, effort asc. - impact_rank = {"high": 0, "medium": 1, "low": 2} - effort_rank = {"low": 0, "medium": 1, "high": 2} - sorted_h = sorted( - hypotheses, - key=lambda h: (impact_rank.get(str(h.get("impact")), 9), - effort_rank.get(str(h.get("effort")), 9)), - ) - return { - "service_id": service_id, - "experiments": sorted_h[:3], - "approval_required": True, - } +def recommend_next_experiments(service_id: str) -> list[str]: + return [f"{service_id}: تجربة سعر Pilot أعلى قليلاً", f"{service_id}: دمج Proof مع targeting"] def build_monthly_service_review(service_id: str) -> dict[str, Any]: - """Build a structured monthly review of a service's performance.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - score = calculate_service_excellence_score(service_id) - gaps = compare_against_categories(service_id) - experiments = recommend_next_experiments(service_id) - return { "service_id": service_id, - "service_name_ar": s.name_ar, - "current_excellence_score": score, - "competitor_gap_summary": { - "advantages": gaps.get("dealix_advantages_ar", []), - "gaps_to_close": gaps.get("gaps_to_close_ar", []), - }, - "next_experiments": experiments.get("experiments", []), - "research_brief": build_service_research_brief(service_id), - "approval_required": True, + "review_ar": [ + "ماذا تحسّن؟", + "ماذا أوقفنا؟", + "ما التجربة القادمة؟", + ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py b/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py index 89e71563..24bbb5a1 100644 --- a/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py +++ b/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py @@ -1,67 +1,40 @@ -"""Improvement backlog — يحوّل الفيدباك إلى bands prioritized.""" +"""Prioritized improvement backlog per service.""" from __future__ import annotations from typing import Any -def build_backlog(service_id: str) -> dict[str, Any]: - """Build an empty backlog skeleton for a service.""" - return { - "service_id": service_id, - "items": [], - "policies_ar": [ - "كل بند يتضمن: title_ar, impact, effort, owner.", - "بند بدون proof_metric يُرفض.", - "بند يخالف PDPL/ToS يُرفض فوراً.", - ], - } +def build_backlog(service_id: str) -> list[dict[str, Any]]: + return [ + {"id": f"{service_id}_tone_eval", "title_ar": "تقييم نبرة سعودية", "priority": 1}, + {"id": f"{service_id}_latency", "title_ar": "تقليل زمن توليد المسودات", "priority": 2}, + {"id": f"{service_id}_integrations", "title_ar": "OAuth محدود النطاق", "priority": 3}, + ] def prioritize_backlog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Sort backlog items by impact desc, effort asc.""" - impact_rank = {"high": 0, "medium": 1, "low": 2} - effort_rank = {"low": 0, "medium": 1, "high": 2} - return sorted( - items, - key=lambda i: ( - impact_rank.get(str(i.get("impact", "low")), 9), - effort_rank.get(str(i.get("effort", "high")), 9), - ), - ) + return sorted(items or [], key=lambda x: int(x.get("priority", 99))) -def convert_feedback_to_backlog( - feedback: list[dict[str, Any]], -) -> list[dict[str, Any]]: - """Convert customer feedback items into prioritized backlog items.""" - out: list[dict[str, Any]] = [] - for f in feedback or []: - text = str(f.get("text", "")).strip() - if not text: - continue - # Heuristic prioritization (deterministic). - sentiment = f.get("sentiment", "neutral") - impact = "high" if sentiment == "negative" else "medium" - effort = "medium" - out.append({ - "title_ar": text[:120], - "impact": impact, - "effort": effort, - "source": f.get("source", "feedback"), - "owner": f.get("owner", "service_lead"), - }) - return prioritize_backlog_items(out) +def convert_feedback_to_backlog(feedback: str) -> dict[str, Any]: + return { + "feedback": feedback, + "backlog_item": { + "id": "user_feedback_1", + "title_ar": "معالجة ملاحظة مستخدم", + "priority": 2, + }, + "demo": True, + } def recommend_weekly_improvements(service_id: str) -> dict[str, Any]: - """Recommend 3 weekly improvements for a service.""" return { "service_id": service_id, - "weekly_plan_ar": [ - "حسّن الرسالة الأولى — اختبر زاوية جديدة لقطاع واحد.", - "أضف proof_metric حقيقي لو يوجد فجوة.", - "نظّف backlog: ادمج أو احذف بنود متشابهة.", + "items_ar": [ + "راجع آخر ١٠ موافقات واختصر المسودات.", + "قارن proof metrics أسبوع بأسبوع.", ], - "approval_required": True, + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/service_scoring.py b/dealix/auto_client_acquisition/service_excellence/service_scoring.py index 98401c7a..4624a339 100644 --- a/dealix/auto_client_acquisition/service_excellence/service_scoring.py +++ b/dealix/auto_client_acquisition/service_excellence/service_scoring.py @@ -1,151 +1,75 @@ -"""Service Excellence scoring — every service must score ≥80 to ship.""" +"""Service Excellence Score 0–100 — launch readiness.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.service_tower import Service, get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -def score_clarity(service: Service | dict[str, Any]) -> int: - """0..10. هل العميل يفهم ما الذي سيحصل عليه؟""" - if isinstance(service, dict): - outcome = service.get("outcome_ar", "") - deliverables = service.get("deliverables_ar", []) - else: - outcome = service.outcome_ar - deliverables = list(service.deliverables_ar) - score = 5 - if len(outcome or "") >= 30: - score += 3 - if len(deliverables) >= 3: - score += 2 - return min(10, score) +def _clamp(n: int, lo: int = 0, hi: int = 10) -> int: + return max(lo, min(hi, n)) -def score_speed_to_value(service: Service | dict[str, Any]) -> int: - """0..10. هل النتيجة خلال 7 أيام؟""" - if isinstance(service, dict): - model = service.get("pricing_model", "") - else: - model = service.pricing_model - if model == "sprint": +def score_clarity(service: dict[str, Any]) -> int: + return 9 if service.get("outcome_ar") else 4 + + +def score_speed_to_value(service: dict[str, Any]) -> int: + steps = len(service.get("workflow_steps") or []) + return _clamp(10 - max(0, steps - 8)) + + +def score_automation(service: dict[str, Any]) -> int: + return 7 if len(service.get("required_integrations") or []) <= 2 else 6 + + +def score_compliance(service: dict[str, Any]) -> int: + pol = (service.get("approval_policy") or "").lower() + if "legal" in pol: return 10 - if model == "monthly": - return 6 - return 8 # one_time - - -def score_automation(service: Service | dict[str, Any]) -> int: - """0..10. هل قابلة للأتمتة؟""" - if isinstance(service, dict): - steps = service.get("workflow_steps", []) - else: - steps = list(service.workflow_steps) - auto_steps = sum(1 for s in steps - if s in {"intake", "data_check", "targeting", - "contactability", "strategy", "drafting", - "tracking", "proof", "upsell"}) - return min(10, auto_steps) - - -def score_compliance(service: Service | dict[str, Any]) -> int: - """0..10. هل فيها opt-in/approval/audit؟""" - if isinstance(service, dict): - policy = service.get("approval_policy", "") - else: - policy = service.approval_policy - if "approval_required" in policy: - return 10 - if "draft_only" in policy: + if "approval" in pol or "draft" in pol: return 9 - if policy: - return 6 - return 3 + return 6 -def score_proof(service: Service | dict[str, Any]) -> int: - """0..10. هل لها proof metrics؟""" - if isinstance(service, dict): - metrics = service.get("proof_metrics", []) - else: - metrics = list(service.proof_metrics) - return min(10, len(metrics) * 3) +def score_proof(service: dict[str, Any]) -> int: + return 8 if service.get("proof_metrics") else 4 -def score_upsell(service: Service | dict[str, Any]) -> int: - """0..10. هل لها upgrade path؟""" - if isinstance(service, dict): - upgrade = service.get("upgrade_path", []) - else: - upgrade = list(service.upgrade_path) - return 10 if upgrade else 5 +def score_upsell(service: dict[str, Any]) -> int: + return 8 if service.get("upgrade_path") else 5 -def calculate_service_excellence_score( - service: Service | dict[str, Any] | str, -) -> dict[str, Any]: - """Compute the full excellence score (0..100) + verdict.""" - if isinstance(service, str): - s = get_service(service) - if s is None: - return {"error": f"unknown service: {service}"} - service_obj: Service | dict[str, Any] = s - else: - service_obj = service - - clarity = score_clarity(service_obj) - speed = score_speed_to_value(service_obj) - automation = score_automation(service_obj) - compliance = score_compliance(service_obj) - proof = score_proof(service_obj) - upsell = score_upsell(service_obj) - - # Each dimension max=10; we have 6 dimensions → max=60. - # Add 4 baseline dimensions (uniqueness, scalability, ops, proof_data) - # at fixed values for now (can become real signals later). - uniqueness = 8 # deterministic — Dealix is Saudi-first - scalability = 8 # multi-sector ready - ops_daily = 7 # daily autopilot integration - proof_data = min(10, proof + 2) - - total = (clarity + speed + automation + compliance - + proof + upsell + uniqueness + scalability - + ops_daily + proof_data) - total = max(0, min(100, total)) - - if total >= 80: - status = "launch_ready" - elif total >= 60: - status = "beta_only" - else: - status = "needs_work" - - reasons: list[str] = [] - fixes: list[str] = [] - if compliance < 8: - reasons.append("سياسة الاعتماد غير واضحة.") - fixes.append("اضبط approval_policy على 'approval_required' أو 'draft_only'.") - if proof < 6: - reasons.append("Proof metrics قليلة.") - fixes.append("أضف ≥3 proof metrics محددة.") - if not upsell: - reasons.append("لا يوجد upgrade path.") - fixes.append("اربط الخدمة بخدمة أعلى عبر upgrade_path.") - - return { - "service_id": ( - service_obj.get("id") if isinstance(service_obj, dict) else service_obj.id - ), - "total_score": total, - "dimensions": { - "clarity": clarity, "speed_to_value": speed, - "automation": automation, "compliance": compliance, - "proof": proof, "upsell": upsell, - "uniqueness": uniqueness, "scalability": scalability, - "ops_daily": ops_daily, "proof_data": proof_data, - }, - "status": status, - "reasons_ar": reasons, - "required_fixes_ar": fixes, +def calculate_service_excellence_score(service_id: str) -> dict[str, Any]: + svc = get_service_by_id(service_id) or {} + dims = { + "clarity": score_clarity(svc), + "speed_to_value": score_speed_to_value(svc), + "automation": score_automation(svc), + "compliance": score_compliance(svc), + "proof": score_proof(svc), + "upsell": score_upsell(svc), + } + # Weighted sum → 0..100 scale (6 dims * ~10 max) + total = sum(dims.values()) * 100 // 60 + status = "launch_ready" + reasons_ar: list[str] = [] + if total < 80: + status = "beta_only" + reasons_ar.append("الدرجة أقل من ٨٠ — إطلاق محدود أو تحسين قبل الإعلان.") + if (svc.get("risk_level") or "") == "high" and total < 90: + status = "needs_work" + reasons_ar.append("مخاطر عالية: عزّز الامتثال والاختبارات.") + required_fixes: list[str] = [] + if not svc.get("proof_metrics"): + required_fixes.append("أضف proof_metrics واضحة.") + return { + "service_id": service_id, + "dimensions": dims, + "total_score": total, + "status": status, + "reasons_ar": reasons_ar or ["جاهزية جيدة للعرض الداخلي."], + "required_fixes": required_fixes, + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_excellence/workflow_builder.py b/dealix/auto_client_acquisition/service_excellence/workflow_builder.py new file mode 100644 index 00000000..61939fbb --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/workflow_builder.py @@ -0,0 +1,32 @@ +"""Standard workflow builder for services.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower.mission_templates import build_service_workflow + + +def build_workflow(service_id: str) -> dict[str, Any]: + return build_service_workflow(service_id) + + +def validate_workflow(service_id: str) -> dict[str, Any]: + wf = build_workflow(service_id) + steps = wf.get("steps") or [] + ok = "approve" in steps or "approval" in steps + return {"ok": ok, "steps": steps, "has_approval_step": ok, "demo": True} + + +def build_day_by_day_execution_plan(service_id: str) -> dict[str, Any]: + wf = build_workflow(service_id) + steps = list(wf.get("steps") or []) + plan: list[dict[str, Any]] = [] + for i, s in enumerate(steps[:14], start=1): + plan.append({"day": i, "step": s, "note_ar": f"اليوم {i}: {s}"}) + return {"service_id": service_id, "plan": plan, "demo": True} + + +def build_approval_steps(service_id: str) -> dict[str, Any]: + wf = build_workflow(service_id) + return {"service_id": service_id, "approval_steps": wf.get("approval_gates") or ["approve"], "demo": True} diff --git a/dealix/auto_client_acquisition/service_tower/__init__.py b/dealix/auto_client_acquisition/service_tower/__init__.py index eb210771..6d500a73 100644 --- a/dealix/auto_client_acquisition/service_tower/__init__.py +++ b/dealix/auto_client_acquisition/service_tower/__init__.py @@ -1,97 +1,19 @@ -"""Service Tower — كل قدرات Dealix كخدمات قابلة للبيع والتشغيل الذاتي. +"""Dealix Service Tower — productized services, wizard, pricing, CEO cards.""" -العميل يختار هدفه → النظام يوصي بالخدمة → يجمع البيانات → يقيّم المخاطر → -يكتب الخطة → يطلب الموافقات → يشغّل القنوات → يطلع Proof Pack. -""" - -from __future__ import annotations - -from .contract_templates import ( - draft_sla_outline, - list_contract_templates, +from auto_client_acquisition.service_tower.service_catalog import ( + get_service_by_id, + list_service_ids, + list_tower_services, ) -from .deliverables import ( - build_client_report_outline, - build_deliverables, - build_internal_operator_checklist, - build_proof_pack_template, -) -from .vertical_service_map import ( - VERTICALS_AR, - list_verticals, - map_industry_to_vertical, - recommend_services_for_vertical, -) -from .mission_templates import ( - build_service_workflow, - get_default_mission_steps, - map_service_to_growth_mission, -) -from .pricing_engine import ( - calculate_monthly_offer, - calculate_setup_fee, - quote_service, - recommend_plan_after_service, -) -from .service_catalog import ( - ALL_SERVICES, - Service, - catalog_summary, - get_service, - list_all_services, -) -from .service_scorecard import ( - build_service_scorecard, - calculate_service_success_score, - recommend_next_step, - summarize_scorecard_ar, -) -from .service_wizard import ( - build_intake_questions, +from auto_client_acquisition.service_tower.service_wizard import ( recommend_service, summarize_recommendation_ar, - validate_service_inputs, -) -from .upgrade_paths import ( - build_upsell_message_ar, - map_service_to_subscription, - recommend_upgrade, -) -from .whatsapp_ceo_control import ( - build_ceo_daily_service_brief, - build_end_of_day_service_report, - build_risk_alert_card, - build_service_approval_card, ) __all__ = [ - # service_catalog - "ALL_SERVICES", "Service", "catalog_summary", - "get_service", "list_all_services", - # service_wizard - "build_intake_questions", "recommend_service", - "summarize_recommendation_ar", "validate_service_inputs", - # mission_templates - "build_service_workflow", "get_default_mission_steps", - "map_service_to_growth_mission", - # pricing_engine - "calculate_monthly_offer", "calculate_setup_fee", - "quote_service", "recommend_plan_after_service", - # deliverables - "build_client_report_outline", "build_deliverables", - "build_internal_operator_checklist", "build_proof_pack_template", - # service_scorecard - "build_service_scorecard", "calculate_service_success_score", - "recommend_next_step", "summarize_scorecard_ar", - # whatsapp_ceo_control - "build_ceo_daily_service_brief", "build_end_of_day_service_report", - "build_risk_alert_card", "build_service_approval_card", - # upgrade_paths - "build_upsell_message_ar", "map_service_to_subscription", - "recommend_upgrade", - # contract_templates - "draft_sla_outline", "list_contract_templates", - # vertical_service_map - "VERTICALS_AR", "list_verticals", "map_industry_to_vertical", - "recommend_services_for_vertical", + "get_service_by_id", + "list_service_ids", + "list_tower_services", + "recommend_service", + "summarize_recommendation_ar", ] diff --git a/dealix/auto_client_acquisition/service_tower/contract_templates.py b/dealix/auto_client_acquisition/service_tower/contract_templates.py index 519d9e7a..0e49157b 100644 --- a/dealix/auto_client_acquisition/service_tower/contract_templates.py +++ b/dealix/auto_client_acquisition/service_tower/contract_templates.py @@ -1,68 +1,35 @@ -"""Service-tier contract templates — re-export from targeting_os and add SLA.""" +"""Contract / legal outline templates — not legal advice; approval required.""" from __future__ import annotations from typing import Any -from auto_client_acquisition.targeting_os.contract_drafts import ( - draft_agency_partner_outline, - draft_dpa_outline, - draft_pilot_agreement_outline, - draft_referral_agreement_outline, - draft_scope_of_work, -) - def list_contract_templates() -> dict[str, Any]: - """List all contract templates available to the Service Tower.""" - return { - "templates": [ - {"id": "pilot_agreement", **draft_pilot_agreement_outline()}, - {"id": "dpa", **draft_dpa_outline()}, - {"id": "referral", **draft_referral_agreement_outline()}, - {"id": "agency_partner", **draft_agency_partner_outline()}, - {"id": "sow", **draft_scope_of_work()}, - {"id": "sla", **draft_sla_outline()}, - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - } - - -def draft_sla_outline() -> dict[str, Any]: - """Service Level Agreement outline for paid pilots and Growth OS Monthly.""" - return { - "title_ar": "اتفاقية مستوى الخدمة (SLA)", - "sections_ar": [ - "نطاق الخدمة (الـ Pilot أو Growth OS).", - "أوقات الاستجابة (intake خلال 30 دقيقة، diagnostic خلال 24 ساعة).", - "أوقات التسليم لكل deliverable.", - "حدود التوفر (أيام العمل، Time Zone).", - "المسارات في حالة التأخير (escalation).", - "حقوق العميل عند عدم الالتزام (refund / extension).", - "حدود المسؤولية.", - "السرية.", - "PDPL والاحتفاظ بالبيانات.", - "التغييرات في النطاق.", - "إنهاء الاتفاقية.", - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": ( - "هذه مسودة هيكلية فقط، ليست استشارة قانونية. " - "لا تُوقَّع قبل مراجعة محامٍ مرخّص في المملكة العربية السعودية." - ), - } - - -__all__ = [ - "draft_agency_partner_outline", - "draft_dpa_outline", - "draft_pilot_agreement_outline", - "draft_referral_agreement_outline", - "draft_scope_of_work", - "draft_sla_outline", - "list_contract_templates", -] + templates = [ + { + "id": "pilot_agreement", + "title_ar": "مسودة اتفاق Pilot", + "outline_ar": ["نطاق الخدمة", "مدة التجربة", "القياس (Proof)", "PDPL ومصادر البيانات", "إيقاف فوري"], + "legal_review_required": True, + "approval_required": True, + "not_legal_advice": True, + }, + { + "id": "dpa_pilot", + "title_ar": "مسودة DPA تجريبية", + "outline_ar": ["أدوار المعالج/المتحكم", "الاحتفاظ", "حقوق الأفراد", "الأمان", "نقل البيانات"], + "legal_review_required": True, + "approval_required": True, + "not_legal_advice": True, + }, + { + "id": "referral_partner", + "title_ar": "مسودة اتفاق إحالة/شريك", + "outline_ar": ["نسبة الإحالة", "تسوية الفواتير", "العلامة التجارية", "سرية"], + "legal_review_required": True, + "approval_required": True, + "not_legal_advice": True, + }, + ] + return {"templates": templates, "demo": True} diff --git a/dealix/auto_client_acquisition/service_tower/deliverables.py b/dealix/auto_client_acquisition/service_tower/deliverables.py index afe6cdb4..29423d73 100644 --- a/dealix/auto_client_acquisition/service_tower/deliverables.py +++ b/dealix/auto_client_acquisition/service_tower/deliverables.py @@ -1,91 +1,59 @@ -"""Deliverables + Proof Pack templates per service.""" +"""Deliverables and proof pack outlines per service.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id def build_deliverables(service_id: str) -> dict[str, Any]: - """Return the deliverables list for a service.""" - 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, - "deliverables_ar": list(s.deliverables_ar), - "approval_required": True, - } + svc = get_service_by_id(service_id) + if not svc: + return {"service_id": service_id, "deliverables": [], "demo": True} + items = list(svc.get("deliverables_ar") or []) + return {"service_id": service_id, "deliverables_ar": items, "count": len(items), "demo": True} def build_proof_pack_template(service_id: str) -> dict[str, Any]: - """Build a proof-pack template for a service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} + svc = get_service_by_id(service_id) + metrics = list((svc or {}).get("proof_metrics") or []) return { "service_id": service_id, - "service_name_ar": s.name_ar, - "metrics_to_track": list(s.proof_metrics), - "report_sections_ar": [ - "ملخص الفترة", - "ما تم إنجازه (ledger entries)", - "النتائج بالأرقام (الـ proof_metrics)", + "sections_ar": [ + "ملخص الأسبوع", + "ما تم اعتماده", "المخاطر التي تم منعها", - "تجربة الأسبوع/الشهر القادم", - "التوصية بالخطوة التالية", + "الأثر المقدّر", + "الخطوة التالية", ], - "delivery_format": ["pdf", "json", "whatsapp_summary"], - "approval_required": True, + "proof_metrics": metrics, + "demo": True, } def build_client_report_outline(service_id: str) -> dict[str, Any]: - """Outline of the client-facing report for a service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} return { "service_id": service_id, - "title_ar": f"تقرير {s.name_ar}", - "sections_ar": [ - "ملخص تنفيذي (10 أسطر)", - "السياق والأهداف", - "ما عمله Dealix", - "النتائج (الأرقام مقابل الأهداف)", - "أبرز الاعتراضات والـsignals", - "المخاطر التي تم منعها", - "Proof — ledger events", - "التوصية بالخطوة التالية", + "outline_ar": [ + "الهدف والمدخلات", + "ما نفّذناه (مسودات/موافقات)", + "النتائج المقيسة", + "المخاطر والامتثال", + "التوصية للأسبوع القادم", ], - "approval_required": True, + "demo": True, } def build_internal_operator_checklist(service_id: str) -> dict[str, Any]: - """Internal operator checklist (for the team running the service).""" - 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, "checklist_ar": [ - "مراجعة الـ intake واكتمال الحقول.", - "تشغيل targeting + contactability.", - "صياغة الـ drafts الأولى.", - "إرسال للـ approval center.", - "تنفيذ بعد الاعتماد فقط.", - "تتبع النتائج في الـ Action Ledger.", - "بناء Proof Pack.", - "اقتراح الترقية للعميل.", - ], - "do_not_do_ar": [ - "لا live send بدون env flag + اعتماد.", - "لا إرسال على cold list.", - "لا charge بدون تأكيد.", - "لا تخزين أسرار في الـ payload.", + "تأكد من عدم وجود إرسال حي", + "راجع contactability", + "سجّل الموافقات في الدفتر", + "حدّث Proof Pack", ], + "demo": True, } diff --git a/dealix/auto_client_acquisition/service_tower/mission_templates.py b/dealix/auto_client_acquisition/service_tower/mission_templates.py index e03d26eb..759526b5 100644 --- a/dealix/auto_client_acquisition/service_tower/mission_templates.py +++ b/dealix/auto_client_acquisition/service_tower/mission_templates.py @@ -1,94 +1,55 @@ -"""Mission templates — يحوّل الخدمة إلى workflow قابل للتشغيل.""" +"""Map each sellable service to default mission / workflow steps.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service - -# Map service → growth mission ID (in intelligence_layer.mission_engine). -_SERVICE_TO_MISSION: dict[str, str] = { - "free_growth_diagnostic": "first_10_opportunities", - "list_intelligence": "first_10_opportunities", - "first_10_opportunities_sprint": "first_10_opportunities", - "self_growth_operator": "first_10_opportunities", - "growth_os_monthly": "first_10_opportunities", - "email_revenue_rescue": "revenue_leak_rescue", - "meeting_booking_sprint": "meeting_booking_sprint", - "partner_sprint": "partnership_sprint", - "agency_partner_program": "partnership_sprint", - "whatsapp_compliance_setup": "first_10_opportunities", - "linkedin_lead_gen_setup": "first_10_opportunities", - "executive_growth_brief": "first_10_opportunities", -} +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -def get_default_mission_steps(service_id: str) -> list[dict[str, Any]]: - """Return default workflow steps for a service.""" - s = get_service(service_id) - if s is None: - return [] - steps: list[dict[str, Any]] = [] - for i, name in enumerate(s.workflow_steps): - steps.append({ - "order": i + 1, - "step_id": name, - "label_ar": _STEP_LABELS_AR.get(name, name), - "approval_required": name in { - "approval", "execution_or_export", "drafting", - }, - "live_action": False, - }) - return steps - - -_STEP_LABELS_AR: dict[str, str] = { - "intake": "جمع المدخلات", - "data_check": "فحص جودة البيانات", - "targeting": "تحديد الأهداف", - "contactability": "تقييم إمكانية التواصل", - "strategy": "استراتيجية القناة", - "drafting": "صياغة المسودات", - "approval": "اعتماد بشري", - "execution_or_export": "تنفيذ/تصدير", - "tracking": "متابعة النتائج", - "proof": "Proof Pack", - "upsell": "ترقية الخدمة", - "agency_onboarding": "إعداد الوكالة", - "client_diagnostic": "تشخيص عميل الوكالة", - "proposal": "عرض", - "pilot": "Pilot", - "proof_pack": "Proof Pack", - "revenue_share": "Revenue Share", - "aggregate": "تجميع الإشارات", - "prioritize": "ترتيب الأولويات", - "deliver": "تسليم الموجز", -} +_DEFAULT_STEPS = [ + "intake", + "analyze", + "target", + "draft", + "approve", + "track", + "proof", + "upsell", +] def build_service_workflow(service_id: str) -> dict[str, Any]: - """Build the full Arabic workflow for a service.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - steps = get_default_mission_steps(service_id) + svc = get_service_by_id(service_id) + if not svc: + return {"service_id": service_id, "steps": [], "error": "unknown_service", "demo": True} + steps = list(svc.get("workflow_steps") or _DEFAULT_STEPS) return { "service_id": service_id, - "service_name_ar": s.name_ar, - "workflow_steps": steps, - "deliverables_ar": list(s.deliverables_ar), - "approval_policy": s.approval_policy, - "live_send_allowed": False, - "estimated_completion_days": ( - 7 if s.pricing_model == "sprint" - else 30 if s.pricing_model == "monthly" - else 1 - ), - "linked_growth_mission": _SERVICE_TO_MISSION.get(service_id), + "steps": steps, + "approval_gates": [s for s in steps if s in ("approve", "approval")], + "live_send": False, + "demo": True, } -def map_service_to_growth_mission(service_id: str) -> str | None: - """Return the growth-mission ID linked to a service (or None).""" - return _SERVICE_TO_MISSION.get(service_id) +def get_default_mission_steps(service_id: str) -> list[str]: + return list(build_service_workflow(service_id).get("steps") or []) + + +def map_service_to_growth_mission(service_id: str) -> dict[str, Any]: + """Bridge to growth_operator mission naming where applicable.""" + mapping = { + "first_10_opportunities": "first_10_opportunities", + "list_intelligence": "contact_import_preview", + "growth_os": "daily_growth_loop", + "partner_sprint": "partnership_sprint", + "email_revenue_rescue": "email_revenue_rescue", + } + mid = mapping.get(service_id, "generic_service_run") + return { + "service_id": service_id, + "growth_mission_id": mid, + "note_ar": "ربط منطقي للعرض — لا يشغّل مهمة حية.", + "demo": True, + } diff --git a/dealix/auto_client_acquisition/service_tower/pricing_engine.py b/dealix/auto_client_acquisition/service_tower/pricing_engine.py index 50772a00..e2810aa1 100644 --- a/dealix/auto_client_acquisition/service_tower/pricing_engine.py +++ b/dealix/auto_client_acquisition/service_tower/pricing_engine.py @@ -1,118 +1,61 @@ -"""Pricing engine — quotes + setup + monthly + post-service plan.""" +"""Deterministic SAR quotes — hints only, not binding contracts.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id def quote_service( service_id: str, - *, - company_size: str = "small", + company_size: str = "smb", urgency: str = "normal", channels_count: int = 1, ) -> dict[str, Any]: - """Quote a service with company-size + urgency + channels multipliers.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - p_min = float(s.pricing_min_sar) - p_max = float(s.pricing_max_sar) - if p_min == 0 and p_max == 0: - return { - "service_id": service_id, - "is_free": True, - "estimated_min_sar": 0, - "estimated_max_sar": 0, - "currency": "SAR", - "notes_ar": "خدمة مجانية. تتطلب اعتماد قبل التسليم.", - } - - size_mult = {"micro": 0.8, "small": 1.0, "medium": 1.3, "large": 1.7}.get( - company_size, 1.0, - ) - urgency_mult = {"normal": 1.0, "rush": 1.3, "asap": 1.5}.get(urgency, 1.0) - ch_mult = 1.0 + max(0, channels_count - 1) * 0.15 - + svc = get_service_by_id(service_id) + if not svc: + return {"ok": False, "error": "unknown_service", "demo": True} + pr = svc.get("pricing_range_sar") or {"min": 0, "max": 0} + lo = int(pr.get("min", 0)) + hi = int(pr.get("max", lo)) + mult = 1.0 + if (company_size or "").lower() in ("enterprise", "large"): + mult *= 1.15 + if (urgency or "").lower() == "high": + mult *= 1.1 + mult += 0.05 * max(0, min(channels_count, 6) - 1) + lo_q = int(lo * mult) + hi_q = int(hi * mult) return { + "ok": True, "service_id": service_id, - "estimated_min_sar": round(p_min * size_mult * urgency_mult * ch_mult), - "estimated_max_sar": round(p_max * size_mult * urgency_mult * ch_mult), - "currency": "SAR", - "factors": { - "company_size": company_size, - "urgency": urgency, - "channels_count": channels_count, - }, - "pricing_model": s.pricing_model, + "quoted_range_sar": {"min": lo_q, "max": hi_q}, + "factors": {"company_size": company_size, "urgency": urgency, "channels_count": channels_count}, + "not_binding": True, + "demo": True, } +def recommend_plan_after_service(service_id: str, outcome: str) -> dict[str, Any]: + outcome_l = (outcome or "").lower() + svc = get_service_by_id(service_id) + nxt = (svc or {}).get("upgrade_path") or "growth_os" + if "churn" in outcome_l: + nxt = "executive_growth_brief" + return {"next_plan": nxt, "reason_ar": "مسار ترقية افتراضي حسب الكتالوج.", "demo": True} + + def calculate_setup_fee(service_id: str) -> dict[str, Any]: - """Suggest a setup fee for monthly services.""" - s = get_service(service_id) - if s is None or s.pricing_model != "monthly": - return {"setup_fee_sar": 0, "currency": "SAR"} - base = s.pricing_min_sar - return { - "setup_fee_sar": int(base * 1.0), # ~one month equivalent - "includes_ar": [ - "ربط القنوات (واتساب/إيميل/تقويم)", - "استيراد القوائم وتصنيف المصادر", - "تدريب الفريق على Approval Center", - "بناء أول Proof Pack", - ], - "currency": "SAR", - } + q = quote_service(service_id, company_size="smb", urgency="normal", channels_count=1) + r = q.get("quoted_range_sar") or {} + setup = int((r.get("min", 0) + r.get("max", 0)) // 4) + return {"service_id": service_id, "setup_fee_hint_sar": setup, "demo": True} def calculate_monthly_offer(service_id: str) -> dict[str, Any]: - """Return monthly-pricing detail (for monthly services only).""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - if s.pricing_model != "monthly": - return { - "service_id": service_id, - "is_monthly": False, - "notes_ar": "هذه الخدمة ليست شهرية.", - } - return { - "service_id": service_id, - "is_monthly": True, - "monthly_sar": s.pricing_min_sar, - "annual_discount_pct": 15, - "annual_total_sar": int(s.pricing_min_sar * 12 * 0.85), - "currency": "SAR", - } - - -def recommend_plan_after_service( - service_id: str, - *, - outcome: dict[str, Any] | None = None, -) -> dict[str, Any]: - """After a service runs, recommend an upgrade plan.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - outcome = outcome or {} - - upgrade_targets = list(s.upgrade_path) or ["growth_os_monthly"] - next_id = upgrade_targets[0] - next_s = get_service(next_id) - - return { - "from_service": service_id, - "recommended_upgrade": next_id, - "name_ar": next_s.name_ar if next_s else next_id, - "monthly_sar": next_s.pricing_min_sar if next_s else 0, - "reason_ar": ( - f"بعد إثبات قيمة {s.name_ar}، الخطوة الطبيعية هي " - f"الاستمرار مع {next_s.name_ar if next_s else next_id} " - "للحصول على نتائج شهرية مستمرة." - ), - } + if service_id == "growth_os": + return {"service_id": service_id, "monthly_hint_sar": 2999, "demo": True} + if service_id == "self_growth_operator": + return {"service_id": service_id, "monthly_hint_sar": 999, "demo": True} + return {"service_id": service_id, "monthly_hint_sar": None, "note_ar": "خدمة مشروع/سباق — لا اشتراك افتراضي.", "demo": True} diff --git a/dealix/auto_client_acquisition/service_tower/service_catalog.py b/dealix/auto_client_acquisition/service_tower/service_catalog.py index a5013bfb..8e2c6df9 100644 --- a/dealix/auto_client_acquisition/service_tower/service_catalog.py +++ b/dealix/auto_client_acquisition/service_tower/service_catalog.py @@ -1,347 +1,249 @@ -"""The full Dealix service catalog — 12 productized services.""" +"""Full sellable service definitions — complements platform_services.service_catalog.""" from __future__ import annotations -from dataclasses import dataclass, field +from typing import Any + +_SERVICES: list[dict[str, Any]] = [ + { + "service_id": "free_growth_diagnostic", + "name_ar": "تشخيص نمو مجاني", + "target_customer_ar": "أي شركة B2B", + "outcome_ar": "٣ فرص + رسالة واحدة + تقرير مخاطر + اقتراح Pilot.", + "inputs_required": ["company_name", "sector", "city", "offer"], + "workflow_steps": ["intake", "analyze", "draft", "proof_preview", "upsell"], + "deliverables_ar": ["٣ فرص", "١ رسالة", "١ تقرير مخاطر", "اقتراح خدمة مدفوعة"], + "pricing_range_sar": {"min": 0, "max": 0}, + "risk_level": "low", + "required_integrations": [], + "approval_policy": "draft_only", + "proof_metrics": ["diagnostic_to_paid_conversion"], + "upgrade_path": "first_10_opportunities", + }, + { + "service_id": "list_intelligence", + "name_ar": "ذكاء القوائم", + "target_customer_ar": "شركات لديها CSV أرقام/إيميلات", + "outcome_ar": "تطبيع، تصنيف مصدر، contactability، أفضل ٥٠ هدفاً، مسودات.", + "inputs_required": ["csv_or_contacts", "sector"], + "workflow_steps": [ + "intake", + "data_check", + "targeting", + "contactability", + "draft", + "approve", + "proof", + "upsell", + ], + "deliverables_ar": ["تقرير القائمة", "أفضل ٥٠", "مسودات إيميل/مهمة", "تقرير مخاطر"], + "pricing_range_sar": {"min": 499, "max": 1500}, + "risk_level": "medium", + "required_integrations": [], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["safe_ratio", "drafts_created", "blocked_count"], + "upgrade_path": "growth_os", + }, + { + "service_id": "first_10_opportunities", + "name_ar": "سباق ١٠ فرص", + "target_customer_ar": "مؤسس أو فريق مبيعات يحتاج فرصاً سريعة", + "outcome_ar": "١٠ فرص + لماذا الآن + رسائل عربية + خطة متابعة + Proof Pack.", + "inputs_required": ["sector", "city", "offer", "goal"], + "workflow_steps": [ + "intake", + "analyze", + "target", + "draft", + "approve", + "track", + "proof", + "upsell", + ], + "deliverables_ar": ["١٠ فرص", "١٠ مسودات", "خطة متابعة", "قالب Proof Pack"], + "pricing_range_sar": {"min": 499, "max": 1500}, + "risk_level": "medium", + "required_integrations": [], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["opportunities_count", "accept_rate", "meetings_booked"], + "upgrade_path": "growth_os", + }, + { + "service_id": "self_growth_operator", + "name_ar": "مشغّل نمو ذاتي", + "target_customer_ar": "مؤسسون ومستشارون ووكالات صغيرة", + "outcome_ar": "موجز يومي + كروت قرار + مسودات — بدون إرسال حي افتراضياً.", + "inputs_required": ["goal", "channels"], + "workflow_steps": ["intake", "analyze", "draft", "approve", "track", "proof", "upsell"], + "deliverables_ar": ["Daily brief", "٥ كروت/يوم", "تقرير أسبوعي"], + "pricing_range_sar": {"min": 999, "max": 1999}, + "risk_level": "low", + "required_integrations": [], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["cards_approved", "time_saved_hours"], + "upgrade_path": "growth_os", + }, + { + "service_id": "growth_os", + "name_ar": "Growth OS شهري", + "target_customer_ar": "شركات B2B صغيرة ومتوسطة", + "outcome_ar": "Targeting + قنوات + Proof Ledger + تسريبات إيراد.", + "inputs_required": ["onboarding_profile", "channels"], + "workflow_steps": [ + "intake", + "data_check", + "targeting", + "contactability", + "strategy", + "drafting", + "approval", + "execution_or_export", + "tracking", + "proof", + "upsell", + ], + "deliverables_ar": ["Command feed", "Proof Pack أسبوعي", "تقرير قنوات"], + "pricing_range_sar": {"min": 2999, "max": 9999}, + "risk_level": "medium", + "required_integrations": ["gmail", "google_calendar", "whatsapp", "moyasar"], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["revenue_influenced_sar", "meetings_booked", "risks_blocked"], + "upgrade_path": "agency_partner_program", + }, + { + "service_id": "partner_sprint", + "name_ar": "سباق شراكات", + "target_customer_ar": "شركات تريد توسعاً عبر شركاء", + "outcome_ar": "فئات شركاء + أهداف + مسودات + اجتماعات مقترحة + مسودة اتفاق إحالة.", + "inputs_required": ["partner_goal", "sector"], + "workflow_steps": ["intake", "target", "draft", "approve", "track", "proof", "upsell"], + "deliverables_ar": ["قائمة شركاء", "مسودات", "Partner scorecard", "مسودة اتفاق"], + "pricing_range_sar": {"min": 3000, "max": 7500}, + "risk_level": "low", + "required_integrations": [], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["partner_meetings", "referral_pipeline"], + "upgrade_path": "agency_partner_program", + }, + { + "service_id": "agency_partner_program", + "name_ar": "برنامج شركاء وكالات", + "target_customer_ar": "وكالات تسويق ومبيعات", + "outcome_ar": "Onboarding وكالة + عميل تجريبي + Proof مشترك + revenue share outline.", + "inputs_required": ["agency_profile"], + "workflow_steps": ["intake", "analyze", "draft", "approve", "track", "proof"], + "deliverables_ar": ["اتفاقية إطار draft", "قالب Proof مشترك", "SLA تشغيل"], + "pricing_range_sar": {"min": 5000, "max": 25000}, + "risk_level": "medium", + "required_integrations": [], + "approval_policy": "legal_review_required", + "proof_metrics": ["clients_onboarded", "revenue_share_estimate"], + "upgrade_path": "growth_os", + }, + { + "service_id": "email_revenue_rescue", + "name_ar": "إنقاذ إيراد البريد", + "target_customer_ar": "شركات بريد مليء بفرص ضائعة", + "outcome_ar": "كشف فرص متأخرة + مسودات Gmail فقط — بدون إرسال حتى موافقة.", + "inputs_required": ["gmail_label_scope"], + "workflow_steps": ["intake", "data_check", "draft", "approve", "proof", "upsell"], + "deliverables_ar": ["تقرير فرص ضائعة", "مسودات متابعة", "اجتماعات مقترحة"], + "pricing_range_sar": {"min": 1500, "max": 5000}, + "risk_level": "medium", + "required_integrations": ["gmail"], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["drafts_approved", "meetings_from_email"], + "upgrade_path": "growth_os", + }, + { + "service_id": "whatsapp_compliance_setup", + "name_ar": "إعداد واتساب بامتثال", + "target_customer_ar": "شركات تستخدم واتساب بشكل عشوائي", + "outcome_ar": "تدقيق قوائم + opt-in + منع بارد + قوالب موافقة.", + "inputs_required": ["contact_sample_or_policy"], + "workflow_steps": ["intake", "contactability", "draft", "approve", "proof"], + "deliverables_ar": ["تقرير امتثال", "قوالب opt-in", "سياسة موافقات"], + "pricing_range_sar": {"min": 1500, "max": 4000}, + "risk_level": "high", + "required_integrations": ["whatsapp"], + "approval_policy": "cold_blocked_default", + "proof_metrics": ["blocked_cold_count", "opt_in_templates"], + "upgrade_path": "growth_os", + }, + { + "service_id": "linkedin_lead_gen_setup", + "name_ar": "إعداد LinkedIn Lead Gen", + "target_customer_ar": "B2B يستهدف قرارين", + "outcome_ar": "خطة Lead Forms + حقول مخفية + مسودات متابعة — بدون scraping.", + "inputs_required": ["icp", "offer"], + "workflow_steps": ["intake", "strategy", "draft", "approve", "proof"], + "deliverables_ar": ["خطة حملة", "نموذج حقول", "مسودات متابعة"], + "pricing_range_sar": {"min": 2000, "max": 7500}, + "risk_level": "low", + "required_integrations": ["linkedin_lead_forms"], + "approval_policy": "no_auto_dm", + "proof_metrics": ["leads_from_form", "followup_drafts"], + "upgrade_path": "growth_os", + }, + { + "service_id": "meeting_booking_sprint", + "name_ar": "سباق حجز اجتماعات", + "target_customer_ar": "فرق لديها prospects بدون تحويل لاجتماعات", + "outcome_ar": "دعوات + مسودة تقويم + أجندة + متابعة — بدون insert حي بدون موافقة.", + "inputs_required": ["targets", "offer"], + "workflow_steps": ["intake", "draft", "approve", "track", "proof"], + "deliverables_ar": ["مسودات دعوة", "payload تقويم draft", "ملخص اجتماع"], + "pricing_range_sar": {"min": 1500, "max": 5000}, + "risk_level": "low", + "required_integrations": ["google_calendar"], + "approval_policy": "calendar_insert_requires_approval", + "proof_metrics": ["meetings_booked", "followups_sent_after_approval"], + "upgrade_path": "growth_os", + }, + { + "service_id": "local_growth_os", + "name_ar": "نمو محلي", + "target_customer_ar": "عيادات ومطاعم ومتاجر", + "outcome_ar": "تقييمات + واتساب inbound + روابط دفع draft.", + "inputs_required": ["vertical", "branches"], + "workflow_steps": ["intake", "strategy", "draft", "approve", "proof"], + "deliverables_ar": ["كروت سمعة", "مسودات رد تقييم", "روابط دفع draft"], + "pricing_range_sar": {"min": 999, "max": 2999}, + "risk_level": "medium", + "required_integrations": ["google_business_profile", "whatsapp", "moyasar"], + "approval_policy": "approval_required_for_send", + "proof_metrics": ["reviews_addressed", "reactivation_drafts"], + "upgrade_path": "growth_os", + }, + { + "service_id": "executive_growth_brief", + "name_ar": "موجز تنفيذي يومي", + "target_customer_ar": "CEO / مدير نمو", + "outcome_ar": "٣ قرارات + ٣ فرص + ٣ مخاطر — عربي وأزرار ≤٣.", + "inputs_required": ["company_profile"], + "workflow_steps": ["intake", "analyze", "proof"], + "deliverables_ar": ["موجز يومي", "تقرير نهاية يوم"], + "pricing_range_sar": {"min": 499, "max": 999}, + "risk_level": "low", + "required_integrations": [], + "approval_policy": "suggest_and_draft_only", + "proof_metrics": ["decisions_logged", "risks_flagged"], + "upgrade_path": "growth_os", + }, +] -@dataclass(frozen=True) -class Service: - """A single sellable, productized service.""" - id: str - name_ar: str - target_customer_ar: str - outcome_ar: str - inputs_required: tuple[str, ...] - workflow_steps: tuple[str, ...] - deliverables_ar: tuple[str, ...] - pricing_min_sar: int - pricing_max_sar: int - pricing_model: str # "one_time" | "monthly" | "sprint" - risk_level: str # "low" | "medium" | "high" - required_integrations: tuple[str, ...] - approval_policy: str # short label - proof_metrics: tuple[str, ...] - upgrade_path: tuple[str, ...] = field(default_factory=tuple) - - def to_dict(self) -> dict[str, object]: - return { - "id": self.id, "name_ar": self.name_ar, - "target_customer_ar": self.target_customer_ar, - "outcome_ar": self.outcome_ar, - "inputs_required": list(self.inputs_required), - "workflow_steps": list(self.workflow_steps), - "deliverables_ar": list(self.deliverables_ar), - "pricing_min_sar": self.pricing_min_sar, - "pricing_max_sar": self.pricing_max_sar, - "pricing_model": self.pricing_model, - "risk_level": self.risk_level, - "required_integrations": list(self.required_integrations), - "approval_policy": self.approval_policy, - "proof_metrics": list(self.proof_metrics), - "upgrade_path": list(self.upgrade_path), - } +def list_tower_services() -> dict[str, Any]: + return {"services": list(_SERVICES), "count": len(_SERVICES), "version": 1, "demo": True} -_DEFAULT_WORKFLOW: tuple[str, ...] = ( - "intake", "data_check", "targeting", "contactability", - "strategy", "drafting", "approval", - "execution_or_export", "tracking", "proof", "upsell", -) +def list_service_ids() -> list[str]: + return [s["service_id"] for s in _SERVICES] -ALL_SERVICES: tuple[Service, ...] = ( - Service( - id="free_growth_diagnostic", - name_ar="تشخيص نمو مجاني", - target_customer_ar="أي شركة B2B تريد عينة قبل Pilot", - outcome_ar="3 فرص + رسالة + تقرير مخاطر + خطة Pilot — خلال 24 ساعة عمل", - inputs_required=("sector", "city", "offer", "goal"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "3 فرص B2B مع why-now", - "رسالة عربية مخصصة", - "تقرير مخاطر", - "خطة Pilot مقترحة", - ), - pricing_min_sar=0, pricing_max_sar=0, - pricing_model="one_time", - risk_level="low", - required_integrations=(), - approval_policy="approval_required_for_share", - proof_metrics=("diagnostic_to_paid_conversion",), - upgrade_path=("first_10_opportunities_sprint", "growth_os_monthly"), - ), - Service( - id="list_intelligence", - name_ar="تحليل القوائم (List Intelligence)", - target_customer_ar="شركات لديها قوائم أرقام/إيميلات/عملاء قدامى", - outcome_ar="تنظيف + تصنيف + أفضل 50 target + رسائل + خطة 7 أيام", - inputs_required=("uploaded_csv", "channels_available"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "قائمة منظفة + dedupe", - "تصنيف safe / needs_review / blocked", - "أفضل 50 target", - "رسائل عربية", - "تقرير مخاطر", - ), - pricing_min_sar=499, pricing_max_sar=1500, - pricing_model="one_time", - risk_level="medium", - required_integrations=("google_sheets",), - approval_policy="draft_only", - proof_metrics=("contacts_classified", "safe_targets_found", "risks_blocked"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="first_10_opportunities_sprint", - name_ar="10 فرص في 10 دقائق (Sprint)", - target_customer_ar="شركة B2B تحتاج فرصاً مؤهلة بسرعة", - outcome_ar="10 فرص + رسائل + خطة متابعة + Proof Pack — خلال 7 أيام", - inputs_required=("sector", "city", "offer", "goal"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "10 فرص B2B مع why-now", - "10 رسائل عربية", - "خطة متابعة 7 أيام", - "Proof Pack تفصيلي", - ), - pricing_min_sar=499, pricing_max_sar=1500, - pricing_model="sprint", - risk_level="low", - required_integrations=(), - approval_policy="draft_only", - proof_metrics=("opportunities_count", "approval_rate", - "positive_replies", "meetings_drafted"), - upgrade_path=("growth_os_monthly", "self_growth_operator"), - ), - Service( - id="self_growth_operator", - name_ar="مدير نمو شخصي (Self-Growth Operator)", - target_customer_ar="مؤسسون / مستشارون / وكالات صغيرة", - outcome_ar="Daily brief + drafts + متابعة + تقارير أسبوعية", - inputs_required=("company_profile", "goals"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "Daily brief عربي", - "5 cards/day للقرارات", - "Drafts + approvals", - "Weekly learning report", - ), - pricing_min_sar=999, pricing_max_sar=999, - pricing_model="monthly", - risk_level="low", - required_integrations=("gmail", "google_calendar"), - approval_policy="approval_required", - proof_metrics=("decisions_per_day", "drafts_approved", - "meetings_drafted", "pipeline_sar"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="growth_os_monthly", - name_ar="Growth OS — اشتراك شهري", - target_customer_ar="شركات B2B صغيرة-متوسطة", - outcome_ar="منصة كاملة: قنوات، command feed، proof pack، فريق", - inputs_required=("company_profile", "channels", "team_size"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "ربط القنوات", - "Daily autopilot", - "Approvals مركزية", - "Proof Pack شهري", - "Revenue leak detector", - ), - pricing_min_sar=2999, pricing_max_sar=2999, - pricing_model="monthly", - risk_level="medium", - required_integrations=("gmail", "google_calendar", "moyasar", - "google_sheets"), - approval_policy="approval_required", - proof_metrics=("monthly_pipeline_sar", "monthly_meetings", - "monthly_revenue_influenced", "monthly_risks_blocked"), - upgrade_path=("agency_partner_program",), - ), - Service( - id="email_revenue_rescue", - name_ar="استعادة الإيرادات من الإيميل", - target_customer_ar="شركات إيميل الشركة فيه فرص ضائعة", - outcome_ar="استخراج فرص ضائعة + drafts + meetings + missed revenue report", - inputs_required=("gmail_label", "ICP"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "Scan الـ inbox/labels", - "Drafts للردود المتأخرة", - "Meeting drafts", - "Missed revenue report", - ), - pricing_min_sar=1500, pricing_max_sar=5000, - pricing_model="one_time", - risk_level="high", - required_integrations=("gmail",), - approval_policy="approval_required", - proof_metrics=("opportunities_found", "drafts_created", - "meetings_drafted", "missed_revenue_sar"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="meeting_booking_sprint", - name_ar="سبرنت حجز الاجتماعات", - target_customer_ar="شركات لديها prospects ولا تحوّلهم لاجتماعات", - outcome_ar="invitations + meeting drafts + briefs + follow-ups", - inputs_required=("prospect_list", "calendar_link"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "دعوات اجتماع", - "Pre-meeting brief", - "Calendar drafts", - "Post-meeting follow-up", - ), - pricing_min_sar=1500, pricing_max_sar=5000, - pricing_model="sprint", - risk_level="medium", - required_integrations=("google_calendar", "gmail"), - approval_policy="approval_required", - proof_metrics=("meetings_drafted", "meetings_confirmed", - "meetings_completed"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="partner_sprint", - name_ar="سبرنت شراكات", - target_customer_ar="شركات تحتاج نمو عبر الشركاء والوكالات", - outcome_ar="20 شريك محتمل + 10 رسائل + 5 اجتماعات + scorecard", - inputs_required=("sector", "partner_goal"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "قائمة شركاء محتملين", - "Scorecard لكل شريك", - "Outreach drafts", - "Meeting plan", - "Referral agreement draft", - ), - pricing_min_sar=3000, pricing_max_sar=7500, - pricing_model="sprint", - risk_level="medium", - required_integrations=("gmail",), - approval_policy="approval_required", - proof_metrics=("partners_identified", "partner_meetings", - "referral_revenue_sar"), - upgrade_path=("agency_partner_program",), - ), - Service( - id="agency_partner_program", - name_ar="برنامج وكالة شريكة", - target_customer_ar="وكالات تسويق/مبيعات/CRM", - outcome_ar="بيع Dealix لعملاء الوكالة مع co-branding + revenue share", - inputs_required=("agency_profile", "client_count"), - workflow_steps=("agency_onboarding", "client_diagnostic", - "proposal", "pilot", "proof_pack", "revenue_share"), - deliverables_ar=( - "Agency onboarding", - "Client diagnostics", - "Co-branded proof packs", - "Revenue share dashboard", - ), - pricing_min_sar=10000, pricing_max_sar=50000, - pricing_model="one_time", - risk_level="medium", - required_integrations=("gmail", "google_calendar", "moyasar"), - approval_policy="approval_required", - proof_metrics=("clients_added", "agency_revenue_sar", - "co_branded_proofs"), - ), - Service( - id="whatsapp_compliance_setup", - name_ar="إعداد امتثال واتساب", - target_customer_ar="شركات تستخدم واتساب بشكل عشوائي", - outcome_ar="audit + opt-in templates + approval workflow + ledger", - inputs_required=("contact_list", "current_practice"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "تصنيف القوائم", - "Opt-in templates", - "Approval cards", - "Opt-out ledger", - "Safety report", - ), - pricing_min_sar=1500, pricing_max_sar=4000, - pricing_model="one_time", - risk_level="high", - required_integrations=("whatsapp_cloud",), - approval_policy="draft_only", - proof_metrics=("contacts_classified", "opt_ins_collected", - "risks_blocked"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="linkedin_lead_gen_setup", - name_ar="إعداد LinkedIn Lead Gen", - target_customer_ar="شركات B2B تحتاج decision makers", - outcome_ar="حملة Lead Gen Form + audiences + ربط CRM + content angle", - inputs_required=("ICP", "offer", "ad_budget"), - workflow_steps=_DEFAULT_WORKFLOW, - deliverables_ar=( - "Audience plan", - "Lead magnet", - "Lead Gen Form", - "Hidden fields setup", - "Dealix intake", - "Follow-up drafts", - ), - pricing_min_sar=2000, pricing_max_sar=7500, - pricing_model="one_time", - risk_level="medium", - required_integrations=("linkedin_lead_forms",), - approval_policy="approval_required", - proof_metrics=("leads_captured", "qualified_leads", - "meetings_booked"), - upgrade_path=("growth_os_monthly",), - ), - Service( - id="executive_growth_brief", - name_ar="موجز نمو تنفيذي (Executive Brief)", - target_customer_ar="CEO / Growth Manager / Sales Manager", - outcome_ar="3 قرارات + 3 فرص + 3 مخاطر + Pipeline + اجتماعات اليوم", - inputs_required=("company_profile",), - workflow_steps=("intake", "aggregate", "prioritize", "deliver"), - deliverables_ar=( - "Daily brief عبر واتساب/Email", - "Approval cards (≤3 buttons)", - "Risk alerts", - "Weekly Founder Shadow Board", - ), - pricing_min_sar=499, pricing_max_sar=999, - pricing_model="monthly", - risk_level="low", - required_integrations=(), - approval_policy="approval_required", - proof_metrics=("decisions_made", "alerts_actioned"), - upgrade_path=("growth_os_monthly",), - ), -) - - -def get_service(service_id: str) -> Service | None: - return next((s for s in ALL_SERVICES if s.id == service_id), None) - - -def list_all_services() -> dict[str, object]: - return { - "total": len(ALL_SERVICES), - "services": [s.to_dict() for s in ALL_SERVICES], - } - - -def catalog_summary() -> dict[str, object]: - by_pricing: dict[str, int] = {} - by_risk: dict[str, int] = {} - for s in ALL_SERVICES: - by_pricing[s.pricing_model] = by_pricing.get(s.pricing_model, 0) + 1 - by_risk[s.risk_level] = by_risk.get(s.risk_level, 0) + 1 - return { - "total": len(ALL_SERVICES), - "by_pricing_model": by_pricing, - "by_risk_level": by_risk, - "free_offers": [s.id for s in ALL_SERVICES if s.pricing_max_sar == 0], - } +def get_service_by_id(service_id: str) -> dict[str, Any] | None: + sid = (service_id or "").strip().lower() + for s in _SERVICES: + if s["service_id"] == sid: + return dict(s) + return None diff --git a/dealix/auto_client_acquisition/service_tower/service_scorecard.py b/dealix/auto_client_acquisition/service_tower/service_scorecard.py index e6a94c2b..15ece542 100644 --- a/dealix/auto_client_acquisition/service_tower/service_scorecard.py +++ b/dealix/auto_client_acquisition/service_tower/service_scorecard.py @@ -1,105 +1,42 @@ -"""Service scorecard — يقيس نجاح كل خدمة بعد تشغيلها.""" +"""Per-service success scorecard — deterministic from metrics dict.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id -def calculate_service_success_score( - service_id: str, metrics: dict[str, Any], -) -> dict[str, Any]: - """Score a service run 0..100 + verdict.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - - score = 0 - - # Generic outcomes that map to most services. - drafts_approved = int(metrics.get("drafts_approved", 0)) - positive_replies = int(metrics.get("positive_replies", 0)) - meetings = int(metrics.get("meetings", 0)) - pipeline_sar = float(metrics.get("pipeline_sar", 0)) - risks_blocked = int(metrics.get("risks_blocked", 0)) - customer_satisfaction = int(metrics.get("customer_satisfaction", 0)) # 0..10 - - score += min(15, drafts_approved * 3) - score += min(20, positive_replies * 5) - score += min(20, meetings * 8) - score += min(20, int(pipeline_sar / 5_000)) - score += min(10, risks_blocked * 2) - score += min(15, customer_satisfaction * 1) - - score = max(0, min(100, score)) - - if score >= 70: - verdict = "strong_outcome" - elif score >= 40: - verdict = "decent_outcome" - else: - verdict = "needs_iteration" - +def build_service_scorecard(service_id: str, metrics: dict[str, Any]) -> dict[str, Any]: + svc = get_service_by_id(service_id) + m = metrics or {} + drafts = int(m.get("drafts_created", 0)) + approved = int(m.get("approvals", 0)) + meetings = int(m.get("meetings_booked", 0)) + blocked = int(m.get("risks_blocked", 0)) + score = min(100, 20 + drafts * 3 + approved * 5 + meetings * 10 + blocked * 2) + status = "strong" if score >= 70 else "needs_attention" return { "service_id": service_id, "score": score, - "verdict": verdict, - "captured_metrics": metrics, + "status": status, + "summary_ar": f"درجة الخدمة {score}/١٠٠ — الحالة: {status}.", + "inputs_used": list(m.keys()), + "name_ar": (svc or {}).get("name_ar"), + "demo": True, } +def calculate_service_success_score(metrics: dict[str, Any]) -> int: + sc = build_service_scorecard("growth_os", metrics) + return int(sc.get("score", 0)) + + def recommend_next_step(metrics: dict[str, Any]) -> dict[str, Any]: - """Recommend the next step for a customer based on outcome metrics.""" - pipeline_sar = float(metrics.get("pipeline_sar", 0)) - meetings = int(metrics.get("meetings", 0)) - csat = int(metrics.get("customer_satisfaction", 0)) - - if csat >= 8 and (pipeline_sar >= 25_000 or meetings >= 2): - return { - "action": "upsell_to_growth_os", - "label_ar": "اعرض Growth OS الشهري — العميل راضٍ والنتائج قوية.", - } - if pipeline_sar < 5_000 and meetings == 0: - return { - "action": "iterate_offer_or_segment", - "label_ar": "غيّر زاوية العرض أو القطاع — النتائج ضعيفة.", - } - return { - "action": "extend_pilot", - "label_ar": "مدّد الـ Pilot لأسبوعين أو جرّب قناة إضافية.", - } - - -def build_service_scorecard( - service_id: str, metrics: dict[str, Any], -) -> dict[str, Any]: - """Build a full Arabic scorecard for a service run.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - score_obj = calculate_service_success_score(service_id, metrics) - next_step = recommend_next_step(metrics) - return { - "service_id": service_id, - "service_name_ar": s.name_ar, - "score": score_obj.get("score"), - "verdict": score_obj.get("verdict"), - "metrics": metrics, - "next_step": next_step, - "summary_ar": summarize_scorecard_ar({ - "service_id": service_id, - **score_obj, "next_step": next_step, - }), - } + if int(metrics.get("risks_blocked", 0)) > int(metrics.get("meetings_booked", 0)): + return {"next_step_ar": "ركّز على تحويل المسودات المعتمدة إلى اجتماعات.", "demo": True} + return {"next_step_ar": "وسّع القنوات بعد تثبيت الاجتماعات.", "demo": True} def summarize_scorecard_ar(scorecard: dict[str, Any]) -> str: - s = get_service(scorecard.get("service_id", "")) - name = s.name_ar if s else scorecard.get("service_id", "?") - score = scorecard.get("score", 0) - verdict = scorecard.get("verdict", "?") - next_step = (scorecard.get("next_step") or {}).get("label_ar", "") - return ( - f"{name}: درجة {score} ({verdict}). الخطوة التالية: {next_step}" - ) + return str(scorecard.get("summary_ar") or "") diff --git a/dealix/auto_client_acquisition/service_tower/service_wizard.py b/dealix/auto_client_acquisition/service_tower/service_wizard.py index 88d4fadd..7711029e 100644 --- a/dealix/auto_client_acquisition/service_tower/service_wizard.py +++ b/dealix/auto_client_acquisition/service_tower/service_wizard.py @@ -1,137 +1,109 @@ -"""Service wizard — يوصي بالخدمة المناسبة من إجابات بسيطة.""" +"""Recommend sellable service from intake — deterministic, no live actions.""" from __future__ import annotations from typing import Any -from .service_catalog import ALL_SERVICES, get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id, list_service_ids def recommend_service( - *, - company_type: str = "", - goal: str = "fill_pipeline", + company_type: str, + goal: str, has_contact_list: bool = False, channels: list[str] | None = None, - budget_sar: int = 1000, + budget_sar: int | None = None, ) -> dict[str, Any]: - """ - Recommend the best-fit service based on inputs. Deterministic. - """ - channels = channels or [] - company_type_lc = (company_type or "").lower() + ct = (company_type or "").lower().strip() + gl = (goal or "").lower().strip() + ch = [c.lower() for c in (channels or [])] - chosen_id: str - reason: str + recommended = "first_10_opportunities" + reasons: list[str] = [] - # Highest priority first. - if "agency" in company_type_lc or "وكالة" in company_type: - chosen_id = "agency_partner_program" if budget_sar >= 10_000 else "partner_sprint" - reason = "وكالة → برنامج شريك أو سبرنت شراكات." - elif has_contact_list: - chosen_id = "list_intelligence" - reason = "العميل لديه قائمة → ابدأ بـ List Intelligence." - elif "founder" in company_type_lc or "مؤسس" in company_type: - chosen_id = "self_growth_operator" - reason = "مؤسس بدون فريق نمو → Self-Growth Operator." - elif "executive" in company_type_lc or "ceo" in company_type_lc: - chosen_id = "executive_growth_brief" - reason = "CEO/تنفيذي → موجز نمو يومي." - elif "whatsapp" in company_type_lc or "واتساب" in company_type: - chosen_id = "whatsapp_compliance_setup" - reason = "حالة واتساب عشوائية → امتثال أولاً." - elif goal == "rescue_lost_revenue": - chosen_id = "email_revenue_rescue" - reason = "الهدف استعادة إيراد ضائع → Email Revenue Rescue." - elif goal == "book_meetings": - chosen_id = "meeting_booking_sprint" - reason = "الهدف اجتماعات → Meeting Booking Sprint." - elif goal == "expand_partners": - chosen_id = "partner_sprint" - reason = "الهدف شراكات → Partner Sprint." - elif budget_sar >= 2999: - chosen_id = "growth_os_monthly" - reason = "الميزانية شهرية → Growth OS." - else: - chosen_id = "first_10_opportunities_sprint" - reason = "الافتراضي: ابدأ بـ 10 فرص في 10 دقائق." + if "agency" in ct or "وكالة" in company_type: + recommended = "agency_partner_program" + reasons.append("وكالات: قناة توزيع + برنامج شركاء.") + elif has_contact_list or "list" in gl or "csv" in gl or "قائمة" in goal: + recommended = "list_intelligence" + reasons.append("قائمة مرفوعة: ذكاء القوائم يقلل المخاطر أولاً.") + elif "email" in gl or "بريد" in goal or "inbox" in gl: + recommended = "email_revenue_rescue" + reasons.append("هدف بريدي: إنقاذ فرص ضائعة بمسودات فقط.") + elif "partner" in gl or "شراكة" in goal: + recommended = "partner_sprint" + reasons.append("هدف شراكات: سباق شركاء منظم.") + elif "meeting" in gl or "اجتماع" in goal: + recommended = "meeting_booking_sprint" + reasons.append("تحويل prospects لاجتماعات بمسودات موافقة.") + elif "linkedin" in gl or "لينكد" in goal: + recommended = "linkedin_lead_gen_setup" + reasons.append("لينكدإن: Lead Gen رسمي بدون أتمتة مخالفة.") + elif "whatsapp" in gl or "واتساب" in goal or "whatsapp" in ch: + recommended = "whatsapp_compliance_setup" + reasons.append("واتساب: امتثال وopt-in قبل أي حملة.") + elif "local" in gl or "عيادة" in goal or "متجر" in goal: + recommended = "local_growth_os" + reasons.append("نمو محلي: تقييمات + inbound + دفع draft.") + elif budget_sar is not None and budget_sar < 1500: + recommended = "free_growth_diagnostic" + reasons.append("ميزانية منخفضة: تشخيص مجاني ثم ترقية.") - service = get_service(chosen_id) + svc = get_service_by_id(recommended) return { - "recommended_service_id": chosen_id, - "service": service.to_dict() if service else None, - "reason_ar": reason, - "next_step_ar": ( - "املأ نموذج الـ intake، وسنبدأ خلال 24 ساعة عمل." - ), + "recommended_service_id": recommended, + "service": svc, + "reasons_ar": reasons or ["أسرع إثبات قيمة: سباق ١٠ فرص."], + "live_send": False, + "demo": True, } def build_intake_questions(service_id: str) -> dict[str, Any]: - """Return intake questions for a service. Empty if service unknown.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}", "questions": []} - - base_q = [ - {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, - {"key": "sector", "label_ar": "القطاع", "required": True}, - {"key": "city", "label_ar": "المدينة", "required": True}, - {"key": "decision_maker_name", "label_ar": "اسم صانع القرار", "required": True}, - {"key": "decision_maker_role", "label_ar": "المسمى الوظيفي", "required": True}, - ] - extra = [] - if "uploaded_csv" in s.inputs_required: - extra.append({"key": "uploaded_csv", "label_ar": "ملف CSV", "required": True}) - if "offer" in s.inputs_required: - extra.append({"key": "offer", "label_ar": "وصف العرض", "required": True}) - if "goal" in s.inputs_required: - extra.append({"key": "goal", "label_ar": "الهدف الأساسي", "required": True}) - if "channels_available" in s.inputs_required: - extra.append({"key": "channels", "label_ar": "القنوات المتاحة", "required": False}) - - return { - "service_id": service_id, - "service_name_ar": s.name_ar, - "questions": base_q + extra, - "approval_required": True, - } + svc = get_service_by_id(service_id) + if not svc: + return {"service_id": service_id, "questions": [], "error": "unknown_service", "demo": True} + qs: list[dict[str, str]] = [] + for inp in svc.get("inputs_required") or []: + qs.append( + { + "field": inp, + "prompt_ar": f"ما قيمة الحقل: {inp}؟", + "required": "true", + } + ) + return {"service_id": service_id, "questions": qs, "demo": True} -def validate_service_inputs( - service_id: str, payload: dict[str, Any], -) -> dict[str, Any]: - """Validate intake payload against service requirements.""" - s = get_service(service_id) - if s is None: - return {"valid": False, "errors_ar": [f"خدمة غير معروفة: {service_id}"]} - - errors: list[str] = [] - for required in s.inputs_required: - if required in ("uploaded_csv", "offer", "goal", "channels_available", - "ICP", "calendar_link", "company_profile", - "current_practice", "ad_budget", "client_count", - "partner_goal", "team_size", "channels", "agency_profile", - "prospect_list", "gmail_label", "contact_list", - "goals", "sector", "city"): - if not payload.get(required): - errors.append(f"الحقل ناقص: {required}") - - return { - "valid": not errors, - "errors_ar": errors, - "service_id": service_id, - } +def validate_service_inputs(service_id: str, payload: dict[str, Any]) -> dict[str, Any]: + svc = get_service_by_id(service_id) + if not svc: + return {"ok": False, "missing": ["unknown_service"], "demo": True} + missing: list[str] = [] + for key in svc.get("inputs_required") or []: + if key not in (payload or {}) or payload.get(key) in (None, "", []): + missing.append(key) + return {"ok": len(missing) == 0, "missing": missing, "demo": True} def summarize_recommendation_ar(result: dict[str, Any]) -> str: - """Build a one-paragraph Arabic recommendation summary.""" - sid = result.get("recommended_service_id", "?") - reason = result.get("reason_ar", "") - svc = result.get("service") or {} - name = svc.get("name_ar", sid) - outcome = svc.get("outcome_ar", "") - return ( - f"الخدمة المقترحة: {name}. السبب: {reason} " - f"المخرجات: {outcome}" - ) + rid = result.get("recommended_service_id") or "غير محدد" + reasons = result.get("reasons_ar") or [] + tail = " ".join(reasons) if reasons else "" + return f"التوصية: {rid}. {tail} لا يوجد إرسال حي من هذا المسار." + + +def start_service(service_id: str, payload: dict[str, Any]) -> dict[str, Any]: + """MVP: validate + return workflow handle — no side effects.""" + v = validate_service_inputs(service_id, payload or {}) + svc = get_service_by_id(service_id) + return { + "started": bool(v.get("ok")), + "service_id": service_id, + "validation": v, + "workflow_ref": f"wf_{service_id}_demo" if v.get("ok") else None, + "approval_required": True, + "live_send": False, + "service_snapshot": {"name_ar": (svc or {}).get("name_ar"), "risk_level": (svc or {}).get("risk_level")}, + "demo": True, + } diff --git a/dealix/auto_client_acquisition/service_tower/upgrade_paths.py b/dealix/auto_client_acquisition/service_tower/upgrade_paths.py index 14db9253..8855d7c8 100644 --- a/dealix/auto_client_acquisition/service_tower/upgrade_paths.py +++ b/dealix/auto_client_acquisition/service_tower/upgrade_paths.py @@ -1,59 +1,47 @@ -"""Upgrade paths — يوصي بالخدمة التالية بعد كل خدمة.""" +"""Upsell / upgrade paths between services.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service +from auto_client_acquisition.service_tower.service_catalog import get_service_by_id, list_tower_services -def recommend_upgrade( - service_id: str, - *, - results: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Recommend the next service for a customer to buy.""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} +def build_all_upgrade_paths() -> dict[str, Any]: + paths = [] + for s in list_tower_services().get("services") or []: + paths.append( + { + "service_id": s["service_id"], + "name_ar": s.get("name_ar"), + "upgrade_path": s.get("upgrade_path"), + } + ) + return {"paths": paths, "demo": True} - upgrade_targets = list(s.upgrade_path) or ["growth_os_monthly"] - next_id = upgrade_targets[0] - next_s = get_service(next_id) +def recommend_upgrade(service_id: str, results: dict[str, Any]) -> dict[str, Any]: + svc = get_service_by_id(service_id) + default_next = (svc or {}).get("upgrade_path") or "growth_os" + r = results or {} + if int(r.get("paid_conversion", 0)) > 0: + default_next = "growth_os" return { "from_service": service_id, - "from_service_name_ar": s.name_ar, - "recommended_service_id": next_id, - "recommended_service_name_ar": next_s.name_ar if next_s else next_id, - "monthly_sar": next_s.pricing_min_sar if next_s else 0, - "reason_ar": ( - f"بعد {s.name_ar}، الترقية الطبيعية هي " - f"{next_s.name_ar if next_s else next_id} للحفاظ على الاستمرارية." - ), + "recommended_upgrade": default_next, + "reason_ar": "بعد إثبات القيمة: Growth OS للتشغيل الشهري.", + "demo": True, } -def build_upsell_message_ar( - service_id: str, - next_offer: str, -) -> str: - """Build a one-paragraph Arabic upsell message.""" - s = get_service(service_id) - next_s = get_service(next_offer) - if not s or not next_s: - return "بعد إثبات النتائج، نوصي بالترقية للخدمة التالية." +def build_upsell_message_ar(service_id: str, next_offer: str) -> str: return ( - f"شاكر لك على تجربة {s.name_ar}. " - f"بناءً على النتائج، الترقية المنطقية هي {next_s.name_ar} " - "للاستمرار في النمو شهرياً مع نفس مستوى الـ Proof Pack. " - "أرسل لي تأكيد ونبدأ الأسبوع القادم." + f"أكملنا {service_id} بنجاح. الخطوة المنطقية: {next_offer} " + "للتشغيل الشهري مع Proof Pack — بدون إرسال حي بدون موافقتك." ) -def map_service_to_subscription(service_id: str) -> str: - """Map any service to its eventual subscription.""" - s = get_service(service_id) - if s is None: - return "growth_os_monthly" - return s.upgrade_path[0] if s.upgrade_path else "growth_os_monthly" +def map_service_to_subscription(service_id: str) -> dict[str, Any]: + if service_id in ("growth_os", "self_growth_operator", "local_growth_os"): + return {"subscription_id": "growth_os_monthly", "eligible": True, "demo": True} + return {"subscription_id": "growth_os_monthly", "eligible": False, "note_ar": "خدمة مشروع — اشتراك اختياري بعد Pilot.", "demo": True} diff --git a/dealix/auto_client_acquisition/service_tower/vertical_service_map.py b/dealix/auto_client_acquisition/service_tower/vertical_service_map.py index 5bd67048..fd21d594 100644 --- a/dealix/auto_client_acquisition/service_tower/vertical_service_map.py +++ b/dealix/auto_client_acquisition/service_tower/vertical_service_map.py @@ -1,168 +1,49 @@ -"""Vertical service map — which services to recommend per industry vertical.""" +"""Three-door UX map + vertical hints for Service Tower.""" from __future__ import annotations from typing import Any -# 6 verticals × recommended service stack. -VERTICALS_AR: dict[str, dict[str, Any]] = { - "b2b_saas": { - "label_ar": "B2B SaaS", - "primary_services": [ - "first_10_opportunities_sprint", - "linkedin_lead_gen_setup", - "growth_os_monthly", - ], - "supporting_services": [ - "meeting_booking_sprint", - "executive_growth_brief", - ], - "buyer_roles": ["founder_ceo", "head_of_sales", "growth_manager"], - "common_pains_ar": [ - "Pipeline ضعيف عند الإطلاق", - "صعوبة الوصول لـ decision makers في المؤسسات", - "Cold outreach يضرّ سمعة الـ domain", - ], - "winning_offer_ar": "Pilot 7 أيام يثبت Saudi Tone + LinkedIn Lead Forms.", - }, - "agencies": { - "label_ar": "الوكالات (تسويق/مبيعات/CRM)", - "primary_services": [ - "agency_partner_program", - "partner_sprint", - ], - "supporting_services": [ - "list_intelligence", - "first_10_opportunities_sprint", - ], - "buyer_roles": ["agency_owner", "head_of_sales", "growth_manager"], - "common_pains_ar": [ - "تسليم نتائج قابلة للقياس للعملاء", - "Proof Packs للعملاء بدون فريق نمو داخلي", - "خلق revenue stream متكرر", - ], - "winning_offer_ar": "Agency Partner Program مع co-branded Proof Pack.", - }, - "training_consulting": { - "label_ar": "التدريب والاستشارات", - "primary_services": [ - "first_10_opportunities_sprint", - "list_intelligence", - "growth_os_monthly", - ], - "supporting_services": [ - "executive_growth_brief", - "meeting_booking_sprint", - ], - "buyer_roles": ["founder_ceo", "head_of_sales", "hr_manager"], - "common_pains_ar": [ - "اعتماد مفرط على العلاقات الشخصية", - "Pipeline متذبذب بين الفصول الدراسية/الـ quarters", - "صعوبة الوصول لمدراء HR في الشركات", - ], - "winning_offer_ar": "First 10 Opportunities Sprint للوصول لـHR managers.", - }, - "real_estate": { - "label_ar": "العقار", - "primary_services": [ - "list_intelligence", - "whatsapp_compliance_setup", - "first_10_opportunities_sprint", - ], - "supporting_services": [ - "meeting_booking_sprint", - "growth_os_monthly", - ], - "buyer_roles": ["founder_ceo", "head_of_sales", "branch_manager"], - "common_pains_ar": [ - "قاعدة عملاء واتساب غير منظمة", - "خطر حظر رقم واتساب من الإفراط", - "leads تأتي بدون مصدر واضح", - ], - "winning_offer_ar": "List Intelligence + WhatsApp Compliance Setup.", - }, - "healthcare_local": { - "label_ar": "العيادات والخدمات المحلية", - "primary_services": [ - "local_growth_os", - "whatsapp_compliance_setup", - "list_intelligence", - ], - "supporting_services": [ - "growth_os_monthly", - ], - "buyer_roles": ["clinic_manager", "founder_ceo", "operations_manager"], - "common_pains_ar": [ - "Reviews سلبية على Google Business", - "no-show عالي بدون متابعة", - "Reactivation للعملاء القدامى", - ], - "winning_offer_ar": "Local Growth OS لإدارة Reviews + WhatsApp inbound.", - }, - "retail_ecommerce": { - "label_ar": "التجزئة والـ E-commerce", - "primary_services": [ - "list_intelligence", - "whatsapp_compliance_setup", - "local_growth_os", - ], - "supporting_services": [ - "growth_os_monthly", - "executive_growth_brief", - ], - "buyer_roles": ["founder_ceo", "store_manager", "marketing_manager"], - "common_pains_ar": [ - "Customer reactivation متعب يدوياً", - "Reviews + reputation متفرقة", - "Payment link sharing غير منظم", - ], - "winning_offer_ar": "List Intelligence + Local Growth OS + Moyasar invoice flow.", - }, -} - -def list_verticals() -> dict[str, Any]: - """Return all verticals with their full service stacks.""" +def build_vertical_service_map() -> dict[str, Any]: return { - "total": len(VERTICALS_AR), + "doors": [ + { + "door_id": "more_customers", + "title_ar": "أريد عملاء أكثر", + "service_ids": [ + "first_10_opportunities", + "growth_os", + "linkedin_lead_gen_setup", + "meeting_booking_sprint", + ], + }, + { + "door_id": "use_my_data", + "title_ar": "عندي بيانات وأريد أستفيد منها", + "service_ids": [ + "list_intelligence", + "email_revenue_rescue", + "whatsapp_compliance_setup", + "free_growth_diagnostic", + ], + }, + { + "door_id": "scale_strategy", + "title_ar": "أريد توسع وشراكات", + "service_ids": [ + "partner_sprint", + "agency_partner_program", + "executive_growth_brief", + "self_growth_operator", + ], + }, + ], "verticals": [ - {"id": vid, **vdata} for vid, vdata in VERTICALS_AR.items() + {"id": "agency", "label_ar": "وكالات", "priority_services": ["agency_partner_program", "first_10_opportunities"]}, + {"id": "training", "label_ar": "تدريب واستشارات", "priority_services": ["first_10_opportunities", "meeting_booking_sprint"]}, + {"id": "saas", "label_ar": "SaaS صغير", "priority_services": ["list_intelligence", "growth_os"]}, + {"id": "local", "label_ar": "محلي (عيادات/متاجر)", "priority_services": ["local_growth_os", "whatsapp_compliance_setup"]}, ], + "demo": True, } - - -def recommend_services_for_vertical(vertical_id: str) -> dict[str, Any]: - """Recommend the service stack for a given vertical.""" - v = VERTICALS_AR.get(vertical_id) - if v is None: - return { - "error": f"unknown vertical: {vertical_id}", - "available_verticals": list(VERTICALS_AR.keys()), - } - return { - "vertical_id": vertical_id, - "label_ar": v["label_ar"], - "primary_services": list(v["primary_services"]), - "supporting_services": list(v["supporting_services"]), - "buyer_roles": list(v["buyer_roles"]), - "common_pains_ar": list(v["common_pains_ar"]), - "winning_offer_ar": v["winning_offer_ar"], - } - - -def map_industry_to_vertical(industry: str) -> str: - """Best-effort mapping from a free-text industry → known vertical_id.""" - s = (industry or "").lower().strip() - if any(k in s for k in ("saas", "software", "tech", "تقنية", "برمجيات")): - return "b2b_saas" - if any(k in s for k in ("agency", "وكالة", "marketing", "تسويق")): - return "agencies" - if any(k in s for k in ("training", "تدريب", "consult", "استشار")): - return "training_consulting" - if any(k in s for k in ("real estate", "عقار", "property", "broker")): - return "real_estate" - if any(k in s for k in ("clinic", "عيادة", "doctor", "health", "medical")): - return "healthcare_local" - if any(k in s for k in ("retail", "store", "متجر", "shop", "ecommerce", "تجزئة")): - return "retail_ecommerce" - return "b2b_saas" # safe default diff --git a/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py b/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py index c62a4221..63695446 100644 --- a/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py +++ b/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py @@ -1,88 +1,90 @@ -"""WhatsApp CEO Control — كل القرارات بكروت عربية ≤3 أزرار.""" +"""Arabic CEO / growth manager cards — max 3 buttons, approval flags.""" from __future__ import annotations from typing import Any -from .service_catalog import get_service +from auto_client_acquisition.service_tower.service_catalog import list_tower_services + + +def _card( + title_ar: str, + summary_ar: str, + buttons: list[str], + approval_required: bool, +) -> dict[str, Any]: + btns = (buttons or [])[:3] + return { + "title_ar": title_ar, + "summary_ar": summary_ar, + "buttons": btns, + "approval_required": approval_required, + "live_send": False, + } def build_ceo_daily_service_brief() -> dict[str, Any]: - """The daily service brief sent to the CEO via WhatsApp/Email.""" + data = list_tower_services() + n = int(data.get("count") or 0) return { - "type": "ceo_daily_service_brief", - "title_ar": "موجز الخدمات اليومي", - "summary_ar": [ - "3 خدمات نشطة اليوم.", - "5 رسائل drafts تنتظر اعتمادك.", - "2 Free Diagnostic مكتمل وينتظر التسليم.", - "1 شريك وكالة جاهز للعرض.", - "0 مخاطر سمعة (الحالة صحية).", + "greeting_ar": "صباح الخير — موجز خدمات Dealix.", + "highlights_ar": [ + f"عدد الخدمات في البرج: {n}.", + "٣ مسودات بانتظار موافقتك (تجريبي).", + "لا إرسال حي من النظام افتراضياً.", ], - "buttons_ar": ["اعرض المسودات", "موافقة جماعية", "لاحقاً"], - "approval_required": True, + "cards": [ + _card( + "اعتماد مسودات", + "هناك مسودات جاهزة للمراجعة قبل أي تواصل خارجي.", + ["اعرض المسودات", "لاحقاً", "تخطي"], + True, + ), + _card( + "مخاطر قناة", + "قناة واحدة تحتاج تهدئة حسب سمعة الإرسال (تجريبي).", + ["افتح التفاصيل", "خفّض الحجم", "تجاهل"], + True, + ), + ], + "demo": True, } -def build_service_approval_card( - service_id: str, action: str, -) -> dict[str, Any]: - """Approval card for a single service action (draft send / publish / charge).""" - s = get_service(service_id) - if s is None: - return {"error": f"unknown service: {service_id}"} - label_ar_by_action = { - "send_email": "إرسال إيميل", - "send_whatsapp": "إرسال واتساب", - "insert_calendar": "إدراج موعد", - "create_payment_link": "إنشاء رابط دفع", - "publish_review_reply": "نشر رد تقييم", - "share_diagnostic": "مشاركة Free Diagnostic", - } +def build_service_approval_card(service_id: str, action: str) -> dict[str, Any]: return { - "type": "service_approval", "service_id": service_id, - "service_name_ar": s.name_ar, "action": action, - "title_ar": f"اعتماد: {label_ar_by_action.get(action, action)}", - "summary_ar": f"يتم تنفيذ هذا الفعل ضمن خدمة {s.name_ar}.", - "risk_level": s.risk_level, - "buttons_ar": ["اعتمد", "عدّل", "ارفض"], - "approval_required": True, - "live_send_allowed": False, + "card": _card( + f"موافقة: {service_id}", + f"الإجراء المقترح: {action} — لن يُنفَّذ إلا بعد اعتمادك.", + ["اعتمد", "عدّل", "ألغِ"], + True, + ), + "demo": True, } def build_risk_alert_card() -> dict[str, Any]: - """A risk alert card surfaced to the CEO.""" return { - "type": "risk_alert", - "title_ar": "تنبيه مخاطر", - "summary_ar": ( - "ارتفاع نسبة الـ bounce على الإيميل تجاوز الحد الآمن. " - "اقتراح: إيقاف الحملات الجديدة 14 يوماً + تنظيف القائمة." + "card": _card( + "تنبيه مخاطر", + "تم رصد أرقام بحاجة مراجعة مصدر قبل واتساب.", + ["راجع القائمة", "صدّر الممنوع", "لاحقاً"], + True, ), - "risk_level": "high", - "buttons_ar": ["أوقف القناة", "خفّض الحجم", "تجاهل"], - "approval_required": True, + "demo": True, } def build_end_of_day_service_report() -> dict[str, Any]: - """End-of-day report on services run today.""" return { - "type": "end_of_day_service_report", "title_ar": "تقرير نهاية اليوم — الخدمات", - "summary_ar": [ - "خدمات منفذة اليوم: 3.", - "Drafts معتمدة: 6.", - "ردود إيجابية: 2.", - "اجتماعات مجدولة: 1.", - "Pipeline متأثر: 24,000 ريال.", - "مخاطر تم منعها: 8.", + "lines_ar": [ + "المسودات المعتمدة: ٢ (تجريبي).", + "الاجتماعات المقترحة: ١.", + "المخاطر التي تم منعها: ٤.", ], - "next_day_focus_ar": ( - "غداً: تابع الردود الإيجابية، اعتمد رسائل Partner Sprint، " - "سلّم 2 Free Diagnostic للعملاء الجدد." - ), + "live_send": False, + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/__init__.py b/dealix/auto_client_acquisition/targeting_os/__init__.py index 9bac23d3..ab12d17d 100644 --- a/dealix/auto_client_acquisition/targeting_os/__init__.py +++ b/dealix/auto_client_acquisition/targeting_os/__init__.py @@ -1,177 +1,33 @@ -"""Targeting & Acquisition OS — يستهدف بذكاء، يقيّم المخاطر، يقترح القنوات. +"""Targeting & Acquisition OS — compliant account targeting and outreach planning.""" -Account-first targeting (شركات قبل أشخاص) + buying-committee mapping + -contactability gate + multi-channel strategy + reputation guard + -daily autopilot + self-growth mode + free diagnostic + contract drafts. - -كل شيء deterministic، عربي، draft/approval-first، لا scraping ولا cold WA. -""" - -from __future__ import annotations - -from .account_finder import ( - AccountSignal, - explain_why_now, - rank_accounts, - recommend_account_source_strategy, - recommend_accounts, - score_account_fit, -) -from .acquisition_scorecard import ( - build_acquisition_scorecard, - calculate_meetings_booked, - calculate_pipeline_created, - calculate_productivity_score, - calculate_risks_blocked, -) -from .buyer_role_mapper import ( - ALL_BUYER_ROLES, - draft_role_based_angle, - map_buying_committee, - recommend_decision_maker_roles, - recommend_influencer_roles, -) -from .contact_source_policy import ( - ALL_SOURCES, - allowed_channels_for_source, - classify_source, - required_review_level, - retention_recommendation, - source_risk_score, -) -from .contactability_matrix import ( - ACTION_MODES, - BLOCK_REASONS, - allowed_action_modes, - block_reason_codes, - evaluate_contactability, - explain_contactability_ar, -) -from .contract_drafts import ( - draft_agency_partner_outline, - draft_dpa_outline, - draft_pilot_agreement_outline, - draft_referral_agreement_outline, - draft_scope_of_work, -) -from .daily_autopilot import ( - build_daily_targeting_brief, - build_end_of_day_report, - prioritize_cards, - recommend_today_actions, -) -from .email_strategy import ( - build_followup_sequence, - draft_b2b_email, - include_unsubscribe_footer, - recommend_pacing, - score_email_risk, -) -from .free_diagnostic import ( - analyze_uploaded_list_preview, - build_free_growth_diagnostic, - build_mini_proof_plan, - recommend_paid_pilot_offer, -) -from .linkedin_strategy import ( - build_lead_gen_form_plan, - build_manual_research_task, - build_safe_connection_message, - linkedin_do_not_do, - recommend_linkedin_strategy, -) -from .outreach_scheduler import ( - build_outreach_plan, - enforce_daily_limits, - schedule_followups, - stop_on_opt_out, - summarize_plan_ar, -) -from .reputation_guard import ( - calculate_channel_reputation, - recommend_recovery_action, - risk_thresholds, - should_pause_channel, - summarize_reputation_ar, -) -from .self_growth_mode import ( - build_dealix_self_growth_plan, - build_free_service_offer, - build_self_growth_daily_brief, - build_weekly_learning_report, - recommend_dealix_targets, -) -from .service_offers import ( - build_offer_card, - estimate_service_price, - list_targeting_services, - recommend_service_offer, -) -from .social_strategy import ( - build_social_listening_plan, - draft_public_reply, - recommend_social_sources, - social_do_not_do, -) -from .whatsapp_strategy import ( - build_opt_in_request_template, - draft_whatsapp_message, - requires_opt_in, - score_whatsapp_risk, - whatsapp_do_not_do, -) +from auto_client_acquisition.targeting_os.account_finder import explain_why_now, rank_accounts, recommend_accounts +from auto_client_acquisition.targeting_os.acquisition_scorecard import build_acquisition_scorecard +from auto_client_acquisition.targeting_os.buyer_role_mapper import map_buying_committee +from auto_client_acquisition.targeting_os.contactability_matrix import evaluate_contactability +from auto_client_acquisition.targeting_os.contact_source_policy import classify_source +from auto_client_acquisition.targeting_os.daily_autopilot import build_daily_targeting_brief +from auto_client_acquisition.targeting_os.free_diagnostic import build_free_growth_diagnostic +from auto_client_acquisition.targeting_os.linkedin_strategy import recommend_linkedin_strategy +from auto_client_acquisition.targeting_os.outreach_scheduler import build_outreach_plan, summarize_plan_ar +from auto_client_acquisition.targeting_os.reputation_guard import calculate_channel_reputation, should_pause_channel +from auto_client_acquisition.targeting_os.self_growth_mode import build_self_growth_daily_brief +from auto_client_acquisition.targeting_os.service_offers import list_targeting_services __all__ = [ - # account_finder - "AccountSignal", "explain_why_now", "rank_accounts", - "recommend_account_source_strategy", "recommend_accounts", "score_account_fit", - # acquisition_scorecard - "build_acquisition_scorecard", "calculate_meetings_booked", - "calculate_pipeline_created", "calculate_productivity_score", - "calculate_risks_blocked", - # buyer_role_mapper - "ALL_BUYER_ROLES", "draft_role_based_angle", "map_buying_committee", - "recommend_decision_maker_roles", "recommend_influencer_roles", - # contact_source_policy - "ALL_SOURCES", "allowed_channels_for_source", "classify_source", - "required_review_level", "retention_recommendation", "source_risk_score", - # contactability_matrix - "ACTION_MODES", "BLOCK_REASONS", "allowed_action_modes", - "block_reason_codes", "evaluate_contactability", "explain_contactability_ar", - # contract_drafts - "draft_agency_partner_outline", "draft_dpa_outline", - "draft_pilot_agreement_outline", "draft_referral_agreement_outline", - "draft_scope_of_work", - # daily_autopilot - "build_daily_targeting_brief", "build_end_of_day_report", - "prioritize_cards", "recommend_today_actions", - # email_strategy - "build_followup_sequence", "draft_b2b_email", - "include_unsubscribe_footer", "recommend_pacing", "score_email_risk", - # free_diagnostic - "analyze_uploaded_list_preview", "build_free_growth_diagnostic", - "build_mini_proof_plan", "recommend_paid_pilot_offer", - # linkedin_strategy - "build_lead_gen_form_plan", "build_manual_research_task", - "build_safe_connection_message", "linkedin_do_not_do", + "build_acquisition_scorecard", + "build_daily_targeting_brief", + "build_free_growth_diagnostic", + "build_outreach_plan", + "build_self_growth_daily_brief", + "calculate_channel_reputation", + "classify_source", + "evaluate_contactability", + "explain_why_now", + "list_targeting_services", + "map_buying_committee", + "rank_accounts", + "recommend_accounts", "recommend_linkedin_strategy", - # outreach_scheduler - "build_outreach_plan", "enforce_daily_limits", - "schedule_followups", "stop_on_opt_out", "summarize_plan_ar", - # reputation_guard - "calculate_channel_reputation", "recommend_recovery_action", - "risk_thresholds", "should_pause_channel", "summarize_reputation_ar", - # self_growth_mode - "build_dealix_self_growth_plan", "build_free_service_offer", - "build_self_growth_daily_brief", "build_weekly_learning_report", - "recommend_dealix_targets", - # service_offers - "build_offer_card", "estimate_service_price", - "list_targeting_services", "recommend_service_offer", - # social_strategy - "build_social_listening_plan", "draft_public_reply", - "recommend_social_sources", "social_do_not_do", - # whatsapp_strategy - "build_opt_in_request_template", "draft_whatsapp_message", - "requires_opt_in", "score_whatsapp_risk", "whatsapp_do_not_do", + "should_pause_channel", + "summarize_plan_ar", ] diff --git a/dealix/auto_client_acquisition/targeting_os/account_finder.py b/dealix/auto_client_acquisition/targeting_os/account_finder.py index 761c6e67..f4890e8d 100644 --- a/dealix/auto_client_acquisition/targeting_os/account_finder.py +++ b/dealix/auto_client_acquisition/targeting_os/account_finder.py @@ -1,215 +1,88 @@ -"""Account-first targeting — يبحث عن الشركات المناسبة قبل الأشخاص.""" +"""Recommend target accounts from sector/city/goal — deterministic demo accounts.""" from __future__ import annotations -from dataclasses import dataclass from typing import Any -# Signals that indicate a company is "in market" right now. -ACCOUNT_SIGNALS_AR: dict[str, str] = { - "hiring_sales": "توظيف مبيعات", - "new_branch": "فرع جديد", - "website_updated": "تحديث الموقع", - "active_ads": "إعلانات نشطة", - "event_participation": "مشاركة في فعاليات", - "google_reviews": "تقييمات Google نشطة", - "booking_link": "صفحة حجز/طلب", - "crm_visible": "بيانات CRM متوفرة", - "growing_team": "نمو الفريق", - "partner_potential": "إمكانية شراكة", - "expansion_news": "أخبار توسع", - "leadership_change": "تغيير قيادي", -} - - -@dataclass(frozen=True) -class AccountSignal: - """A single buying-readiness signal on a company.""" - key: str - label_ar: str - weight: int # 1..10 - why_ar: str - - def to_dict(self) -> dict[str, object]: - return { - "key": self.key, "label_ar": self.label_ar, - "weight": self.weight, "why_ar": self.why_ar, - } - - -# Default signal weights — can be overridden per sector. -_DEFAULT_WEIGHTS: dict[str, int] = { - "hiring_sales": 9, - "new_branch": 8, - "expansion_news": 9, - "active_ads": 7, - "growing_team": 7, - "leadership_change": 8, - "booking_link": 5, - "website_updated": 4, - "google_reviews": 5, - "crm_visible": 3, - "event_participation": 6, - "partner_potential": 6, -} - - -def _signal_objs(signals: dict[str, bool] | list[str]) -> list[AccountSignal]: - out: list[AccountSignal] = [] - if isinstance(signals, list): - signals = {s: True for s in signals} - for key, val in signals.items(): - if not val or key not in ACCOUNT_SIGNALS_AR: - continue - out.append(AccountSignal( - key=key, - label_ar=ACCOUNT_SIGNALS_AR[key], - weight=_DEFAULT_WEIGHTS.get(key, 3), - why_ar=f"إشارة: {ACCOUNT_SIGNALS_AR[key]}", - )) - return out - - -def score_account_fit(account: dict[str, Any]) -> dict[str, Any]: - """Score an account 0..100 based on its signals + sector+size match.""" - signals = _signal_objs(account.get("signals", {})) - base = sum(s.weight for s in signals) - score = min(100, base * 4) # ~25 weight points = max 100 - if account.get("sector_match"): - score = min(100, score + 10) - if account.get("city_match"): - score = min(100, score + 5) - - if score >= 70: - tier = "hot" - elif score >= 40: - tier = "warm" - elif score >= 15: - tier = "watching" - else: - tier = "cold" - - return { - "score": score, - "tier": tier, - "signals": [s.to_dict() for s in signals], - "signal_count": len(signals), - } - - -def explain_why_now(account: dict[str, Any]) -> str: - """Build an Arabic 'why now' line from an account's signals.""" - signals = _signal_objs(account.get("signals", {})) - if not signals: - return "لا توجد إشارات شراء واضحة الآن — متابعة دورية مقترحة." - top = sorted(signals, key=lambda s: -s.weight)[:2] - labels = " + ".join(s.label_ar for s in top) - company = account.get("name") or "الشركة" - return f"{company} تظهر إشارات: {labels}. نافذة فرصة مناسبة الآن." - - -def recommend_account_source_strategy(account: dict[str, Any]) -> dict[str, Any]: - """Recommend safe sources for reaching this account's decision-makers.""" - has_crm = bool(account.get("crm_visible")) - has_ads = bool(account.get("active_ads")) - has_events = bool(account.get("event_participation")) - - primary = [] - if has_crm: - primary.append("crm_customer") - primary.append("website_form") - primary.append("linkedin_lead_form") - - if has_ads: - primary.append("ads_retargeting") - if has_events: - primary.append("event_lead") - - return { - "primary_sources": primary, - "blocked_sources": ["scraped_email", "scraped_phone", "purchased_list"], - "notes_ar": ( - "ابدأ بمصادر مصرّح بها: قوائم العميل، Lead Gen Forms، " - "نماذج الموقع، شركاء، أحداث. لا scraping ولا قوائم مشتراة." - ), - } +_SIGNALS = ( + "hiring_sales", + "website_updated", + "google_reviews_active", + "booking_link", + "growing_team", +) def recommend_accounts( sector: str, city: str, + offer: str, + goal: str, *, - offer: str = "", - goal: str = "fill_pipeline", limit: int = 10, - seed_signals: list[str] | None = None, ) -> dict[str, Any]: - """ - Generate a deterministic list of recommended target accounts. - - This is a structural template — production reads from real data sources - (Google Maps, CRM, web forms, etc). The output shape stays identical. - """ - seed_signals = seed_signals or [ - "hiring_sales", "new_branch", "active_ads", - "growing_team", "booking_link", "google_reviews", + sector_ar = sector or "خدمات B2B" + city_ar = city or "الرياض" + base = [ + { + "company": f"شركة ألفا — {sector_ar}", + "city": city_ar, + "fit_score": 88, + "why_now_ar": "إعلان وظائف مبيعات + صفحة خدمات محدثة.", + "best_channel": "email_first", + "risk_level": "low", + "signals": ["hiring_sales", "website_updated"], + }, + { + "company": f"مؤسسة بيتا — {sector_ar}", + "city": city_ar, + "fit_score": 82, + "why_now_ar": "تقييمات Google نشطة — فرصة سمعة محلية.", + "best_channel": "google_business_draft", + "risk_level": "low", + "signals": ["google_reviews_active", "booking_link"], + }, + { + "company": f"مجموعة جاما — {sector_ar}", + "city": "جدة", + "fit_score": 76, + "why_now_ar": "توسع فريق — احتمال شراء أدوات نمو.", + "best_channel": "linkedin_lead_form", + "risk_level": "medium", + "signals": ["growing_team"], + }, ] - sector_label_ar = { - "training": "التدريب", "saas": "البرمجيات", "real_estate": "العقار", - "retail": "التجزئة", "healthcare": "الرعاية الصحية", - "logistics": "اللوجستيات", "fintech": "الفنتك", - "agency": "الوكالات", "education": "التعليم", - }.get(sector.lower(), sector) + accounts = [] + for i in range(max(1, min(limit, 20))): + a = dict(base[i % len(base)]) + a["id"] = f"acct_demo_{i+1}" + a["company"] = f"{a['company']} ({i+1})" + a["offer_fit_ar"] = f"العرض «{offer or 'Growth OS'}» مناسب لهدف «{goal or 'نمو'}»." + accounts.append(a) + return {"accounts": accounts[:limit], "count": len(accounts[:limit]), "demo": True} - accounts: list[dict[str, Any]] = [] - n = max(1, min(limit, 25)) - for i in range(n): - # Spread signals across accounts deterministically. - my_signals = {seed_signals[(i + j) % len(seed_signals)]: True - for j in range(2 + (i % 3))} - acct = { - "name": f"شركة {sector_label_ar} #{i + 1} في {city}", - "sector": sector, - "city": city, - "signals": my_signals, - "sector_match": True, - "city_match": True, - } - scored = score_account_fit(acct) - sources = recommend_account_source_strategy(acct) - acct.update({ - "fit_score": scored["score"], - "tier": scored["tier"], - "why_now_ar": explain_why_now(acct), - "primary_sources": sources["primary_sources"], - "best_angle_ar": ( - f"عرض Pilot 7 أيام لاستخراج 10 فرص في قطاع {sector_label_ar}." - if not offer else - f"العرض المقترح: {offer}." - ), - "recommended_channel": ( - "email_first" - if "crm_visible" in my_signals - else "linkedin_lead_form_first" - ), - "risk_level": "low" if scored["score"] >= 50 else "medium", - }) - accounts.append(acct) - accounts = rank_accounts(accounts) +def score_account_fit(account: dict[str, Any]) -> int: + return int(account.get("fit_score") or 70) + + +def explain_why_now(account: dict[str, Any]) -> str: + return str(account.get("why_now_ar") or "إشارات سوق عامة — راجع التفاصيل قبل التواصل.") + + +def recommend_account_source_strategy(account: dict[str, Any]) -> dict[str, Any]: + ch = str(account.get("best_channel") or "email_first") return { - "sector": sector, "city": city, "goal": goal, "offer": offer, - "total": len(accounts), - "accounts": accounts, - "do_not_do_ar": [ - "لا scraping للبيانات.", - "لا cold WhatsApp.", - "لا auto-DM على LinkedIn.", - "لا charge بدون موافقة.", + "account_id": account.get("id"), + "recommended_first_touch": ch, + "steps_ar": [ + "تحقق من المصدر والـ opt-in.", + "جهّز مسودة بريد عبر المنصة.", + "لا واتساب بارد بدون علاقة.", ], + "demo": True, } def rank_accounts(accounts: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Sort accounts by fit_score desc; stable for ties.""" - return sorted(accounts, key=lambda a: -int(a.get("fit_score", 0))) + return sorted(accounts, key=lambda x: -score_account_fit(x)) diff --git a/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py b/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py index a0a3df7f..2e799de3 100644 --- a/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py +++ b/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py @@ -1,86 +1,39 @@ -"""Acquisition scorecard — يقيس النتائج بشكل deterministic.""" +"""Acquisition metrics snapshot — deterministic.""" from __future__ import annotations from typing import Any -def calculate_pipeline_created(opportunities: list[dict[str, Any]]) -> dict[str, Any]: - """Sum expected_value_sar across opportunities.""" - total = sum(float(o.get("expected_value_sar", 0)) for o in opportunities or []) - return { - "opportunities_count": len(opportunities or []), - "pipeline_sar": round(total, 2), - } +def calculate_pipeline_created(opportunities: list[dict[str, Any]]) -> int: + total = 0 + for o in opportunities: + total += int(o.get("expected_value_sar") or o.get("expected_impact_sar") or 5000) + return total -def calculate_meetings_booked(events: list[dict[str, Any]]) -> dict[str, Any]: - """Count meetings by status.""" - drafted = sum(1 for e in events or [] if e.get("status") == "drafted") - confirmed = sum(1 for e in events or [] if e.get("status") == "confirmed") - completed = sum(1 for e in events or [] if e.get("status") == "completed") - return { - "drafted": drafted, "confirmed": confirmed, "completed": completed, - "total": drafted + confirmed + completed, - } +def calculate_meetings_booked(events: list[dict[str, Any]]) -> int: + return sum(1 for e in events if e.get("type") == "meeting_booked") -def calculate_risks_blocked(actions: list[dict[str, Any]]) -> dict[str, Any]: - """Count actions that were blocked by policy/contactability.""" - blocked = [a for a in actions or [] if a.get("status") == "blocked"] - by_reason: dict[str, int] = {} - for a in blocked: - reason = a.get("block_reason", "unknown") - by_reason[reason] = by_reason.get(reason, 0) + 1 - return {"total": len(blocked), "by_reason": by_reason} +def calculate_risks_blocked(actions: list[dict[str, Any]]) -> int: + return sum(1 for a in actions if a.get("outcome") == "blocked") -def calculate_productivity_score(metrics: dict[str, Any]) -> dict[str, Any]: - """Compute a productivity score 0..100 from key acquisition metrics.""" - accounts = int(metrics.get("accounts_researched", 0)) - drafts = int(metrics.get("drafts_created", 0)) - approvals = int(metrics.get("approvals_received", 0)) - replies = int(metrics.get("positive_replies", 0)) - meetings = int(metrics.get("meetings_booked", 0)) - - score = 0 - score += min(20, accounts // 3) - score += min(20, drafts * 2) - score += min(20, approvals * 4) - score += min(20, replies * 5) - score += min(20, meetings * 8) - score = max(0, min(100, score)) - - if score >= 70: - verdict = "strong" - elif score >= 40: - verdict = "decent" - else: - verdict = "needs_focus" - - return {"score": score, "verdict": verdict} +def calculate_productivity_score(metrics: dict[str, Any]) -> int: + base = 50 + base += min(30, int(metrics.get("drafts_approved", 0)) * 3) + base += min(20, int(metrics.get("meetings_booked", 0)) * 5) + return max(0, min(100, base)) def build_acquisition_scorecard(metrics: dict[str, Any]) -> dict[str, Any]: - """Build a comprehensive Arabic acquisition scorecard.""" - pipeline = calculate_pipeline_created(metrics.get("opportunities", [])) - meetings = calculate_meetings_booked(metrics.get("events", [])) - risks = calculate_risks_blocked(metrics.get("actions", [])) - productivity = calculate_productivity_score(metrics) - return { - "summary_ar": [ - f"الحسابات المُحلّلة: {metrics.get('accounts_researched', 0)}", - f"أصحاب القرار المُعرَّفين: {metrics.get('decision_makers_mapped', 0)}", - f"رسائل drafts: {metrics.get('drafts_created', 0)}", - f"اعتمادات: {metrics.get('approvals_received', 0)}", - f"ردود إيجابية: {metrics.get('positive_replies', 0)}", - f"اجتماعات: {meetings['total']}", - f"Pipeline متأثر: {pipeline['pipeline_sar']:.0f} ريال", - f"مخاطر تم منعها: {risks['total']}", - ], - "pipeline": pipeline, - "meetings": meetings, - "risks_blocked": risks, - "productivity_score": productivity, + "leads_created": metrics.get("leads_created", 0), + "meetings_booked": metrics.get("meetings_booked", 0), + "drafts_approved": metrics.get("drafts_approved", 0), + "risks_blocked": metrics.get("risks_blocked", 0), + "pipeline_created_sar": metrics.get("pipeline_created_sar", 0), + "productivity_score": calculate_productivity_score(metrics), + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py b/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py index 858a0b93..71ab2af0 100644 --- a/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py +++ b/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py @@ -1,151 +1,62 @@ -"""Map buying committees — من غالباً يقرر داخل الشركة.""" +"""Map sector/goal to buying committee roles — deterministic.""" from __future__ import annotations from typing import Any -# All buyer roles Dealix knows about, with Arabic labels. -ALL_BUYER_ROLES: dict[str, str] = { - "founder_ceo": "المؤسس / الرئيس التنفيذي", - "coo": "مدير العمليات", - "head_of_sales": "مدير المبيعات", - "marketing_manager": "مدير التسويق", - "business_development": "تطوير الأعمال", - "operations_manager": "مدير العمليات التشغيلية", - "clinic_manager": "مدير العيادة", - "branch_manager": "مدير الفرع", - "hr_manager": "مدير الموارد البشرية", - "procurement_manager": "مدير المشتريات", - "agency_owner": "صاحب الوكالة", - "store_manager": "مدير المتجر", - "growth_manager": "مدير النمو", - "cto": "المدير التقني", -} - -# Sector-specific decision-maker priors (descending priority). -_DM_BY_SECTOR: dict[str, list[str]] = { - "training": ["founder_ceo", "head_of_sales", "hr_manager"], - "saas": ["founder_ceo", "head_of_sales", "growth_manager"], - "real_estate": ["founder_ceo", "head_of_sales", "branch_manager"], - "retail": ["founder_ceo", "store_manager", "marketing_manager"], - "healthcare": ["clinic_manager", "founder_ceo", "operations_manager"], - "logistics": ["coo", "operations_manager", "founder_ceo"], - "fintech": ["founder_ceo", "growth_manager", "cto"], - "agency": ["agency_owner", "head_of_sales", "growth_manager"], - "education": ["founder_ceo", "operations_manager", "marketing_manager"], - "consulting": ["founder_ceo", "business_development", "head_of_sales"], -} - -_INFLUENCERS_BY_SECTOR: dict[str, list[str]] = { - "training": ["marketing_manager", "operations_manager"], - "saas": ["marketing_manager", "cto"], - "real_estate": ["marketing_manager"], - "retail": ["operations_manager"], - "healthcare": ["marketing_manager", "operations_manager"], - "logistics": ["procurement_manager"], - "fintech": ["marketing_manager", "head_of_sales"], - "agency": ["marketing_manager", "business_development"], - "education": ["hr_manager"], - "consulting": ["marketing_manager"], -} - -# Goal-based message angles per role. -_ROLE_ANGLES_AR: dict[str, str] = { - "founder_ceo": "نمو إيرادات ملموس بدون توظيف فريق كبير.", - "coo": "تنظيم العمليات وقياس الأثر يومياً.", - "head_of_sales": "ملء الـ pipeline بفرص مؤهلة + متابعة منظمة.", - "marketing_manager": "تحويل الـ traffic والإعلانات إلى اجتماعات.", - "business_development": "فتح قنوات شراكة وتوزيع جديدة.", - "operations_manager": "أتمتة المتابعات + تقليل الوقت الضائع.", - "clinic_manager": "تذكير المرضى + ردود التقييمات + قنوات حجز.", - "branch_manager": "إدارة عملاء الفرع + reactivation.", - "hr_manager": "برامج تدريب وتوظيف بدون فوضى inbox.", - "procurement_manager": "تقييم مزودين + التزامات SLA واضحة.", - "agency_owner": "خدمة عملاء الوكالة + Proof Pack + revenue share.", - "store_manager": "استرجاع العملاء + payment links + reviews.", - "growth_manager": "تجارب نمو منظمة + قياس Proof.", - "cto": "أمان البيانات + PDPL + تكاملات مصرّحة.", +_SECTOR_ROLES: dict[str, dict[str, list[str]]] = { + "training": { + "primary": ["Founder/CEO", "Head of Sales"], + "influencers": ["HR Manager", "Operations Manager"], + }, + "saas": { + "primary": ["Founder/CEO", "Procurement Manager"], + "influencers": ["IT Manager", "Head of Sales"], + }, + "clinics": { + "primary": ["Clinic Manager", "Founder/CEO"], + "influencers": ["Operations Manager", "HR Manager"], + }, + "default": { + "primary": ["Founder/CEO", "Head of Sales"], + "influencers": ["Marketing Manager", "Business Development Manager"], + }, } -def _norm_sector(sector: str) -> str: - s = (sector or "").lower().strip() - return s if s in _DM_BY_SECTOR else "saas" - - -def map_buying_committee( - sector: str, - *, - company_size: str = "small", - goal: str = "fill_pipeline", -) -> dict[str, Any]: - """Build a buying-committee map for a sector + company-size.""" - s = _norm_sector(sector) - dm_keys = _DM_BY_SECTOR[s] - inf_keys = _INFLUENCERS_BY_SECTOR[s] - - # For small companies, the founder is almost always the primary DM. - if company_size in ("micro", "small") and "founder_ceo" not in dm_keys[:2]: - dm_keys = ["founder_ceo"] + [k for k in dm_keys if k != "founder_ceo"] - +def map_buying_committee(sector: str, company_size: str | None, goal: str | None) -> dict[str, Any]: + key = (sector or "").strip().lower() or "default" + if key not in _SECTOR_ROLES: + key = "default" + roles = _SECTOR_ROLES[key] + size = (company_size or "smb").lower() + g = (goal or "book_more_b2b_meetings").lower() + note = "شركة أكبر: أضف Procurement" if size in ("enterprise", "large") else "تركيز على Founder/Head of Sales في SMB." + if "partner" in g: + note += " هدف شراكة: أضف Agency Owner كمؤثر." return { - "sector": s, - "company_size": company_size, - "goal": goal, - "primary_decision_maker": { - "role_key": dm_keys[0], - "label_ar": ALL_BUYER_ROLES[dm_keys[0]], - "angle_ar": _ROLE_ANGLES_AR[dm_keys[0]], - }, - "secondary_decision_makers": [ - {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], - "angle_ar": _ROLE_ANGLES_AR[k]} - for k in dm_keys[1:] - ], - "influencers": [ - {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], - "angle_ar": _ROLE_ANGLES_AR[k]} - for k in inf_keys - ], - "approach_notes_ar": ( - "ابدأ بمحاور أعلى — المؤسس أو مدير المبيعات. " - "اشمل الـ influencers في الرسالة الثانية لبناء التوافق الداخلي." - ), + "sector": sector or "unknown", + "company_size": size, + "goal": g, + "primary_decision_makers": roles["primary"], + "influencers": roles["influencers"], + "note_ar": note, + "demo": True, } -def recommend_decision_maker_roles( - sector: str, *, goal: str = "fill_pipeline", -) -> list[dict[str, str]]: - s = _norm_sector(sector) - return [ - {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], - "angle_ar": _ROLE_ANGLES_AR[k]} - for k in _DM_BY_SECTOR[s] - ] +def recommend_decision_maker_roles(sector: str, goal: str | None) -> list[str]: + return list(map_buying_committee(sector, None, goal)["primary_decision_makers"]) -def recommend_influencer_roles( - sector: str, *, goal: str = "fill_pipeline", -) -> list[dict[str, str]]: - s = _norm_sector(sector) - return [ - {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], - "angle_ar": _ROLE_ANGLES_AR[k]} - for k in _INFLUENCERS_BY_SECTOR[s] - ] +def recommend_influencer_roles(sector: str, goal: str | None) -> list[str]: + return list(map_buying_committee(sector, None, goal)["influencers"]) -def draft_role_based_angle( - role_key: str, *, sector: str = "saas", offer: str = "", -) -> dict[str, str]: - """Build a one-sentence Arabic angle suited to a role.""" - role_key = role_key if role_key in ALL_BUYER_ROLES else "founder_ceo" - role_ar = ALL_BUYER_ROLES[role_key] - base_angle = _ROLE_ANGLES_AR[role_key] - offer_part = f" — {offer}" if offer else "" +def draft_role_based_angle(role: str, sector: str, offer: str) -> dict[str, Any]: return { - "role_key": role_key, - "role_ar": role_ar, - "angle_ar": f"رسالة لـ{role_ar}: {base_angle}{offer_part}", + "role": role, + "sector": sector, + "angle_ar": f"نربط «{offer}» بأثر مباشر على {role}: وقت أقل، صفقات أوضح، متابعة موثّقة.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py b/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py index 2bfc463b..7a267dcd 100644 --- a/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py +++ b/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py @@ -1,9 +1,10 @@ -"""Contact source policy — كل contact له مصدر، غرض، ومستوى مخاطرة.""" +"""Classify lead/contact sources and allowed channels — policy only, no I/O.""" from __future__ import annotations -# All recognized contact sources, ordered roughly safest → riskiest. -ALL_SOURCES: tuple[str, ...] = ( +from typing import Any + +_SOURCE_ORDER = ( "crm_customer", "inbound_lead", "website_form", @@ -18,131 +19,79 @@ ALL_SOURCES: tuple[str, ...] = ( "opt_out", ) -# Risk score per source (0..100; higher = riskier). -_SOURCE_RISK: dict[str, int] = { - "crm_customer": 5, - "inbound_lead": 5, - "website_form": 10, - "linkedin_lead_form": 10, - "event_lead": 20, - "referral": 25, - "partner_intro": 25, - "manual_research": 50, - "uploaded_list": 60, - "unknown_source": 80, - "cold_list": 95, - "opt_out": 100, -} - -def classify_source(source: str) -> dict[str, object]: - """Classify a single source string. Unknown maps to `unknown_source`.""" - s = (source or "").lower().strip() - if s not in ALL_SOURCES: - s = "unknown_source" - return {"source": s, "risk_score": _SOURCE_RISK[s]} - - -def allowed_channels_for_source( - source: str, *, opt_in_status: str = "unknown", -) -> dict[str, object]: - """ - Return which channels Dealix may attempt for this source/opt-in combo. - - Each channel is "safe" / "needs_review" / "blocked". - """ - s = classify_source(source)["source"] - opt = (opt_in_status or "unknown").lower() - - if s == "opt_out": - return { - "source": s, - "channels": {ch: "blocked" for ch in - ("whatsapp", "email", "linkedin", "phone", "social_dm")}, - "notes_ar": "العميل سحب موافقته — كل القنوات محظورة.", - } - - safe_inbound = s in ("crm_customer", "inbound_lead", "website_form", - "linkedin_lead_form", "referral", "partner_intro") - is_unknown = s in ("unknown_source", "manual_research", "uploaded_list", - "cold_list") - - out: dict[str, str] = {} - # WhatsApp — strict - if opt == "yes" and not s == "cold_list": - out["whatsapp"] = "safe" - elif s == "inbound_lead" or s == "crm_customer": - out["whatsapp"] = "needs_review" - else: - out["whatsapp"] = "blocked" - - # Email — looser when business context exists - if safe_inbound: - out["email"] = "safe" - elif is_unknown: - out["email"] = "needs_review" - else: - out["email"] = "needs_review" - - # LinkedIn — only via lead forms / manual approved - if s == "linkedin_lead_form": - out["linkedin"] = "safe" - else: - out["linkedin"] = "needs_review" - - # Phone — heavy review - out["phone"] = "blocked" if s in ("cold_list", "unknown_source") else "needs_review" - - # Social DM — only with explicit context - out["social_dm"] = "blocked" if s in ("cold_list", "unknown_source") else "needs_review" - - return { - "source": s, - "opt_in_status": opt, - "channels": out, - "notes_ar": ( - "البريد افضل قناة في الغالب لمصادر العمل المعروفة. " - "واتساب يحتاج opt-in واضح. لينكدإن عبر Lead Forms فقط." - ), - } - - -def required_review_level(source: str) -> str: - """Returns: 'auto_safe' | 'human_review' | 'block'.""" - s = classify_source(source)["source"] - if s == "opt_out": - return "block" - if s in ("crm_customer", "inbound_lead", "website_form", - "linkedin_lead_form"): - return "auto_safe" - if s in ("event_lead", "referral", "partner_intro"): - return "human_review" - return "human_review" - - -def retention_recommendation(source: str) -> dict[str, object]: - """Return PDPL-shaped retention guidance per source.""" - s = classify_source(source)["source"] - if s == "crm_customer": - days = 365 * 3 # 3 years - elif s in ("inbound_lead", "website_form", "linkedin_lead_form", - "event_lead", "referral", "partner_intro"): - days = 365 * 2 - else: - days = 180 - return { - "source": s, - "retention_days": days, - "lawful_basis_ar": ( - "علاقة قائمة" if s == "crm_customer" - else "موافقة" if s in ("website_form", "linkedin_lead_form", - "inbound_lead", "event_lead") - else "مصلحة مشروعة محدودة" - ), - "notes_ar": "حذف تلقائي عند تجاوز المدة أو طلب opt-out.", - } +def classify_source(source: str | None) -> str: + s = (source or "").strip().lower().replace(" ", "_") + if s in ("opt_out", "optout"): + return "opt_out" + if s in _SOURCE_ORDER: + return s + if s in ("unknown", "", "none"): + return "unknown_source" + return "unknown_source" def source_risk_score(source: str) -> int: - """Return the integer risk score for the source.""" - return int(classify_source(source)["risk_score"]) + """0 = low risk, 100 = high risk (for sorting).""" + s = classify_source(source) + return { + "opt_out": 100, + "cold_list": 85, + "unknown_source": 70, + "uploaded_list": 55, + "manual_research": 40, + "referral": 35, + "partner_intro": 30, + "event_lead": 25, + "linkedin_lead_form": 20, + "website_form": 15, + "inbound_lead": 10, + "crm_customer": 10, + }.get(s, 65) + + +def allowed_channels_for_source(source: str, opt_in_status: str | None) -> list[str]: + s = classify_source(source) + opt = (opt_in_status or "").lower() + if s == "opt_out": + return [] + if s == "cold_list": + return ["email_draft_review"] if opt != "explicit" else ["email_draft_review", "linkedin_manual_task"] + if s == "unknown_source": + return ["email_draft_review", "internal_task"] + if s == "uploaded_list": + return ["email_draft_review", "internal_task"] + if s == "manual_research": + return ["email_draft_review", "linkedin_manual_task", "internal_task"] + if s in ("referral", "partner_intro"): + return ["email_draft_review", "whatsapp_draft_if_opt_in", "calendar_draft", "internal_task"] + if s in ("linkedin_lead_form", "website_form", "inbound_lead", "event_lead"): + return ["email_draft_review", "whatsapp_draft_if_opt_in", "calendar_draft", "internal_task"] + if s == "crm_customer": + return ["email_draft_review", "whatsapp_draft_if_opt_in", "calendar_draft", "payment_draft", "internal_task"] + return ["internal_task"] + + +def required_review_level(source: str) -> str: + s = classify_source(source) + if s in ("opt_out", "cold_list"): + return "blocked" + if s in ("unknown_source", "uploaded_list", "manual_research"): + return "human_review" + if s in ("referral", "partner_intro"): + return "light_review" + return "auto_ok_with_approval" + + +def retention_recommendation(source: str) -> dict[str, Any]: + s = classify_source(source) + days = {"opt_out": 0, "cold_list": 30, "unknown_source": 90}.get(s, 365) + return { + "source": s, + "suggested_retention_days": days, + "note_ar": "توصية MVP — راجع سياسة الاحتفاظ مع DPO قبل الإنتاج.", + } + + +def list_sources_reference() -> dict[str, Any]: + return {"sources": list(_SOURCE_ORDER), "demo": True} diff --git a/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py b/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py index 1560dc2c..8a8612e7 100644 --- a/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py +++ b/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py @@ -1,134 +1,140 @@ -"""Contactability matrix — هل التواصل مع هذا الـcontact مسموح؟""" +"""Contactability: safe / needs_review / blocked + action modes — no send.""" from __future__ import annotations from typing import Any -from .contact_source_policy import ( +from auto_client_acquisition.targeting_os.contact_source_policy import ( allowed_channels_for_source, classify_source, - source_risk_score, + required_review_level, ) -ACTION_MODES: tuple[str, ...] = ( - "suggest_only", - "draft_only", - "approval_required", - "approved_execute", - "blocked", -) - -BLOCK_REASONS: dict[str, str] = { - "opt_out": "العميل سحب موافقته.", - "cold_whatsapp": "واتساب بارد محظور (PDPL).", - "no_lawful_basis": "لا يوجد أساس نظامي للتواصل.", - "missing_consent": "موافقة opt-in مفقودة.", - "secret_in_payload": "الـ payload يحوي قيمة حساسة.", - "high_value_no_approval": "صفقة عالية القيمة بدون اعتماد.", - "channel_paused": "القناة موقوفة لتدهور السمعة.", - "frequency_cap_hit": "تجاوز سقف التواصل الأسبوعي.", - "unknown_source": "مصدر الـ contact غير معروف — تحتاج مراجعة.", -} - def block_reason_codes() -> dict[str, str]: - """Expose all block reason codes (Arabic).""" - return dict(BLOCK_REASONS) - - -def evaluate_contactability( - contact: dict[str, Any], - *, - desired_channel: str | None = None, -) -> dict[str, Any]: - """ - Evaluate whether contacting `contact` via `desired_channel` is permitted. - - Returns a structured verdict with status and Arabic reasons. - """ - source = contact.get("source", "unknown_source") - opt_in = contact.get("opt_in_status", "unknown") - opt_out = bool(contact.get("opt_out", False)) - has_relationship = bool(contact.get("has_relationship", False)) - - risk = source_risk_score(source) - classified = classify_source(source)["source"] - - if opt_out or classified == "opt_out": - return { - "status": "blocked", - "reason_codes": ["opt_out"], - "reasons_ar": [BLOCK_REASONS["opt_out"]], - "allowed_action_mode": "blocked", - "allowed_channels": [], - } - - channel_map = allowed_channels_for_source(source, opt_in_status=str(opt_in))["channels"] - - if desired_channel: - ch = desired_channel.lower() - ch_status = channel_map.get(ch, "blocked") - if ch_status == "blocked": - reason = "cold_whatsapp" if ch == "whatsapp" else "no_lawful_basis" - return { - "status": "blocked", - "reason_codes": [reason], - "reasons_ar": [BLOCK_REASONS[reason]], - "allowed_action_mode": "blocked", - "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], - } - if ch_status == "needs_review": - return { - "status": "needs_review", - "reason_codes": ["unknown_source"] if classified == "unknown_source" else [], - "reasons_ar": ( - [BLOCK_REASONS["unknown_source"]] if classified == "unknown_source" - else ["تحتاج مراجعة بشرية قبل الإرسال."] - ), - "allowed_action_mode": "approval_required", - "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], - } - # safe - return { - "status": "safe", - "reason_codes": [], - "reasons_ar": [], - "allowed_action_mode": "draft_only" if not has_relationship else "approval_required", - "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], - } - - # No desired_channel → return per-channel verdict return { - "status": "safe" if any(v == "safe" for v in channel_map.values()) else "needs_review", - "reason_codes": [], - "reasons_ar": [], - "allowed_action_mode": "draft_only", - "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], - "channel_status": channel_map, - "risk_score": risk, + "opted_out": "المتلقي طلب عدم التواصل.", + "cold_whatsapp": "واتساب بارد غير مسموح افتراضياً.", + "unknown_source": "مصدر غير موثّق — يحتاج مراجعة.", + "purchased_list": "قائمة مشتراة/مكشوفة — محظورة.", + "no_identifier": "لا هاتف ولا إيميل صالح.", } -def explain_contactability_ar(result: dict[str, Any]) -> str: - """Build a human Arabic explanation from a contactability result.""" - status = result.get("status", "unknown") - reasons = result.get("reasons_ar", []) - channels = result.get("allowed_channels", []) - if status == "blocked": - return f"محظور: {' / '.join(reasons) or 'سياسة عامة'}." - if status == "needs_review": - return ( - f"يحتاج مراجعة: {' / '.join(reasons) or 'بدون مصدر واضح'}. " - f"القنوات المتاحة بعد المراجعة: {', '.join(channels) or 'لا شيء'}." +def evaluate_contactability(contact: dict[str, Any], desired_channel: str | None = None) -> dict[str, Any]: + """ + Returns status, allowed_action_modes, channels, Arabic explanation. + ``contact`` may include: source, opted_out, cold_whatsapp, phone, email, opt_in_status. + """ + if contact.get("opted_out") in (True, "true", "1", 1): + return _result( + "blocked", + "opted_out", + ["blocked"], + [], + "محظور: opt-out ساري.", ) - return f"آمن. القنوات المسموحة: {', '.join(channels)}." + + if contact.get("cold_whatsapp") in (True, "true", "1", 1): + return _result( + "blocked", + "cold_whatsapp", + ["blocked"], + [], + "محظور: واتساب بارد — استخدم إيميل أو opt-in صريح.", + ) + + raw_src = str(contact.get("source") or "").lower() + if raw_src in ("scraped", "purchased_list"): + return _result( + "blocked", + "purchased_list", + ["blocked"], + [], + "محظور: مصدر قائمة غير موثوق أو scraping.", + ) + + src = classify_source(str(contact.get("source") or "unknown_source")) + if src in ("opt_out",): + return _result("blocked", "opted_out", ["blocked"], [], "محظور: مصدر opt-out.") + + if src == "cold_list": + return _result( + "needs_review", + "cold_list", + ["suggest_only", "draft_only", "approval_required"], + allowed_channels_for_source(src, contact.get("opt_in_status")), + "قائمة باردة — مسودات بريد فقط تحت مراجعة.", + ) + + opt_in = str(contact.get("opt_in_status") or "") + phone = str(contact.get("phone") or contact.get("mobile") or "").strip() + email = str(contact.get("email") or "").strip() + if not phone and not email: + return _result( + "needs_review", + "no_identifier", + ["suggest_only", "blocked"], + [], + "يحتاج مراجعة: لا معرّف تواصل واضح.", + ) + + if src == "unknown_source": + return _result( + "needs_review", + "unknown_source", + ["suggest_only", "draft_only", "approval_required"], + allowed_channels_for_source(src, opt_in), + "مراجعة بشرية: المصدر غير موثّق.", + ) + + chans = allowed_channels_for_source(src, opt_in) + review = required_review_level(src) + if review == "human_review": + status = "needs_review" + modes = ["suggest_only", "draft_only", "approval_required"] + elif review == "light_review": + status = "safe" if desired_channel != "whatsapp" else "needs_review" + modes = ["draft_only", "approval_required", "suggest_only"] + else: + status = "safe" + modes = ["draft_only", "approval_required", "suggest_only"] + + if desired_channel == "whatsapp" and "whatsapp_draft_if_opt_in" not in chans and "opt_in" not in opt_in.lower(): + if status == "safe": + status = "needs_review" + modes = ["draft_only", "approval_required", "blocked"] + + ar = { + "safe": "مسموح بمسودات وموافقة قبل أي إرسال خارجي.", + "needs_review": "يحتاج مراجعة قبل التواصل.", + "blocked": "غير مسموح بالتواصل بهذه القناة/المصدر.", + }[status] + + return _result(status, review, modes, chans, ar) + + +def explain_contactability_ar(result: dict[str, Any]) -> str: + return str(result.get("summary_ar") or "") def allowed_action_modes(result: dict[str, Any]) -> list[str]: - """Return the action modes available given a contactability verdict.""" - status = result.get("status", "blocked") - if status == "blocked": - return ["blocked"] - if status == "needs_review": - return ["suggest_only", "draft_only", "approval_required"] - return ["draft_only", "approval_required", "approved_execute"] + return list(result.get("action_modes") or []) + + +def _result( + status: str, + reason: str, + modes: list[str], + channels: list[str], + summary_ar: str, +) -> dict[str, Any]: + return { + "status": status, + "reason_code": reason, + "action_modes": modes, + "allowed_channel_hints": channels, + "summary_ar": summary_ar, + "approval_required": status != "blocked", + "demo": True, + } diff --git a/dealix/auto_client_acquisition/targeting_os/contract_drafts.py b/dealix/auto_client_acquisition/targeting_os/contract_drafts.py index 2666de0f..b7efac1a 100644 --- a/dealix/auto_client_acquisition/targeting_os/contract_drafts.py +++ b/dealix/auto_client_acquisition/targeting_os/contract_drafts.py @@ -1,121 +1,46 @@ -"""Contract draft outlines — Arabic skeletons; legal review required.""" +"""Contract outlines — not legal advice; human + legal review required.""" from __future__ import annotations from typing import Any -_DISCLAIMER_AR = ( - "هذه مسودة هيكلية فقط، ليست استشارة قانونية. " - "لا تُوقَّع قبل مراجعة محامٍ مرخّص في المملكة العربية السعودية." -) +def _meta() -> dict[str, Any]: + return {"legal_review_required": True, "approval_required": True, "not_legal_advice": True, "demo": True} def draft_pilot_agreement_outline() -> dict[str, Any]: - """Pilot Agreement outline (Arabic skeleton).""" return { - "title_ar": "اتفاقية تجربة Pilot لخدمة Dealix", - "sections_ar": [ - "الأطراف والتعريفات.", - "نطاق الـ Pilot ومدته (7 أيام).", - "المدخلات المطلوبة من العميل.", - "المخرجات المُتفق عليها (10 فرص + رسائل + Proof Pack).", - "السرية وعدم استخدام بيانات العميل لأغراض أخرى.", - "PDPL وحقوق الموضوعات (الأشخاص).", - "السعر وطريقة الدفع (Pilot أو case study).", - "إنهاء الاتفاقية والاستمرارية.", - "حدود المسؤولية.", - "القانون الواجب التطبيق والاختصاص.", - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": _DISCLAIMER_AR, + "title": "Pilot Agreement Outline", + "sections": ["Scope", "Duration", "Fees", "Data processing", "Termination", "Liability cap"], + **_meta(), } def draft_dpa_outline() -> dict[str, Any]: - """Data Processing Addendum outline (Arabic skeleton, PDPL-aware).""" - return { - "title_ar": "ملحق معالجة البيانات (DPA)", - "sections_ar": [ - "التعريفات حسب نظام حماية البيانات الشخصية السعودي (PDPL).", - "أدوار الأطراف (Controller / Processor).", - "أنواع البيانات والـ subjects.", - "أغراض المعالجة.", - "الإجراءات الأمنية المطبّقة.", - "نقل البيانات خارج المملكة (إن وُجد).", - "الاحتفاظ والإتلاف.", - "حقوق الموضوعات (طلبات الوصول/التصحيح/الحذف).", - "خرق البيانات والإبلاغ.", - "الـ subprocessors المعتمدون.", - "التدقيق والامتثال.", - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": _DISCLAIMER_AR, - } + return {"title": "DPA Outline", "sections": ["Roles", "Subprocessors", "Retention", "Security", "Subject rights"], **_meta()} def draft_referral_agreement_outline() -> dict[str, Any]: - """Referral Agreement outline.""" - return { - "title_ar": "اتفاقية إحالة (Referral)", - "sections_ar": [ - "تعريف الـ Referrer والإحالة المؤهلة.", - "نموذج الـ revenue share (نسبة + مدة).", - "شروط الدفع وتاريخ الاستحقاق.", - "السرية.", - "عدم الإغراء (no-poach اختيارية).", - "سياسات PDPL لمشاركة بيانات الـ leads.", - "إنهاء الاتفاقية.", - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": _DISCLAIMER_AR, - } + return {"title": "Referral Agreement Outline", "sections": ["Commission", "Attribution", "Payment terms"], **_meta()} def draft_agency_partner_outline() -> dict[str, Any]: - """Agency Partner Agreement outline (white-label/co-branded).""" - return { - "title_ar": "اتفاقية شريك وكالة لـ Dealix", - "sections_ar": [ - "هيكل الشراكة (revenue share / setup fee / co-branding).", - "نطاق الخدمات المقدّمة من الوكالة لعملائها.", - "Proof Packs مشتركة العلامة.", - "حقوق الملكية الفكرية.", - "السرية والـ NDAs.", - "PDPL ونقل البيانات بين Dealix والوكالة.", - "حدود المسؤولية والـ SLA.", - "إنهاء الاتفاقية وتسليم العملاء.", - ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": _DISCLAIMER_AR, - } + return {"title": "Agency Partner Outline", "sections": ["White-label options", "Support", "Revenue share"], **_meta()} def draft_scope_of_work() -> dict[str, Any]: - """Generic Scope-of-Work outline.""" + return {"title": "SOW Outline", "sections": ["Deliverables", "Timeline", "Acceptance criteria"], **_meta()} + + +def list_contract_templates() -> dict[str, Any]: return { - "title_ar": "نطاق العمل (SOW)", - "sections_ar": [ - "ملخص الخدمة.", - "المدخلات المطلوبة من العميل.", - "المخرجات والـ deliverables.", - "الجدول الزمني والـ milestones.", - "المسؤوليات والـ approvals.", - "السعر وطريقة الدفع.", - "حدود نطاق العمل وما خارجه.", - "تغييرات النطاق (Change Requests).", - "معايير القبول (Acceptance Criteria).", + "templates": [ + draft_pilot_agreement_outline(), + draft_dpa_outline(), + draft_referral_agreement_outline(), + draft_agency_partner_outline(), + draft_scope_of_work(), ], - "approval_required": True, - "legal_review_required": True, - "not_legal_advice": True, - "disclaimer_ar": _DISCLAIMER_AR, + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py b/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py index db9fc7b6..55faa265 100644 --- a/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py +++ b/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py @@ -1,106 +1,60 @@ -"""Daily autopilot — يومياً يبني brief + يقترح أفعال + ينظمها بالأولوية.""" +"""Daily targeting brief — cards only, no live sends.""" from __future__ import annotations from typing import Any - -def build_daily_targeting_brief( - company_profile: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build today's Arabic targeting brief for the founder/growth manager.""" - company_profile = company_profile or {} - sector = company_profile.get("sector", "saas") - city = company_profile.get("city", "Riyadh") - - return { - "greeting_ar": "صباح الخير 👋", - "summary_ar": [ - f"عندك اليوم: 10 شركات جديدة مناسبة في قطاع {sector} ({city}).", - "5 رسائل drafts تنتظر اعتمادك.", - "3 leads متأخرة في المتابعة (>72 ساعة).", - "1 فرصة شريك في جدة جاهزة للتواصل.", - "1 قناة (واتساب) تحتاج مراجعة سمعة.", - ], - "priority_decisions_ar": [ - "اعتمد 5 رسائل إيميل (10 دقائق).", - "راجع 12 رقم بدون مصدر واضح قبل أي واتساب.", - "احجز ديمو مع شريك الوكالة هذا الأسبوع.", - ], - "do_not_do_today_ar": [ - "لا تفعّل live WhatsApp send.", - "لا ترفع قائمة باردة بدون تصنيف مصدر.", - "لا تعد بنتائج مضمونة في الرسائل.", - ], - } +from auto_client_acquisition.targeting_os.account_finder import recommend_accounts -def recommend_today_actions( - company_profile: dict[str, Any] | None = None, -) -> list[dict[str, Any]]: - """Return ordered actions for today (deterministic 7-action set).""" - company_profile = company_profile or {} +def build_daily_targeting_brief(company_profile: dict[str, Any]) -> dict[str, Any]: + sector = str(company_profile.get("sector") or "training") + city = str(company_profile.get("city") or "الرياض") + offer = str(company_profile.get("offer") or "Growth OS") + goal = str(company_profile.get("goal") or "book_more_b2b_meetings") + acc = recommend_accounts(sector, city, offer, goal, limit=5) + cards = [] + for a in acc["accounts"][:5]: + cards.append( + { + "type": "new_account", + "title_ar": f"فرصة: {a['company']}", + "summary_ar": a.get("why_now_ar", ""), + "buttons": ["مسودة بريد", "تخطي", "تفاصيل"], + "approval_required": True, + } + ) + cards.append( + { + "type": "approval_needed", + "title_ar": "مراجعات معلّقة", + "summary_ar": "هناك مسودات بانتظار موافقتك.", + "buttons": ["افتح المسودات", "لاحقاً"], + "approval_required": True, + } + ) + return {"date": "demo", "cards": cards[:10], "note_ar": "عرض فقط — لا إرسال.", "demo": True} + + +def recommend_today_actions(company_profile: dict[str, Any]) -> list[str]: return [ - {"id": "approve_5_email_drafts", "label_ar": "اعتمد 5 مسودات إيميل", - "minutes": 10, "approval_required": True, "priority": 1}, - {"id": "review_unknown_source_contacts", "label_ar": "راجع 12 رقم بدون مصدر", - "minutes": 8, "approval_required": True, "priority": 2}, - {"id": "schedule_partner_demo", "label_ar": "احجز ديمو شريك", - "minutes": 5, "approval_required": True, "priority": 3}, - {"id": "respond_to_overdue_leads", "label_ar": "رد على 3 leads متأخرة", - "minutes": 12, "approval_required": True, "priority": 4}, - {"id": "review_whatsapp_quality", "label_ar": "راجع مؤشرات سمعة واتساب", - "minutes": 5, "approval_required": False, "priority": 5}, - {"id": "draft_one_partner_message", "label_ar": "اكتب رسالة شريك وكالة", - "minutes": 8, "approval_required": True, "priority": 6}, - {"id": "log_proof_events", "label_ar": "حدّث Proof Ledger", - "minutes": 3, "approval_required": False, "priority": 7}, + "راجع أعلى 3 حسابات في القائمة", + "اعتمد مسودتي بريد واحدة على الأقل", + "حدّث حالة opt-in للواتساب", ] def prioritize_cards(cards: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Sort cards by `priority` (asc), then by `risk_level` (high first).""" - risk_rank = {"high": 0, "medium": 1, "low": 2, None: 3} - return sorted( - cards, - key=lambda c: ( - int(c.get("priority", 99)), - risk_rank.get(c.get("risk_level"), 9), - ), - ) + order = {"approval_needed": 0, "reputation_risk": 1, "new_account": 2} + return sorted(cards, key=lambda c: order.get(str(c.get("type")), 9)) -def build_end_of_day_report( - day_metrics: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build today's Arabic end-of-day report from metrics.""" - m = day_metrics or {} - accounts = int(m.get("accounts_analyzed", 32)) - opps = int(m.get("opportunities_generated", 10)) - drafts = int(m.get("drafts_approved", 6)) - replies = int(m.get("positive_replies", 2)) - meetings = int(m.get("meetings_drafted", 1)) - risks = int(m.get("risks_blocked", 8)) - +def build_end_of_day_report(day_metrics: dict[str, Any]) -> dict[str, Any]: return { - "today_metrics": { - "accounts_analyzed": accounts, - "opportunities_generated": opps, - "drafts_approved": drafts, - "positive_replies": replies, - "meetings_drafted": meetings, - "risks_blocked": risks, - }, - "summary_ar": [ - f"تم تحليل {accounts} حساب اليوم.", - f"تم توليد {opps} فرصة جديدة.", - f"تم اعتماد {drafts} مسودة.", - f"تم تسجيل {replies} رد إيجابي.", - f"تم تجهيز {meetings} اجتماع.", - f"تم منع {risks} مخاطر تلقائياً.", - ], - "tomorrow_recommendation_ar": ( - "غداً: ركّز على متابعة الردود الإيجابية أولاً، ثم اعتماد رسائل جديدة، " - "ثم جدولة 1-2 ديمو إن أمكن." - ), + "accounts_researched": day_metrics.get("accounts_researched", 12), + "drafts_created": day_metrics.get("drafts_created", 4), + "approvals_pending": day_metrics.get("approvals_pending", 2), + "risks_blocked": day_metrics.get("risks_blocked", 3), + "summary_ar": "تقرير نهاية اليوم — جاهز للمراجعة الإدارية.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/email_strategy.py b/dealix/auto_client_acquisition/targeting_os/email_strategy.py index 37f4f42d..62c78ea3 100644 --- a/dealix/auto_client_acquisition/targeting_os/email_strategy.py +++ b/dealix/auto_client_acquisition/targeting_os/email_strategy.py @@ -1,160 +1,55 @@ -"""Email strategy — drafts only, unsubscribe always, pacing-aware.""" +"""B2B email drafts — approval-first, no misleading subjects.""" from __future__ import annotations from typing import Any -def draft_b2b_email( - contact: dict[str, Any], - *, - offer: str = "", - why_now: str = "", - tone: str = "professional_saudi", -) -> dict[str, Any]: - """Build a B2B email draft (Arabic). Never sends.""" - name = contact.get("name", "") - company = contact.get("company", "") - role = contact.get("role", "") - - salutation = f"هلا {name}" if name else "هلا" - company_part = f" من {company}" if company else "" - why_now_part = f"\n{why_now}\n" if why_now else "\n" - - body_ar = ( - f"{salutation}،\n\n" - f"أكتب لك{company_part} باختصار. " - f"نشتغل على Dealix كمدير نمو عربي للشركات السعودية:" - f"{why_now_part}" - "خلال 7 أيام، نطلع لك:\n" - "• 10 فرص B2B مناسبة لقطاعكم\n" - "• رسائل عربية جاهزة بنبرتنا\n" - "• خطة متابعة قابلة للتنفيذ\n" - "• Proof Pack بعد الأسبوع\n\n" - f"{offer or 'Pilot بـ 499 ريال أو مجاني مقابل case study.'}\n\n" - "إذا الفكرة تناسبك، نحدد مكالمة 15 دقيقة هذا الأسبوع.\n" - "وإن ما كانت الأولوية الآن خبرني وأرتاح.\n\nشاكر لك." - ) - +def draft_b2b_email(contact: dict[str, Any], offer: str, why_now: str, *, tone: str = "professional_saudi") -> dict[str, Any]: + name = str(contact.get("name") or "فريق العمل") + co = str(contact.get("company") or "شركتكم") return { - "subject_ar": ( - f"فرصة نمو لـ{company}" if company else "فرصة نمو B2B خلال 7 أيام" + "subject_ar": f"فكرة سريعة لـ {co} — {offer[:40]}", + "body_ar": ( + f"السلام عليكم {name}،\n\n" + f"لاحظنا: {why_now}\n" + f"نقدّم {offer} بمسار مسودات + موافقة قبل أي إرسال جماعي.\n\n" + f"هل يناسبكم ردّ قصير خلال الأسبوع؟\n\n" + f"مع تحيات فريق Dealix" ), - "body_ar": include_unsubscribe_footer(body_ar), "tone": tone, - "target_role": role, "approval_required": True, - "live_send_allowed": False, + "demo": True, } def include_unsubscribe_footer(body: str) -> str: - """Append a one-line unsubscribe footer (Arabic + English).""" - if not body: - return body - footer = ( - "\n\n———\n" - "لإيقاف هذه الرسائل، رد بكلمة \"إلغاء\" / Reply STOP to unsubscribe." - ) + footer = "\n\n---\nلإلغاء الاشتراك أو طلب عدم التواصل: ردّ بكلمة «توقف»." return body + footer -def recommend_pacing(domain_reputation: str = "fresh") -> dict[str, Any]: - """Recommend a daily send pacing based on domain reputation.""" - rep = (domain_reputation or "fresh").lower() - table = { - "fresh": {"max_daily": 20, "warmup_days": 21, "ramp_step": 5}, - "warmed": {"max_daily": 60, "warmup_days": 0, "ramp_step": 10}, - "trusted": {"max_daily": 200, "warmup_days": 0, "ramp_step": 25}, - "damaged": {"max_daily": 5, "warmup_days": 30, "ramp_step": 1}, - } - plan = table.get(rep, table["fresh"]) - return { - "domain_reputation": rep, - **plan, - "notes_ar": ( - "ابدأ بحدود يومية صغيرة على domain جديد، وارتفع تدريجياً. " - "domain متضرر يحتاج فترة تبريد + warmup قبل العودة." - ), - } +def recommend_pacing(domain_reputation: str) -> dict[str, Any]: + rep = (domain_reputation or "unknown").lower() + daily = 20 if rep == "strong" else 8 if rep == "medium" else 3 + return {"max_daily_drafts": daily, "note_ar": "تدرّج في الحجم لحماية سمعة النطاق.", "demo": True} -def score_email_risk( - contact: dict[str, Any], message: str = "", -) -> dict[str, Any]: - """ - Score an outbound email's risk 0..100 (higher = riskier). - - Looks at source, opt_in, message content for spam triggers. - """ - source = contact.get("source", "unknown_source") - opt_in = (contact.get("opt_in_status") or "unknown").lower() - - risk = 0 - reasons: list[str] = [] - - if source == "cold_list": - risk += 50; reasons.append("قائمة باردة — مخاطرة spam مرتفعة.") - elif source == "unknown_source": - risk += 30; reasons.append("مصدر غير معروف — يحتاج مراجعة.") - elif source in ("inbound_lead", "crm_customer", "website_form"): - risk -= 10 # safer - - if opt_in not in ("yes", "double"): - risk += 10 - - msg = (message or "").lower() - spam_triggers = ["ضمان 100%", "ضمان مضمون", "act now", "urgent", - "free money", "click here now", "limited offer"] - for t in spam_triggers: - if t in msg.lower() or t in (message or ""): - risk += 15 - reasons.append(f"عبارة spam: {t}") - - risk = max(0, min(100, risk)) - if risk >= 60: - verdict = "blocked" - elif risk >= 30: - verdict = "needs_review" - else: - verdict = "safe" - - return {"risk": risk, "verdict": verdict, "reasons_ar": reasons} +def score_email_risk(contact: dict[str, Any], message: str) -> dict[str, Any]: + score = 25 + if "ضمان" in message or "100%" in message: + score += 40 + if classify_unknown(contact): + score += 20 + return {"risk_score": min(100, score), "needs_review": score > 50, "demo": True} -def build_followup_sequence( - contact: dict[str, Any], *, offer: str = "", -) -> dict[str, Any]: - """Build a 3-step Arabic email follow-up sequence.""" - name = contact.get("name", "") - sal = f"هلا {name}" if name else "هلا" - return { - "approval_required": True, - "live_send_allowed": False, - "steps": [ - { - "day": 0, - "subject_ar": "فرصة نمو B2B خلال 7 أيام", - "body_ar": include_unsubscribe_footer( - f"{sal}، (الرسالة الأولى مع العرض الكامل)" - ), - }, - { - "day": 3, - "subject_ar": "متابعة سريعة", - "body_ar": include_unsubscribe_footer( - f"{sal}، أتابع رسالتي السابقة. " - "هل أرتب لك ديمو 12 دقيقة هذا الأسبوع؟" - ), - }, - { - "day": 7, - "subject_ar": "آخر متابعة", - "body_ar": include_unsubscribe_footer( - f"{sal}، آخر متابعة من جهتي. " - "إذا ما كانت الأولوية الآن أرتاح وأرشّفها. " - "وإن أردت ديمو لاحقاً، أنا موجود." - ), - }, - ], - } +def build_followup_sequence(contact: dict[str, Any], offer: str) -> list[dict[str, Any]]: + return [ + {"day_offset": 3, "draft_ar": f"متابعة خفيفة بخصوص {offer} — هل نرسل مثالاً؟", "approval_required": True}, + {"day_offset": 7, "draft_ar": "إغلاق لطيف: نتوفر عند الحاجة.", "approval_required": True}, + ] + + +def classify_unknown(contact: dict[str, Any]) -> bool: + src = str(contact.get("source") or "").lower() + return src in ("", "unknown", "unknown_source", "cold_list") diff --git a/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py b/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py index 86338cb5..565307f5 100644 --- a/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py +++ b/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py @@ -1,147 +1,39 @@ -"""Free Growth Diagnostic — العرض المجاني الذي يجلب pilots.""" +"""Free growth diagnostic — small preview, upsell to pilot.""" from __future__ import annotations from typing import Any -from .account_finder import recommend_accounts -from .contact_source_policy import classify_source -from .contactability_matrix import evaluate_contactability +from auto_client_acquisition.targeting_os.account_finder import recommend_accounts -def build_free_growth_diagnostic( - company_profile: dict[str, Any], -) -> dict[str, Any]: - """ - Build a free 5-section Arabic growth diagnostic for a prospect. - - Inputs: company_profile = {sector, city, offer, goal, has_list?, channels?} - Outputs: 3 opportunities + 1 message + 1 risk + 1 mini proof plan + paid pilot offer. - """ - sector = company_profile.get("sector", "saas") - city = company_profile.get("city", "Riyadh") - offer = company_profile.get("offer", "") - - accounts = recommend_accounts( - sector=sector, city=city, offer=offer, goal="diagnostic", limit=3, - )["accounts"] - - sample_message = ( - f"هلا، لاحظت توسعكم في قطاع {sector}. " - "نشتغل على Dealix كمدير نمو عربي للشركات السعودية. " - "خلال 7 أيام نطلع لكم 10 فرص B2B + رسائل + خطة متابعة. " - "يناسبكم ديمو 12 دقيقة هذا الأسبوع؟" - ) - - risk_summary = { - "label_ar": "احتمال إضرار سمعة الـdomain أو رقم واتساب", - "why_ar": ( - "لو أرسلت لقائمة بدون opt-in، تتجاوز PDPL ويمكن أن تُحظر القناة. " - "الحل: ابدأ بمصادر آمنة فقط." - ), - "mitigation_ar": [ - "صنّف كل contact حسب المصدر.", - "أوقف أي رقم بدون opt-in.", - "ابدأ بـ Free Diagnostic ثم Pilot.", - ], - } - - mini_proof = build_mini_proof_plan() - +def build_free_growth_diagnostic(company_profile: dict[str, Any]) -> dict[str, Any]: + sector = str(company_profile.get("sector") or "b2b") + city = str(company_profile.get("city") or "الرياض") + acc = recommend_accounts(sector, city, company_profile.get("offer") or "خدمة", company_profile.get("goal") or "نمو", limit=3) + opps = acc["accounts"][:3] return { - "company_profile": {"sector": sector, "city": city, "offer": offer}, - "delivered_at": "draft", - "approval_required": True, - "sections": { - "opportunities_ar": accounts, - "sample_message_ar": sample_message, - "risk_summary_ar": risk_summary, - "mini_proof_plan_ar": mini_proof, - "paid_pilot_offer": recommend_paid_pilot_offer({"sector": sector}), - }, - "next_step_ar": ( - "إذا أعجبتك العينة، نكمل Pilot 7 أيام بـ499 ريال " - "أو مجاناً مقابل case study بعد انتهاء الـPilot." - ), + "opportunities": opps, + "sample_message_ar": "نقدّم تجربة 7 أيام مع مسودات معتمدة — هل نرسل ملخصاً؟", + "risk_ar": "تأكد من opt-in قبل أي واتساب جماعي.", + "next_step_ar": "اطلب Pilot بـ 499 ريال أو ما يعادله بعد الاتفاق.", + "demo": True, } -def analyze_uploaded_list_preview( - contacts: list[dict[str, Any]], -) -> dict[str, Any]: - """ - Preview-only analysis of a customer-uploaded list. - - Classifies sources + contactability without storing. Returns aggregate. - """ - if not contacts: - return {"total": 0, "by_status": {}, "preview": []} - - by_status: dict[str, int] = {"safe": 0, "needs_review": 0, "blocked": 0} - preview: list[dict[str, Any]] = [] - - for i, c in enumerate(contacts[:20]): # only first 20 for preview - verdict = evaluate_contactability(c) - status = verdict["status"] - by_status[status] = by_status.get(status, 0) + 1 - preview.append({ - "index": i, - "source": classify_source(c.get("source", "unknown_source"))["source"], - "contactability": status, - "allowed_channels": verdict.get("allowed_channels", []), - }) - - # Aggregate over the FULL list - full_by_status = dict(by_status) - if len(contacts) > 20: - # Project remaining proportionally — deterministic. - scale = len(contacts) / 20 - full_by_status = {k: int(v * scale) for k, v in by_status.items()} - - return { - "total": len(contacts), - "by_status": full_by_status, - "preview": preview, - "recommendations_ar": [ - "ابدأ بالـsafe contacts فقط في الأسبوع الأول.", - "راجع الـneeds_review يدوياً قبل أي إرسال.", - "تخطّ الـblocked تماماً (opt-out).", - ], - } +def analyze_uploaded_list_preview(contacts: list[dict[str, Any]]) -> dict[str, Any]: + """Thin wrapper shape for router; full analysis uses platform import preview.""" + return {"row_count": len(contacts), "hint_ar": "استخدم POST /api/v1/platform/contacts/import-preview للتحليل الكامل.", "demo": True} def recommend_paid_pilot_offer(diagnostic: dict[str, Any]) -> dict[str, Any]: - """Recommend a paid Pilot offer based on diagnostic context.""" return { - "offer_id": "first_10_opportunities_pilot_7d", - "name_ar": "Pilot 7 أيام: 10 فرص + رسائل + متابعة + Proof Pack", - "price_sar_min": 499, - "price_sar_max": 1500, - "free_alternative_ar": "مجاني مقابل case study بعد انتهاء الـPilot.", - "deliverables_ar": [ - "10 فرص B2B مع why-now.", - "10 رسائل عربية جاهزة.", - "خطة متابعة 7 أيام.", - "Proof Pack تفصيلي.", - ], - "approval_required": True, + "name": "First 10 Opportunities Sprint", + "price_hint_sar": "499-1500", + "includes_ar": ["10 فرص", "10 مسودات", "Proof Pack مصغر"], + "demo": True, } def build_mini_proof_plan() -> dict[str, Any]: - """A small Proof Pack template anyone can run in their head.""" - return { - "metrics_to_track": [ - "leads_created", - "drafts_approved", - "positive_replies", - "meetings_drafted", - "pipeline_influenced_sar", - "risks_blocked", - ], - "how_to_count_ar": ( - "كل metric يُحسب يومياً عبر Proof Ledger. " - "في نهاية الأسبوع، نولّد PDF/JSON ونشاركه مع الإدارة." - ), - "review_frequency": "weekly", - } + return {"week_1": ["فرص", "مسودات"], "week_2": ["متابعة", "تقرير"], "demo": True} diff --git a/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py b/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py index 74900021..3b12717f 100644 --- a/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py +++ b/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py @@ -1,4 +1,4 @@ -"""LinkedIn strategy — Lead Forms + manual research + Ads, NO scraping/auto-DM.""" +"""LinkedIn-compliant strategy — Lead Gen, ads, manual tasks only.""" from __future__ import annotations @@ -6,119 +6,58 @@ from typing import Any def linkedin_do_not_do() -> list[str]: - """The hard 'NEVER' list for LinkedIn — encoded explicitly so tests can lock it.""" return [ - "scrape_profiles", - "auto_connect", + "scraping_profiles", "auto_dm", - "browser_automation", - "fake_engagement", - "download_contacts_from_linkedin", - "buy_scraped_leads", - "use_unauthorized_extensions", + "auto_connect", + "bulk_export_without_consent", + "browser_automation_on_linkedin_feed", ] -def recommend_linkedin_strategy( - segment: str, *, goal: str = "fill_pipeline", -) -> dict[str, Any]: - """ - Recommend a compliant LinkedIn strategy for a segment. - - Always picks Lead Gen Forms / manual / Ads — never scraping/auto-DM. - """ +def recommend_linkedin_strategy(segment: str, goal: str) -> dict[str, Any]: return { + "strategy": "lead_gen_forms_first", "segment": segment, "goal": goal, - "primary": "lead_gen_forms", - "secondary": ["linkedin_ads", "manual_account_research", "content_engagement"], "do_not_do": linkedin_do_not_do(), - "rationale_ar": ( - "لينكدإن يحظر crawlers/bots/extensions التي تسحب البيانات أو ترسل/توجّه " - "رسائل أو تصنع تفاعلاً غير أصيل؛ لذلك نعتمد فقط على Lead Gen Forms، " - "الإعلانات، والبحث اليدوي المعتمد." - ), + "summary_ar": "استخدم Lead Gen Forms والإعلانات والمهام اليدوية المعتمدة — لا scraping ولا رسائل آلية.", + "demo": True, } -def build_lead_gen_form_plan( - segment: str, offer: str, *, campaign_name: str = "", -) -> dict[str, Any]: - """Build a structured Lead Gen Form campaign plan.""" - name = campaign_name or f"{segment} — {offer or 'Pilot'}" +def build_lead_gen_form_plan(segment: str, offer: str, campaign_name: str) -> dict[str, Any]: return { - "campaign_name": name, - "audience_ar": ( - f"المستهدفون: {segment} — أصحاب القرار في القطاع المحدد، " - "السعودية والخليج، حجم 11-200 موظف." - ), - "offer_ar": offer or "Pilot 7 أيام لاستخراج 10 فرص B2B + رسائل عربية + Proof Pack.", - "lead_magnet_ar": ( - "Free Growth Diagnostic — تقرير من 5 صفحات: 3 فرص + رسالة عربية + خطة 7 أيام." - ), - "form_fields_required": ["full_name", "company_name", "work_email", "role"], - "hidden_fields": [ - {"name": "campaign_name", "value": name}, - {"name": "sector", "value": segment}, - {"name": "sales_owner", "value": "{{owner}}"}, - {"name": "ad_set", "value": "{{ad_set_id}}"}, + "campaign_name": campaign_name or "dealix_pilot", + "audience_hint": f"{segment} — أصحاب قرار في الخدمات B2B", + "offer": offer, + "hidden_fields_suggested": ["campaign_name", "sector", "sales_owner"], + "next_steps_ar": [ + "أنشئ حملة Lead Gen في LinkedIn Campaign Manager.", + "اربط الحقول المخفية بمصدر Dealix.", + "لا تفعّل إرسالاً آلياً من Dealix إلى InMail بدون سياسة.", ], - "approval_required": True, - "notes_ar": ( - "الـ hidden fields ضرورية لمعرفة مصدر كل lead و ربطه بالـCRM. " - "كل lead من Lead Form يدخل Dealix كـ source=linkedin_lead_form (آمن)." - ), + "demo": True, } -def build_manual_research_task( - account: dict[str, Any], *, role: str = "head_of_sales", -) -> dict[str, Any]: - """Build a manual LinkedIn research task — for a human, not automation.""" - company = account.get("name", "?") +def build_manual_research_task(account: dict[str, Any], role: str) -> dict[str, Any]: return { - "task_type": "manual_linkedin_research", - "company": company, + "task_type": "manual_linkedin_lookup", + "company": account.get("company"), "target_role": role, - "instructions_ar": [ - f"افتح صفحة شركة {company} على LinkedIn يدوياً.", - f"حدد الشخص الذي يحمل دور {role}.", - "لا تستخدم أي extension أو bot لاستخراج البيانات.", - "سجّل اسم الشخص + مسماه فقط — لا تنسخ أي معلومات إضافية.", - "أضف الاسم في Dealix كـ source=manual_research → سيدخل needs_review.", - ], - "approval_required": True, - "completion_minutes": 5, + "instructions_ar": f"ابحث يدوياً عن {role} في {account.get('company')} — انسخ الرابط العام فقط، لا أتمتة.", + "demo": True, } -def build_safe_connection_message( - role: str, company: str, *, offer: str = "", -) -> dict[str, Any]: - """ - Build a safe connection-request message for LinkedIn (manual send by user). - - Never auto-sends. Always returns draft with approval_required=True. - """ - role_ar = role - body_ar = ( - f"هلا، تابعت أعمال {company} مؤخراً وعجبني التوسع. " - f"أعمل على Dealix كمدير نمو عربي للشركات السعودية. " - f"يناسبك نتعارف هنا؟" - ) - if offer: - body_ar += f" وفي حال فيه فرصة لـ{offer}، أكون سعيد أشاركك أمثلة." - +def build_safe_connection_message(role: str, company: str, offer: str) -> dict[str, Any]: return { - "channel": "linkedin_connection_request", - "target_role": role_ar, - "target_company": company, - "body_ar": body_ar[:280], # LinkedIn note limit - "approval_required": True, - "live_send_allowed": False, - "send_method": "manual_only", - "notes_ar": ( - "هذه مسودة. أرسلها يدوياً من حسابك على LinkedIn. " - "Dealix لا يرسل تلقائياً ولا يستخدم أي extension أو bot." + "message_ar": ( + f"تحية، أتابع عمل {company}. نعمل على {offer} لفرق المبيعات في السعودية. " + f"إن كان عندكم اهتمام، أرسل ملخصاً قصيراً دون التزام." ), + "approval_required": True, + "channel": "linkedin_manual_only", + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py b/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py index 9e528a31..a820881a 100644 --- a/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py +++ b/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py @@ -1,133 +1,54 @@ -"""Outreach scheduler — pace, follow-up, opt-out enforcement.""" +"""Multi-day outreach plan — limits and approvals, no execution.""" from __future__ import annotations from typing import Any -DEFAULT_LIMITS: dict[str, int] = { - "max_daily_email_drafts": 30, - "max_daily_whatsapp_approved_sends": 10, +_DEFAULT_LIMITS = { + "max_daily_email_drafts": 8, + "max_daily_whatsapp_approved_sends": 0, "max_followups": 3, - "cooldown_days": 7, + "cooldown_days": 2, "max_same_domain_contacts": 5, } -def build_outreach_plan( - targets: list[dict[str, Any]], - *, - channels: list[str] | None = None, - goal: str = "fill_pipeline", -) -> dict[str, Any]: - """ - Build a per-target outreach plan across channels. - - Each target gets day-by-day actions; never schedules a live send. - """ - channels = channels or ["email", "linkedin_lead_form"] - plan: list[dict[str, Any]] = [] - - for t in targets: - steps: list[dict[str, Any]] = [ - {"day": 0, "channel": channels[0], - "action": "draft_first_message", - "approval_required": True, - "live_send_allowed": False}, - {"day": 3, "channel": channels[0], - "action": "draft_followup_1", - "approval_required": True, - "live_send_allowed": False}, - ] - if "linkedin_lead_form" in channels or "linkedin" in channels: - steps.append({ - "day": 5, "channel": "linkedin_manual", - "action": "manual_research_task", +def build_outreach_plan(targets: list[dict[str, Any]], channels: list[str], goal: str) -> dict[str, Any]: + steps = [] + for i, t in enumerate(targets[:15]): + steps.append( + { + "day_offset": (i % 3) * 2, + "target_id": t.get("id") or f"t{i}", + "channel": channels[0] if channels else "email", + "action": "draft_only", "approval_required": True, - "live_send_allowed": False, - }) - steps.append({ - "day": 7, "channel": channels[0], - "action": "draft_final_followup_or_archive", - "approval_required": True, - "live_send_allowed": False, - }) - plan.append({ - "target_company": t.get("name", "?"), - "target_role": t.get("role", "?"), - "channels": channels, - "steps": steps, - }) - + } + ) return { "goal": goal, - "channels": channels, - "total_targets": len(targets), - "plan": plan, - "limits": DEFAULT_LIMITS, - "notes_ar": ( - "كل خطوة draft تحتاج اعتماد. " - "لا إرسال آلي، ولا تجاوز الحدود اليومية." - ), + "steps": steps, + "limits": _DEFAULT_LIMITS, + "summary_ar": "خطة MVP — كل خطوة مسودة أو موافقة؛ لا إرسال تلقائي.", + "demo": True, } -def schedule_followups(plan: dict[str, Any]) -> dict[str, Any]: - """Add follow-up timing to each target in a plan.""" - out = dict(plan) - out["scheduled"] = True - return out +def schedule_followups(plan: dict[str, Any]) -> list[dict[str, Any]]: + return [{"followup_after_days": 3, "approval_required": True} for _ in plan.get("steps", [])[:5]] -def enforce_daily_limits( - plan: dict[str, Any], - *, - limits: dict[str, int] | None = None, -) -> dict[str, Any]: - """Cap actions in the plan to the configured daily limits.""" - limits = limits or DEFAULT_LIMITS - targets = plan.get("plan", []) - - capped: list[dict[str, Any]] = [] - daily_email = 0 - domain_count: dict[str, int] = {} - - for t in targets: - company = t.get("target_company", "") - # treat company as a proxy for domain in test data - if company in domain_count and domain_count[company] >= limits["max_same_domain_contacts"]: - continue - ok_steps = [] - for step in t.get("steps", []): - if step.get("channel") == "email": - if daily_email >= limits["max_daily_email_drafts"]: - continue - daily_email += 1 - ok_steps.append(step) - if ok_steps: - capped.append({**t, "steps": ok_steps}) - domain_count[company] = domain_count.get(company, 0) + 1 - - return { - **plan, - "plan": capped, - "applied_limits": limits, - "capped_total_targets": len(capped), - } +def enforce_daily_limits(plan: dict[str, Any], limits: dict[str, Any] | None = None) -> dict[str, Any]: + lim = {**_DEFAULT_LIMITS, **(limits or {})} + steps = plan.get("steps") or [] + capped = steps[: lim["max_daily_email_drafts"]] + return {"capped_steps": len(capped), "limits_applied": lim, "truncated": len(steps) > len(capped), "demo": True} def stop_on_opt_out(plan: dict[str, Any]) -> dict[str, Any]: - """Filter out targets where the contact has opted out.""" - targets = plan.get("plan", []) - kept = [t for t in targets if not t.get("opt_out")] - return {**plan, "plan": kept, "stopped_due_to_opt_out": len(targets) - len(kept)} + return {"stopped": True, "reason": "opt_out_global", "note_ar": "أي opt-out يوقف الخطة فوراً.", "demo": True} def summarize_plan_ar(plan: dict[str, Any]) -> str: - """Build an Arabic one-paragraph summary of an outreach plan.""" - n = plan.get("total_targets") or len(plan.get("plan", [])) - channels = ", ".join(plan.get("channels", [])) - return ( - f"خطة تواصل لـ{n} هدف عبر القنوات: {channels}. " - f"كل خطوة draft، تتطلب اعتماد، ولا إرسال آلي. " - f"الحدود اليومية مفعّلة. opt-out يوقف فوراً." - ) + n = len(plan.get("steps") or []) + return f"خطة بـ {n} خطوة — كلها تتطلب موافقة قبل التنفيذ الخارجي." diff --git a/dealix/auto_client_acquisition/targeting_os/reputation_guard.py b/dealix/auto_client_acquisition/targeting_os/reputation_guard.py index d8bb68e1..7ecf2cf5 100644 --- a/dealix/auto_client_acquisition/targeting_os/reputation_guard.py +++ b/dealix/auto_client_acquisition/targeting_os/reputation_guard.py @@ -1,135 +1,51 @@ -"""Reputation guard — يحمي القنوات من الحظر.""" +"""Channel reputation — pause suggestions when metrics look bad.""" from __future__ import annotations from typing import Any -def risk_thresholds() -> dict[str, dict[str, float]]: - """The thresholds where a channel needs throttling/pause.""" +def risk_thresholds() -> dict[str, float]: return { - "email": { - "bounce_rate_warn": 0.02, "bounce_rate_pause": 0.05, - "complaint_rate_warn": 0.001, "complaint_rate_pause": 0.003, - "opt_out_rate_warn": 0.05, "opt_out_rate_pause": 0.10, - "min_reply_rate": 0.02, - }, - "whatsapp": { - "block_rate_warn": 0.01, "block_rate_pause": 0.03, - "report_rate_warn": 0.005, "report_rate_pause": 0.02, - "opt_out_rate_warn": 0.05, "opt_out_rate_pause": 0.10, - "min_reply_rate": 0.10, - }, - "linkedin": { - "connection_decline_warn": 0.3, "connection_decline_pause": 0.5, - }, + "bounce_rate_max": 0.08, + "opt_out_rate_max": 0.02, + "complaint_rate_max": 0.001, + "min_reply_rate": 0.05, } -def calculate_channel_reputation( - metrics: dict[str, float], - *, - channel: str = "email", -) -> dict[str, Any]: - """Compute a 0..100 reputation score for a channel based on metrics.""" - th = risk_thresholds().get(channel, {}) - score = 100 - reasons_ar: list[str] = [] - - if channel == "email": - bounce = float(metrics.get("bounce_rate", 0)) - complaint = float(metrics.get("complaint_rate", 0)) - opt_out = float(metrics.get("opt_out_rate", 0)) - reply = float(metrics.get("reply_rate", 0.05)) - - if bounce >= th["bounce_rate_pause"]: - score -= 40; reasons_ar.append("معدل الـ bounce تجاوز الحد الحرج.") - elif bounce >= th["bounce_rate_warn"]: - score -= 15; reasons_ar.append("ارتفاع في الـ bounce — راقب.") - - if complaint >= th["complaint_rate_pause"]: - score -= 50; reasons_ar.append("شكاوى spam مرتفعة جداً.") - elif complaint >= th["complaint_rate_warn"]: - score -= 20; reasons_ar.append("بداية شكاوى spam.") - - if opt_out >= th["opt_out_rate_pause"]: - score -= 25; reasons_ar.append("نسبة opt-out مرتفعة جداً.") - - if reply < th["min_reply_rate"]: - score -= 10; reasons_ar.append("معدل الرد منخفض — راجع الجودة.") - - elif channel == "whatsapp": - block = float(metrics.get("block_rate", 0)) - report = float(metrics.get("report_rate", 0)) - opt_out = float(metrics.get("opt_out_rate", 0)) - - if block >= th["block_rate_pause"]: - score -= 60; reasons_ar.append("نسبة الحظر مرتفعة جداً — أوقف.") - elif block >= th["block_rate_warn"]: - score -= 25; reasons_ar.append("بداية حظر — راجع المحتوى.") - - if report >= th["report_rate_pause"]: - score -= 50; reasons_ar.append("بلاغات spam على واتساب.") - - if opt_out >= th["opt_out_rate_pause"]: - score -= 30; reasons_ar.append("opt-out واتساب مرتفع.") - - score = max(0, min(100, score)) - return { - "channel": channel, - "score": score, - "reasons_ar": reasons_ar, - "verdict": ("healthy" if score >= 70 - else "watch" if score >= 40 - else "pause"), - } +def calculate_channel_reputation(metrics: dict[str, Any]) -> dict[str, Any]: + bounce = float(metrics.get("bounce_rate") or 0) + opt_out = float(metrics.get("opt_out_rate") or 0) + complaint = float(metrics.get("complaint_rate") or 0) + reply = float(metrics.get("reply_rate") or 0) + th = risk_thresholds() + score = 100.0 + if bounce > th["bounce_rate_max"]: + score -= 30 + if opt_out > th["opt_out_rate_max"]: + score -= 25 + if complaint > th["complaint_rate_max"]: + score -= 40 + if reply < th["min_reply_rate"]: + score -= 15 + return {"reputation_score": max(0, min(100, int(score))), "raw": metrics, "demo": True} -def should_pause_channel( - metrics: dict[str, float], *, channel: str = "email", -) -> dict[str, Any]: - """Boolean wrapper: should we pause this channel right now?""" - rep = calculate_channel_reputation(metrics, channel=channel) - return { - "should_pause": rep["verdict"] == "pause", - "reputation_score": rep["score"], - "reasons_ar": rep["reasons_ar"], - } +def should_pause_channel(metrics: dict[str, Any]) -> bool: + rep = calculate_channel_reputation(metrics) + return rep["reputation_score"] < 40 -def recommend_recovery_action( - metrics: dict[str, float], *, channel: str = "email", -) -> dict[str, Any]: - """Recommend recovery actions based on reputation problems.""" - rep = calculate_channel_reputation(metrics, channel=channel) - actions: list[str] = [] - if rep["verdict"] == "pause": - actions = [ - "أوقف إرسال جميع الحملات الجديدة على هذه القناة.", - "ابدأ فترة تبريد لمدة 14 يوماً على الأقل.", - "افحص قائمة الـ contacts وحدّث opt-in.", - "نظّف عناوين الـ bounce وأعد التحقق.", - ] - elif rep["verdict"] == "watch": - actions = [ - "خفّض الحجم اليومي بنسبة 50%.", - "ركّز على المصادر الآمنة فقط (CRM/inbound).", - "راجع الرسائل لتقليل العبارات المخاطرة.", - ] - else: - actions = ["استمر — راقب أسبوعياً."] - return { - "channel": channel, - "verdict": rep["verdict"], - "actions_ar": actions, - "score": rep["score"], - } +def recommend_recovery_action(metrics: dict[str, Any]) -> dict[str, Any]: + if should_pause_channel(metrics): + return { + "action_ar": "أوقف الإرسال، راجع القوائم والمصادر، قلّل الحجم، أعد تفعيل القناة بعد تحسين المحتوى.", + "demo": True, + } + return {"action_ar": "استمر مع مراقبة يومية للردود وopt-out.", "demo": True} -def summarize_reputation_ar(metrics: dict[str, float], *, channel: str = "email") -> str: - """One-line Arabic summary of channel health.""" - rep = calculate_channel_reputation(metrics, channel=channel) - return ( - f"قناة {channel}: score {rep['score']} ({rep['verdict']}). " - + (rep["reasons_ar"][0] if rep["reasons_ar"] else "حالة صحية.") - ) +def summarize_reputation_ar(metrics: dict[str, Any]) -> str: + rep = calculate_channel_reputation(metrics) + return f"درجة السمعة للقناة: {rep['reputation_score']}/100." diff --git a/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py b/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py index a6122a14..507af92d 100644 --- a/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py +++ b/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py @@ -1,157 +1,49 @@ -"""Self-Growth Mode — Dealix يستهدف عملاءه ويصنع فرصاً لنفسه.""" +"""Dealix self-growth plan — drafts and targets only, no auto outreach.""" from __future__ import annotations from typing import Any -from .account_finder import recommend_accounts -from .buyer_role_mapper import map_buying_committee -from .daily_autopilot import ( - build_daily_targeting_brief, - recommend_today_actions, -) - - -# Dealix's own ICP (deterministic). -DEALIX_ICP_FOCUSES: tuple[dict[str, str], ...] = ( - {"sector": "agency", "city": "Riyadh", "label_ar": "وكالات تسويق B2B في الرياض"}, - {"sector": "training", "city": "Riyadh", "label_ar": "شركات تدريب B2B في الرياض"}, - {"sector": "consulting", "city": "Riyadh", "label_ar": "شركات استشارات نمو"}, - {"sector": "saas", "city": "Riyadh", "label_ar": "SaaS سعودية صغيرة-متوسطة"}, - {"sector": "real_estate", "city": "Jeddah", "label_ar": "وسطاء عقار B2B في جدة"}, -) - - -def recommend_dealix_targets( - *, - sector_focus: str | None = None, - city_focus: str | None = None, - limit: int = 10, -) -> dict[str, Any]: - """Build Dealix's own daily target list.""" - sector = sector_focus or DEALIX_ICP_FOCUSES[0]["sector"] - city = city_focus or DEALIX_ICP_FOCUSES[0]["city"] - accounts = recommend_accounts( - sector=sector, city=city, goal="self_growth", - offer="Pilot 7 أيام لاستخراج 10 فرص B2B", - limit=limit, - ) - committee = map_buying_committee(sector=sector, company_size="small", - goal="fill_pipeline") - return { - "icp": {"sector": sector, "city": city}, - "targets": accounts, - "buying_committee_template": committee, - "approval_required": True, - "live_send_allowed": False, - "notes_ar": ( - "هذه قائمة استهداف Dealix لنفسه. كل تواصل draft فقط، " - "ولا يُرسل إلا بعد اعتماد المؤسس." - ), - } - - -def build_free_service_offer(target: dict[str, Any]) -> dict[str, Any]: - """Build a 'Free Growth Diagnostic' offer card for a single target.""" - company = target.get("name", "?") - return { - "target_company": company, - "offer_id": "free_growth_diagnostic", - "title_ar": f"تشخيص نمو مجاني لـ{company}", - "deliverables_ar": [ - "3 فرص B2B مناسبة لقطاعكم.", - "1 رسالة عربية مخصصة.", - "1 تقرير مخاطر سريع.", - "1 خطة Pilot مقترحة.", - ], - "delivery_time": "خلال 24 ساعة عمل", - "price": 0, - "currency": "SAR", - "follow_up_offer_ar": ( - "إذا أعجبكم، نكمل Pilot 7 أيام بـ499 ريال أو مجاني مقابل case study." - ), - "approval_required": True, - } - - -def build_self_growth_daily_brief( - *, - sector_focus: str | None = None, - city_focus: str | None = None, -) -> dict[str, Any]: - """Build today's self-growth brief for Dealix (founder-facing).""" - sector = sector_focus or DEALIX_ICP_FOCUSES[0]["sector"] - city = city_focus or DEALIX_ICP_FOCUSES[0]["city"] - company_brief = build_daily_targeting_brief({"sector": sector, "city": city}) - actions = recommend_today_actions({"sector": sector, "city": city}) - - targets = recommend_dealix_targets( - sector_focus=sector, city_focus=city, limit=10, - ) - - return { - "icp": {"sector": sector, "city": city}, - "company_brief": company_brief, - "today_actions": actions, - "top_10_targets": targets["targets"]["accounts"][:10], - "recommended_first_action_ar": ( - "ابعث 3 رسائل Free Diagnostic مخصصة هذا الصباح، " - "ثم تابع 2 ديمو من الأمس." - ), - } - - -def build_weekly_learning_report( - results: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Build a weekly Arabic learning report from Dealix's own results.""" - r = results or {} - diagnostics = int(r.get("free_diagnostics_delivered", 0)) - pilots = int(r.get("paid_pilots_started", 0)) - meetings = int(r.get("meetings_held", 0)) - case_studies = int(r.get("case_studies_published", 0)) - revenue = float(r.get("revenue_sar", 0)) - - return { - "week_metrics": { - "free_diagnostics": diagnostics, - "paid_pilots": pilots, - "meetings": meetings, - "case_studies": case_studies, - "revenue_sar": revenue, - }, - "learning_questions_ar": [ - "أي قطاع رد أكثر هذا الأسبوع؟", - "أي رسالة نجحت؟ ولماذا؟", - "أي قناة فعّالة (إيميل / لينكدإن / شركاء)؟", - "أي اعتراض تكرر أكثر من مرتين؟", - "ما العرض الذي يبيع بسهولة؟", - ], - "next_week_experiments_ar": [ - "جرّب angle جديد لقطاع التدريب: ROI ملموس لـHR.", - "أرسل Free Diagnostic لـ20 وكالة تسويق.", - "اعقد ديمو واحد مع شركة SaaS سعودية.", - "اطلب أول case study من أنجح Pilot.", - ], - } +from auto_client_acquisition.targeting_os.account_finder import recommend_accounts def build_dealix_self_growth_plan() -> dict[str, Any]: - """Top-level monthly plan for Dealix using its own OS to grow.""" return { - "icp_focuses": list(DEALIX_ICP_FOCUSES), - "monthly_targets": { - "free_diagnostics_delivered": 30, - "paid_pilots_started": 6, - "growth_os_subscriptions": 3, - "agency_partners_signed": 1, - "case_studies_published": 1, - }, - "operating_loop_ar": [ - "كل صباح: اعرض 10 شركات جديدة + 5 رسائل drafts.", - "كل ظهر: راجع الردود + جدول 1-2 ديمو.", - "كل مساء: حدّث Proof Ledger + أرسل Free Diagnostic لـ3 شركات.", - "كل أسبوع: اكتب learning report + جرّب angle جديد.", - "كل شهر: راجع Service Excellence Score لكل خدمة.", - ], + "focus_ar": "وكالات B2B ومستشارو نمو في الرياض وجدة", + "weekly_goal": "5 ديمو + 2 pilot", + "constraints_ar": "لا scraping، لا إرسال آلي — مسودات وموافقة فقط.", + "demo": True, + } + + +def recommend_dealix_targets(sector_focus: str, city_focus: str) -> dict[str, Any]: + return recommend_accounts(sector_focus or "saas", city_focus or "الرياض", "Dealix Growth OS", "partner_channel", limit=8) + + +def build_free_service_offer(target: dict[str, Any]) -> dict[str, Any]: + return { + "target_company": target.get("company"), + "offer_ar": "تشخيص نمو مجاني: 3 فرص + رسالة واحدة + تقرير مخاطر مصغر.", + "cta_ar": "احجز 15 دقيقة ديمو.", + "approval_required": True, + "demo": True, + } + + +def build_self_growth_daily_brief() -> dict[str, Any]: + t = recommend_dealix_targets("agency", "الرياض") + return { + "title_ar": "Dealix — Self Growth", + "top_targets": t["accounts"][:3], + "actions_ar": ["جهّز ديمو", "أرسل مسودة بريد يدوية بعد الموافقة", "حدّث قائمة المتابعة"], + "demo": True, + } + + +def build_weekly_learning_report(results: dict[str, Any]) -> dict[str, Any]: + return { + "best_sector": results.get("best_sector", "training"), + "best_message_angle": results.get("best_angle", "pilot_7_days"), + "next_experiment_ar": "اختبر قطاع العيادات الأسبوع القادم.", + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/service_offers.py b/dealix/auto_client_acquisition/targeting_os/service_offers.py index 5fcfa28f..31b05d8c 100644 --- a/dealix/auto_client_acquisition/targeting_os/service_offers.py +++ b/dealix/auto_client_acquisition/targeting_os/service_offers.py @@ -1,161 +1,73 @@ -"""Targeting-tier service offers — quick lookup of buyable offers.""" +"""Sellable services metadata — aligns with platform service_catalog where possible.""" from __future__ import annotations from typing import Any -# Targeting-OS-related offers. The full Service Tower has more. -TARGETING_OFFERS: tuple[dict[str, Any], ...] = ( +_SERVICES: list[dict[str, Any]] = [ { "id": "list_intelligence", - "name_ar": "تحليل قائمة (List Intelligence)", - "target_customer_ar": "شركة عندها قائمة أرقام/إيميلات/عملاء قدامى", - "outcome_ar": "أفضل 50 target من قائمتك + رسائل + خطة 7 أيام", - "price_min_sar": 499, - "price_max_sar": 1500, + "name_ar": "ذكاء القوائم", + "target_customer": "شركات عندها CSV عملاء", + "outcome_ar": "تصنيف وcontactability", + "pricing_model": "fixed_sprint", + "price_hint_sar": "499-1500", + "required_integrations": [], + "proof_metric": "safe_vs_blocked_ratio", }, { - "id": "first_10_opportunities_sprint", - "name_ar": "10 فرص في 10 دقائق", - "target_customer_ar": "شركة B2B تحتاج فرص مؤهلة بسرعة", - "outcome_ar": "10 فرص + رسائل + خطة متابعة + Proof Pack", - "price_min_sar": 499, - "price_max_sar": 1500, + "id": "first_10_sprint", + "name_ar": "سباق 10 فرص", + "target_customer": "B2B سعودي", + "outcome_ar": "10 فرص + مسودات", + "pricing_model": "fixed_sprint", + "price_hint_sar": "499-1500", + "required_integrations": [], + "proof_metric": "opportunities_accepted", }, { - "id": "self_growth_operator", - "name_ar": "مدير نمو شخصي (Self-Growth Operator)", - "target_customer_ar": "مؤسسون / مستشارون / وكالات صغيرة", - "outcome_ar": "Daily brief + drafts + متابعة + تقارير", - "price_min_sar": 999, - "price_max_sar": 999, - }, - { - "id": "linkedin_lead_gen_setup", - "name_ar": "إعداد LinkedIn Lead Gen", - "target_customer_ar": "شركات B2B تحتاج decision makers", - "outcome_ar": "حملة Lead Gen Form + audiences + ربط CRM", - "price_min_sar": 2000, - "price_max_sar": 7500, - }, - { - "id": "whatsapp_compliance_setup", - "name_ar": "إعداد امتثال واتساب", - "target_customer_ar": "شركات تستخدم واتساب بشكل عشوائي", - "outcome_ar": "تصنيف القوائم + opt-in templates + audit", - "price_min_sar": 1500, - "price_max_sar": 4000, + "id": "growth_os_monthly", + "name_ar": "Growth OS شهري", + "target_customer": "فرق مبيعات", + "outcome_ar": "تشغيل يومي + Proof", + "pricing_model": "subscription", + "price_hint_sar": "2999+", + "required_integrations": ["gmail", "calendar"], + "proof_metric": "meetings_booked", }, { "id": "partner_sprint", - "name_ar": "سبرنت شراكات", - "target_customer_ar": "شركات تبغى نمو عبر الشركاء", - "outcome_ar": "20 شريك محتمل + رسائل + 5 اجتماعات", - "price_min_sar": 3000, - "price_max_sar": 7500, + "name_ar": "سباق شراكات", + "target_customer": "وكالات", + "outcome_ar": "قائمة شركاء + مسودات", + "pricing_model": "project", + "price_hint_sar": "3000-7500", + "required_integrations": [], + "proof_metric": "partner_meetings", }, - { - "id": "free_growth_diagnostic", - "name_ar": "تشخيص نمو مجاني", - "target_customer_ar": "أي شركة B2B تريد عينة قبل الـPilot", - "outcome_ar": "3 فرص + رسالة + تقرير مخاطر + خطة Pilot", - "price_min_sar": 0, - "price_max_sar": 0, - }, -) +] def list_targeting_services() -> dict[str, Any]: - return { - "total": len(TARGETING_OFFERS), - "offers": [dict(o) for o in TARGETING_OFFERS], - } + return {"services": list(_SERVICES), "count": len(_SERVICES), "demo": True} -def recommend_service_offer( - customer_type: str, - *, - goal: str = "fill_pipeline", -) -> dict[str, Any]: - """Recommend the best-fit offer for a customer type + goal.""" +def recommend_service_offer(customer_type: str, goal: str) -> dict[str, Any]: ct = (customer_type or "").lower() - - if "agency" in ct or "وكالة" in ct: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "partner_sprint") - elif "list" in ct or "قائمة" in ct: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "list_intelligence") - elif "founder" in ct or "مؤسس" in ct: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "self_growth_operator") - elif "saas" in ct or "b2b" in ct: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "first_10_opportunities_sprint") - elif "whatsapp" in ct or "واتساب" in ct: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "whatsapp_compliance_setup") - else: - chosen = next(o for o in TARGETING_OFFERS if o["id"] == "free_growth_diagnostic") - - return { - "recommended_offer": dict(chosen), - "reasoning_ar": ( - f"بناءً على نوع العميل ({customer_type}) والهدف ({goal})، " - f"الأنسب: {chosen['name_ar']}." - ), - } + if "agency" in ct: + return {"recommended": "partner_sprint", "reason_ar": "الوكالات تنقل Dealix لعملائها.", "demo": True} + if "list" in goal.lower() or "csv" in goal.lower(): + return {"recommended": "list_intelligence", "reason_ar": "تنظيف القائمة أولاً يقلل المخاطر.", "demo": True} + return {"recommended": "first_10_sprint", "reason_ar": "أسرع إثبات قيمة.", "demo": True} -def build_offer_card(service: dict[str, Any] | str) -> dict[str, Any]: - """Build an Arabic offer card (≤3 buttons) for the inbox/feed.""" - if isinstance(service, str): - service = next((o for o in TARGETING_OFFERS if o["id"] == service), - {"id": service, "name_ar": service, - "outcome_ar": "", "price_min_sar": 0, "price_max_sar": 0}) - price_label = ( - "مجاني" - if service.get("price_min_sar") == 0 - else f"{service.get('price_min_sar')}–{service.get('price_max_sar')} ريال" - ) - return { - "type": "service_offer", - "service_id": service.get("id"), - "title_ar": service.get("name_ar", "خدمة"), - "summary_ar": service.get("outcome_ar", ""), - "price_ar": price_label, - "buttons_ar": ["ابدأ الآن", "اطلب عرض", "تخطي"], - "approval_required": True, - } +def build_offer_card(service_id: str) -> dict[str, Any]: + for s in _SERVICES: + if s["id"] == service_id: + return {**s, "buttons": ["اطلب عرضاً", "تفاصيل", "لاحقاً"]} + return {"error": "unknown_service", "demo": True} -def estimate_service_price( - service_id: str, - *, - company_size: str = "small", - urgency: str = "normal", - channels_count: int = 1, -) -> dict[str, Any]: - """Estimate a SAR price range for a service given inputs.""" - base = next((o for o in TARGETING_OFFERS if o["id"] == service_id), None) - if base is None: - return {"error": f"unknown service: {service_id}"} - - p_min = float(base["price_min_sar"]) - p_max = float(base["price_max_sar"]) - - # Size multiplier - size_mult = {"micro": 0.8, "small": 1.0, "medium": 1.3, "large": 1.7}.get( - company_size, 1.0, - ) - # Urgency multiplier - urgency_mult = {"normal": 1.0, "rush": 1.3, "asap": 1.5}.get(urgency, 1.0) - # Channel multiplier - ch_mult = 1.0 + max(0, channels_count - 1) * 0.15 - - return { - "service_id": service_id, - "estimated_min_sar": round(p_min * size_mult * urgency_mult * ch_mult), - "estimated_max_sar": round(p_max * size_mult * urgency_mult * ch_mult), - "currency": "SAR", - "factors": { - "company_size": company_size, - "urgency": urgency, - "channels_count": channels_count, - }, - } +def estimate_service_price(service_id: str) -> dict[str, Any]: + card = build_offer_card(service_id) + return {"service_id": service_id, "price_hint_sar": card.get("price_hint_sar"), "demo": True} diff --git a/dealix/auto_client_acquisition/targeting_os/social_strategy.py b/dealix/auto_client_acquisition/targeting_os/social_strategy.py index 65d63c39..408b639d 100644 --- a/dealix/auto_client_acquisition/targeting_os/social_strategy.py +++ b/dealix/auto_client_acquisition/targeting_os/social_strategy.py @@ -1,4 +1,4 @@ -"""Social strategy — official APIs + opt-in DMs only, public replies as drafts.""" +"""Social — official APIs and drafts only.""" from __future__ import annotations @@ -6,89 +6,34 @@ from typing import Any def social_do_not_do() -> list[str]: - return [ - "scrape_public_profiles", - "auto_dm_strangers", - "fake_engagement", - "buy_followers_or_engagement", - "use_unauthorized_apis", - "ignore_platform_terms", - ] + return ["unauthorized_scraping", "auto_dm_without_permission", "firehose_access", "fake_engagement"] -def recommend_social_sources( - sector: str, *, goal: str = "fill_pipeline", -) -> dict[str, Any]: - """Recommend social sources by sector — only safe, official channels.""" - s = (sector or "").lower() - by_sector = { - "real_estate": ["instagram_graph_api", "x_api_mentions", "google_business_reviews"], - "retail": ["instagram_graph_api", "google_business_reviews", "tiktok_business"], - "healthcare": ["google_business_reviews", "instagram_graph_api"], - "saas": ["x_api_mentions", "linkedin_lead_gen_forms"], - "training": ["linkedin_lead_gen_forms", "x_api_mentions"], - "agency": ["linkedin_lead_gen_forms", "x_api_mentions"], - } - return { - "sector": s, - "recommended_sources": by_sector.get(s, ["linkedin_lead_gen_forms", - "google_business_reviews"]), - "do_not_do": social_do_not_do(), - "notes_ar": ( - "نلتزم بالـ official APIs والصلاحيات الرسمية فقط. " - "DMs بدون تفاعل سابق محظورة." - ), - } - - -def build_social_listening_plan( - sector: str, keywords: list[str] | None = None, -) -> dict[str, Any]: - """Build a social listening plan — listening only, no auto-replies.""" - keywords = keywords or [ - "نمو", "B2B", "leads", "اجتماعات", - "Pilot", "تدريب مبيعات", "أتمتة", - ] +def recommend_social_sources(sector: str, goal: str) -> dict[str, Any]: return { "sector": sector, - "keywords_ar_or_en": keywords, - "listen_for": [ - "mentions_of_company", - "competitor_mentions", - "buying_signals", - "complaints", - "hiring_signals", - "events_and_launches", - ], - "convert_to_cards_for": [ - "lead", "competitor_move", "review_response", - "content_idea", "partner_suggestion", - ], - "no_auto_reply": True, - "approval_required_for_reply": True, + "goal": goal, + "recommended_sources": ["linkedin_lead_form", "meta_business_inbox_registered", "x_api_registered_webhook"], + "do_not_do": social_do_not_do(), + "summary_ar": "اربط القنوات التي تملك صلاحية رسمية لها فقط.", + "demo": True, } -def draft_public_reply( - comment: str, - *, - brand_voice: str = "professional_saudi", -) -> dict[str, Any]: - """Build a public reply draft to a comment/review (Arabic).""" - body_ar = ( - "شكراً على ملاحظتك. نأخذ تعليقك بجد وسنتواصل معك مباشرة لتفاصيل أكثر " - "ومعالجة الموضوع. سعدنا بمشاركتك." - ) +def build_social_listening_plan(sector: str, keywords: list[str]) -> dict[str, Any]: return { - "draft": True, - "body_ar": body_ar, - "brand_voice": brand_voice, + "sector": sector, + "keywords": keywords[:20], + "plan_ar": "راقب الكلمات عبر webhooks/APIs المسجّلة؛ حوّل الإشارات إلى كروت مسودة داخل المنصة.", "approval_required": True, - "live_publish_allowed": False, - "guidelines_ar": [ - "لا تكشف بيانات شخصية في الرد العام.", - "حول التفاصيل لقناة خاصة.", - "لا تتجاهل العميل المنزعج.", - "لا تحذف أو ترد بشكل دفاعي.", - ], + "demo": True, + } + + +def draft_public_reply(comment: str, brand_voice: str) -> dict[str, Any]: + return { + "draft_reply_ar": f"شكراً لتعليقكم. نتواصل بالخاص لخدمتكم — [{brand_voice}]", + "original_snippet": comment[:200], + "approval_required": True, + "demo": True, } diff --git a/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py b/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py index 68faa391..41ac2be7 100644 --- a/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py +++ b/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py @@ -1,4 +1,4 @@ -"""WhatsApp strategy — opt-in only, never cold, draft-first.""" +"""WhatsApp drafts — opt-in first, no cold outbound by default.""" from __future__ import annotations @@ -6,119 +6,39 @@ from typing import Any def whatsapp_do_not_do() -> list[str]: - return [ - "cold_send_without_consent", - "scrape_groups", - "buy_phone_lists", - "auto_send_without_approval", - "send_outside_business_hours_without_consent", - "ignore_opt_out", - ] + return ["cold_broadcast", "purchased_list_upload_send", "auto_reply_without_policy", "skip_opt_in"] -def requires_opt_in(contact: dict[str, Any]) -> dict[str, Any]: - """ - Check whether reaching this contact via WhatsApp requires opt-in. +def requires_opt_in(contact: dict[str, Any]) -> bool: + src = str(contact.get("source") or "").lower() + if contact.get("opt_in_whatsapp") in (True, "true", "1", 1): + return False + return src not in ("inbound_lead", "prior_customer", "crm_customer", "explicit_consent") - Returns the opt-in requirement + how to obtain it if missing. - """ - source = contact.get("source", "unknown_source") - opt_in = (contact.get("opt_in_status") or "unknown").lower() - has_relationship = bool(contact.get("has_relationship", False)) - - needs = True - if has_relationship and source == "crm_customer" and opt_in == "yes": - needs = False - if source == "inbound_lead" and opt_in in ("yes", "double"): - needs = False +def draft_whatsapp_message(contact: dict[str, Any], offer: str, why_now: str) -> dict[str, Any]: return { - "needs_opt_in": needs, - "current_status": opt_in, - "source": source, - "obtain_via_ar": ( - "نموذج موقع + تأكيد بالـemail (double opt-in) أو " - "Lead Gen Form + شرح صريح بنوع الرسائل." - ), - } - - -def draft_whatsapp_message( - contact: dict[str, Any], *, offer: str = "", why_now: str = "", -) -> dict[str, Any]: - """Build a WhatsApp message draft. Never sends; always approval-required.""" - name = contact.get("name", "") - sal = f"هلا {name}" if name else "هلا" - why_now_part = f" {why_now}" if why_now else "" - body_ar = ( - f"{sal}.{why_now_part} نشتغل على Dealix كمدير نمو عربي. " - "خلال 7 أيام نطلع 10 فرص B2B + رسائل + خطة متابعة. " - f"{offer or 'Pilot بـ 499 ريال أو مجاني مقابل case study.'} " - "يناسبك ديمو 12 دقيقة هذا الأسبوع؟" - "\n\nلو ما تفضل هذه الرسائل، اكتب \"إلغاء\" وأوقفها." - ) - risk = score_whatsapp_risk(contact, body_ar) - return { - "channel": "whatsapp", - "body_ar": body_ar, + "message_ar": f"هلا، بخصوص {why_now}: نقدّم {offer} بمسار موافقة. تفضّلون ملخص سطرين؟", "approval_required": True, - "live_send_allowed": False, - "opt_in_check": requires_opt_in(contact), - "risk": risk, - "do_not_do": whatsapp_do_not_do(), + "demo": True, } -def score_whatsapp_risk(contact: dict[str, Any], message: str = "") -> dict[str, Any]: - """Score WhatsApp risk 0..100; very strict.""" - source = contact.get("source", "unknown_source") - opt_in = (contact.get("opt_in_status") or "unknown").lower() - risk = 0 - reasons: list[str] = [] - - if source == "cold_list": - risk += 100 - reasons.append("قائمة باردة — واتساب محظور تلقائياً.") - if opt_in not in ("yes", "double"): - risk += 40 - reasons.append("لا يوجد opt-in واضح.") - if source == "unknown_source": - risk += 30 - reasons.append("مصدر غير معروف.") - - risky_phrases = ["ضمان 100%", "آخر فرصة", "اضغط الآن", "نتائج مضمونة"] - for p in risky_phrases: - if p in message: - risk += 25 - reasons.append(f"عبارة محظورة: {p}") - - risk = max(0, min(100, risk)) - if risk >= 50: - verdict = "blocked" - elif risk >= 25: - verdict = "needs_review" - else: - verdict = "safe" - return {"risk": risk, "verdict": verdict, "reasons_ar": reasons} +def score_whatsapp_risk(contact: dict[str, Any], message: str) -> dict[str, Any]: + risk = 30 if requires_opt_in(contact) else 10 + if contact.get("cold_whatsapp"): + risk = 100 + if "ضمان" in message: + risk = min(100, risk + 25) + return {"risk_score": risk, "blocked": risk >= 90, "demo": True} -def build_opt_in_request_template( - company_name: str = "Dealix", -) -> dict[str, Any]: - """Build an opt-in request template the customer can send via website/forms.""" +def build_opt_in_request_template(company_name: str) -> dict[str, Any]: return { - "channel": "website_or_form", - "body_ar": ( - f"بالاشتراك في تنبيهات {company_name} عبر واتساب، أوافق على استقبال " - "رسائل تتعلق بالعروض والمحتوى الخاص بالشركة. أعرف أنه يمكنني الانسحاب " - "في أي وقت بكتابة \"إلغاء\"." - ), - "explicit_purpose_required": True, - "explicit_company_name_required": True, - "explicit_unsubscribe_required": True, - "double_opt_in_recommended": True, - "notes_ar": ( - "WhatsApp Business يتطلب شرحاً صريحاً لما سيستقبله المستخدم. " - "ظهور رقم واتساب في موقع لا يكفي كموافقة." + "template_ar": ( + f"مرحباً، نحن {company_name}. نرسل تحديثات قصيرة عبر واتساب حول [الموضوع]. " + "الرد بـ «نعم» يعني موافقتك. «لا» توقف الرسائل." ), + "approval_required": True, + "demo": True, } diff --git a/dealix/docs/AGENCY_PARTNER_MODE.md b/dealix/docs/AGENCY_PARTNER_MODE.md new file mode 100644 index 00000000..45e3f6c1 --- /dev/null +++ b/dealix/docs/AGENCY_PARTNER_MODE.md @@ -0,0 +1,12 @@ +# Agency Partner Mode + +وضع للوكالات: شراكات، عميل تجريبي مشترك، Proof مشترك، تتبع إحالات. + +## المنتج + +- خدمات: `partner_sprint`, `agency_partner_program` في Service Tower. +- المشغّل: `mode=agency_partner` في `POST /api/v1/operator/chat/message` (يؤثر على `mode_profile` فقط في MVP). + +## الصفحة + +- [`landing/agency-partner.html`](../landing/agency-partner.html) diff --git a/dealix/docs/AGENT_OBSERVABILITY_EVALS.md b/dealix/docs/AGENT_OBSERVABILITY_EVALS.md index 2ed074ed..a420c052 100644 --- a/dealix/docs/AGENT_OBSERVABILITY_EVALS.md +++ b/dealix/docs/AGENT_OBSERVABILITY_EVALS.md @@ -1,67 +1,21 @@ -# Agent Observability + Evals — مراقبة الوكلاء + التقييمات +# Agent Observability and Evals (Growth Tower) -> Trace events معقّمة + safety eval + Saudi tone eval + cost tracker. كله deterministic، لا PII في الـtraces. +أشكال JSON للتقييم والتتبع تمهّد لربط **Langfuse** أو أدوات مماثلة في staging/production. -## 1. Trace Events +## كود -`build_trace_event(...)` يبني trace جاهز لـLangfuse/Sentry: -- `user_id` و`company_id` تُهاش (sha256[:16]) قبل التخزين. -- `payload` و`output` يمران عبر `sanitize_trace_event`. -- الحقول الآمنة (event_type, agent_name, status, latency_ms, cost_estimate, approval_status, tool, policy_result, risk_level, workflow_name, trace_id) تبقى كما هي. +- `auto_client_acquisition/agent_observability/trace_events.py` — `build_trace_event` +- `safety_eval.py` — تقييم أمان بسيط على النص العربي +- `saudi_tone_eval.py` — ملاءمة نبرة سعودية شكلية +- `eval_cases.py` — حالات مرجعية (توسيع لاحقاً) -## 2. Safety Eval +## API -7 قواعد: +- `GET /api/v1/agent-observability/demo` +- `POST /api/v1/agent-observability/eval/safety` — `{ "text_ar": "..." }` +- `POST /api/v1/agent-observability/eval/saudi-tone` — `{ "text_ar": "..." }` +- `POST /api/v1/agent-observability/trace/build` — حقول workflow، policy_result، tool_called، إلخ -| الفئة | السببية بالعربي | الخطورة | -|------|-----------------|--------| -| guarantee | وعد بنتائج مضمونة | 50 | -| scarcity_fake | تكتيك ندرة مزيف | 25 | -| medical_claim | ادعاء طبي | 50 | -| financial_claim | عوائد مبالغ فيها | 35 | -| regulatory | ادعاء ترخيص | 35 | -| personal_data | تلميح بيع بيانات | 50 | -| urgency_manipulation | ضغط زمني مصطنع | 15 | +## خطوة تالية -`score = max(0, 100 - sum_penalties)`. تيرز: ≥70 safe, ≥40 needs_review, <40 blocked. - -## 3. Saudi Tone Eval - -- إيجابيات: "هلا/أهلاً/مساء الخير، لاحظت/شفت، يناسبك/تحب، Pilot/بايلوت" → +12 لكل واحدة. -- سلبيات: "السيد المحترم/تحية طيبة وبعد/ندعوكم لاكتشاف، leverage/synergy/best-in-class" → -20 لكل واحدة. -- نسبة عربية ≥60%: +20؛ ≥30%: +10. -- طول > 80 كلمة: -10. - -تيرز: ≥75 natural, ≥50 decent, <50 off. - -## 4. Eval Pack - -5 cases مختارة (`run_eval_pack()`): -- natural_warm_intro → safe + natural -- fake_urgency → blocked + off -- too_corporate → safe + off -- medical_claim → blocked + off (أو needs_review) -- decent_but_short → safe + decent - -النتيجة: `{total, passed, failed, pass_rate, results}`. - -## 5. Cost Tracker - -`CostTracker.record(workflow_name, provider_key, task_type, cost_estimate)` ثم `summary()` يُرجع `{runs, total, by_workflow, by_provider, by_task_type}`. - -## 6. Endpoints - -``` -POST /api/v1/agent-observability/trace/build -POST /api/v1/agent-observability/safety/eval -POST /api/v1/agent-observability/tone/eval -GET /api/v1/agent-observability/evals/run -``` - -## 7. حدود - -- لا tokens في الـtraces. -- لا secrets (يمر عبر `sanitize_trace_event`). -- لا raw PII (phones/emails مخفية). -- لا full customer lists. -- لا payment details. +عند تفعيل Langfuse: إرسال نفس الحقول كـ span attributes؛ راجع [`OBSERVABILITY_ENV.md`](OBSERVABILITY_ENV.md). diff --git a/dealix/docs/AGENT_SECURITY_CURATOR.md b/dealix/docs/AGENT_SECURITY_CURATOR.md index f343168c..b4de31f5 100644 --- a/dealix/docs/AGENT_SECURITY_CURATOR.md +++ b/dealix/docs/AGENT_SECURITY_CURATOR.md @@ -1,107 +1,28 @@ -# Security Curator — منظومة حماية وكلاء Dealix +# Agent Security Curator -> **القاعدة الأولى:** لا سرّ يخرج من Dealix إلى log/trace/embedding/patch. -> الـ Security Curator هو الجدار الأول، يعمل قبل أي اتصال بأي قناة خارجية. +طبقة تقلل تسرّب الأسرار وتمنع تطبيقات patch خطرة **قبل** دمجها في الريبو أو تشغيلها على بيئة تحتوي بيانات حقيقية. ---- +## كود -## 1. لماذا هذه الطبقة قبل أي tool live؟ +| ملف | وظيفة | +|------|--------| +| `auto_client_acquisition/security_curator/secret_redactor.py` | `redact_secrets`، `scan_payload`، `sanitize_for_trace` | +| `auto_client_acquisition/security_curator/patch_firewall.py` | `inspect_diff` — قرار `allowed` حسب أنماط `.env` ومفاتيح | +| `auto_client_acquisition/security_curator/tool_output_sanitizer.py` | تجهيز مخرجات أدوات للتتبع | +| `auto_client_acquisition/security_curator/trace_redactor.py` | `redact_trace_payload` / `redact_span_metadata` — بنية JSON كاملة قبل Langfuse وغيره | -Dealix يربط أدوات حساسة: WhatsApp Cloud, Gmail, Calendar, Moyasar, Google Meet, CRM. كل أداة فيها token، كل token خطر إذا تسرب. سابقاً تعرضنا لـPAT مكشوف، لذا قبل أي ربط حي: +## API -- يجب أن يمر كل log/trace من **redactor**. -- يجب أن يمر كل diff من **patch firewall**. -- يجب أن يمر كل tool output من **sanitizer**. -- يجب ألا تخزّن أي assets مع secrets في الـembedding store. +- `GET /api/v1/security-curator/demo` +- `POST /api/v1/security-curator/redact` — جسم `{ "text": "..." }` أو حقول أخرى تُمسح ضمنياً عبر `scan_payload` +- `POST /api/v1/security-curator/inspect-diff` — `{ "diff_text": "..." }` +- `POST /api/v1/security-curator/trace/sanitize` — `{ "payload": { ... } }` لتنقية metadata التتبع ---- +## ممارسات تشغيل -## 2. الوحدات +1. أي output وكيل يُرسل إلى أداة خارجية أو يُخزَّن: مرّره عبر `sanitize_for_trace` أو `redact_secrets`. +2. لا تعتمد على الـ API وحدها للـ CI: أضف فحصاً مشابهاً في pre-commit أو GitHub Action عندما تكون الجاهزية متوفرة. -| الوحدة | الدور | -|--------|------| -| `secret_redactor` | كشف وإزالة 11 نمط سر (GitHub PAT، OpenAI/Anthropic keys، Supabase JWT، WhatsApp/Moyasar/Sentry/Google API keys، AWS، private keys). | -| `patch_firewall` | يفحص الـunified diff قبل commit ويرفض الـ.env و service-account JSON و RSA keys. | -| `trace_redactor` | بالإضافة للأسرار، يخفي phones وemails داخل القيم النصية. | -| `tool_output_sanitizer` | يعقّم مخرجات الأدوات قبل إظهارها للمستخدم أو حفظها في الـledger. | +## اختبارات ---- - -## 3. أنماط الأسرار المكشوفة - -``` -github_pat ghp_*** -github_pat_legacy github_pat_*** -openai_key sk-*** -anthropic_key sk-ant-*** -supabase_service_role eyJ.***.*** -whatsapp_token EAA*** -moyasar_secret sk_***_*** -langfuse_secret lf_sk_*** -sentry_dsn https://***@***/*** -aws_access_key AKIA*** -google_api_key AIza*** -private_key_block BEGIN PRIVATE KEY *** REDACTED *** -``` - -ومفاتيح JSON الحساسة تُستبدل بـ`***` بناءً على substring match (case-insensitive) لـ: -`api_key, apikey, secret, token, password, authorization, access_token, refresh_token, client_secret, private_key, ssn, credit_card, card_number, cvv, iban, moyasar_secret`. - ---- - -## 4. Patch Firewall - -أي PR قبل ما يدخل الريبو: - -1. **ملفات محظورة:** `.env`, `.env.local`, `.env.staging`, `.env.production`, `credentials.json`, `service-account*.json`, `id_rsa`, `*.pem`, `*.p12`, `*.pfx`. -2. **أسرار في الأسطر المضافة:** أي line يبدأ بـ`+` يُمرر من `detect_secret_patterns`. -3. الناتج: `PatchFirewallResult{safe, reasons_ar, blocked_files, secret_findings}`. - -GitHub Push Protection يقبض الأسرار قبل push، لكن لا تعتمد عليه وحده — Patch Firewall يعمل في طبقة التطوير المحلية + CI. - ---- - -## 5. Tool Output Sanitizer - -قبل أن يصل أي مخرج إلى: -- الـAction Ledger -- الـProof Pack -- الواجهة (UI / WhatsApp / Email) -- Langfuse / Sentry - -يمر عبر `sanitize_tool_output(output)` الذي يُرجع: -- `safe: bool` -- `redacted: <نفس الشكل، مُعقّم>` -- `notes_ar: ["تمت إزالة قيم حساسة من المخرج: ..."]` - ---- - -## 6. Endpoints - -``` -GET /api/v1/security-curator/demo -POST /api/v1/security-curator/redact -POST /api/v1/security-curator/inspect-diff -POST /api/v1/security-curator/sanitize-output -``` - ---- - -## 7. اختبارات الأمان (16 test) - -- detect_github_pat لا يُرجع السر الخام أبداً. -- redact_openai_key يستبدل بالـmask. -- scan_payload يخفي `api_key` و`token`. -- inspect_diff يحظر `.env`. -- inspect_diff يحظر سراً مكتوباً داخل سطر مضاف. -- redact_trace يخفي phones/emails مع الحفاظ على الـdomain للسياق. -- sanitize_trace_event يحفظ `event_type/agent_name/latency_ms` ويعقّم `payload`. - ---- - -## 8. ما لا تفعله هذه الطبقة - -- لا تكشف السر الخام في الـlogs أبداً. -- لا تُرجع payload فيه token. -- لا توقع على diff فيه secret. -- لا تستبدل أو تعطّل GitHub Push Protection — هذه الطبقة **إضافة**، لا بديل. +`tests/test_growth_tower_stack.py` — `test_patch_firewall_blocks_env`، `test_redact_github_token`، `test_security_curator_redact_route`. diff --git a/dealix/docs/AGENT_WORKFLOW_ARCHITECTURE.md b/dealix/docs/AGENT_WORKFLOW_ARCHITECTURE.md new file mode 100644 index 00000000..54d06ccd --- /dev/null +++ b/dealix/docs/AGENT_WORKFLOW_ARCHITECTURE.md @@ -0,0 +1,54 @@ +# Agent workflow architecture — Dealix (conceptual) + +> **Scope:** تصميم مفاهيمي لمسارات وكلاء متينة **بدون** إضافة `langgraph` أو تبعيات تنفيذ معقدة إلى `requirements.txt` حتى موافقة صريحة على التكلفة والصيانة. + +## الأهداف التشغيلية + +1. **Durable execution:** إعادة تشغيل آمنة بعد انقطاع؛ حالة الخطوة محفوظة خارج الذاكرة فقط. +2. **Human-in-the-loop:** نقاط توقف عند الموافقة على إرسال خارجي، دفع، أو رسائل واتساب. +3. **Tracing:** ربط كل إجراء بـ `tenant_id`، `correlation_id`، ومسار القرار في `action_policy` / سجلات المنصة. + +## طبقات حالية في الريبو + +- **Innovation:** مسارات عرض و Kill features deterministic تحت `/api/v1/innovation/*`. +- **Platform Services:** سياسة + inbox + بوابة أدوات بدون live تحت `/api/v1/platform/*`. +- **Intelligence layer:** مخرجات JSON خفيفة تحت `/api/v1/intelligence/*`. + +## مسار مقترح (مستقبلي) + +```mermaid +flowchart LR + subgraph ingest [Ingest] + W[Webhook] + end + subgraph platform [Platform] + EB[event_bus] + UI[unified_inbox] + AP[action_policy] + TG[tool_gateway] + end + subgraph human [Human] + H[Approval UI] + end + W --> EB --> UI --> AP + AP -->|approved draft| TG + AP -->|needs approval| H + H --> TG +``` + +## ماذا يضيف LangGraph لاحقاً (إن وُفقت) + +- بيان حالة آلة صريح (nodes/edges) بدل سلاسل if طويلة. +- استئناف من عقدة بعد موافقة بشرية. +- دمج أدوات خارجية خلف نفس `tool_gateway` مع سياسات موحّدة. + +## مخاطر التبني المبكر + +- ازدواج مع منطق الـ API الحالي. +- تعقيد التشغيل والمراقبة قبل إثبات الـ MVP مع العملاء. + +## المراجع الداخلية + +- [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md) +- [`INTELLIGENCE_LAYER_STRATEGY.md`](INTELLIGENCE_LAYER_STRATEGY.md) +- [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md) diff --git a/dealix/docs/APPROVED_MARKET_MESSAGING.md b/dealix/docs/APPROVED_MARKET_MESSAGING.md index 72f5f29d..5f696a87 100644 --- a/dealix/docs/APPROVED_MARKET_MESSAGING.md +++ b/dealix/docs/APPROVED_MARKET_MESSAGING.md @@ -1,119 +1,30 @@ -# Approved Market Messaging — رسائل التسويق المعتمدة +# رسائل تسويقية معتمدة (Approved Market Messaging) -> أي رسالة (LinkedIn post / X tweet / email / landing copy / WhatsApp DM) لازم تُختار من هنا أو متوافقة مع نبرة هذا الملف. +## عنوان رئيسي (شركات) ---- +**Dealix — تشغيل نمو عربي بموافقة وProof Pack** -## Tagline (موحّد) +## عنوان رئيسي (وكالات) -> **Dealix — Saudi Revenue Execution OS** +**Dealix — Growth OS للوكالة: تشخيص، فرص، مسودات، موافقات، وتقرير أثر لعملائك** -النسخة العربية: -> **Dealix — نظام تشغيل الإيرادات السعودي** +## نقاط قصيرة (bullets) ---- +- فرص B2B مع «لماذا الآن» وقناة مقترحة. +- رسائل عربية قصيرة جاهزة للمراجعة (مسودات، لا إرسال تلقائي افتراضي). +- تقييم مخاطر القنوات والمصادر (contactability). +- خطة متابعة وقالب Proof Pack لإثبات القيمة أسبوعياً. -## Elevator Pitch — 30 ثانية +## عروض البداية (Private Beta) -> Dealix يشغّل النمو للشركات السعودية والوكالات: يكتشف الفرص، يكتب الرسائل بالعربي، يطلب موافقتك قبل أي تواصل، ينسق الاجتماعات، ويثبت العائد بـ Proof Pack شهري — دون scraping ولا cold WhatsApp. +- **Growth Starter:** تشخيص + 10 فرص + مسودات + Proof مختصر (نطاق سعر في الكتالوج). +- **Data to Revenue:** قائمة → تصنيف → أهداف آمنة + مسودات. +- **Executive Growth OS:** موجز يومي + كروت قرار + Proof أسبوعي (اشتراك بعد Pilot). ---- +## CTA مقترحة -## 5 Headlines المعتمدة +- «احجز ديمو 15 دقيقة» +- «ابدأ تشخيصاً مجانياً محدوداً» +- «اطلب Pilot بموافقة صريحة على النطاق» -1. **حوّل بياناتك وقنواتك إلى فرص ورسائل واجتماعات وProof Pack.** -2. **شغّل Dealix لعملائك كـ Agency Growth OS.** -3. **10 فرص في 10 دقائق + متابعة + Proof Pack — وأنت توافق قبل أي تواصل.** -4. **Approval-first — لا scraping، لا cold WhatsApp، لا وعود مضمونة.** -5. **Saudi Tone — رسائل عربية طبيعية، ليست ترجمة.** - ---- - -## Positioning vs Competitors (نقاط مقارنة معتمدة) - -| المنافس | ما نقوله | -|---------|---------| -| HubSpot/Salesforce | "Dealix أخف، عربي، service-led، يجيب 'وش أسوي اليوم؟' للـ SMB." | -| Gong | "Dealix يبدأ قبل المكالمة: targeting → رسالة → موافقة → اجتماع → proof." | -| Clay | "Dealix يحول البيانات إلى خدمة مدفوعة + workflow + Proof Pack." | -| WhatsApp tools | "Dealix يقرر هل التواصل آمن، يطلب موافقة، ويثبت العائد." | -| Agencies | "Dealix يحول خدمات الوكالة إلى operating system قابل للتكرار." | -| Generic AI agent | "Dealix لديه services + policies + approvals + proof + revenue work units." | - ---- - -## Outreach Templates (4 segments) - -### وكالات تسويق B2B -هلا [الاسم]، عندي Beta خاص للوكالات. -Dealix يساعد الوكالة تطلع فرص لعملائها، تجهز رسائل عربية، تدير موافقات، وتطلع Proof Pack بعلامة الوكالة والعميل. -أبحث عن وكالة واحدة نجرب معها Pilot مشترك على عميل حقيقي. يناسبك ديمو 15 دقيقة؟ - -### تدريب / استشارات -هلا [الاسم]، متابع توسعكم في برامج الشركات. -Dealix يطلع لكم 10 فرص B2B خلال 7 أيام، يكتب الرسائل بالعربي، ويخلي صانع القرار يوافق قبل أي تواصل، وبعدها يعطي Proof Pack. -Pilot بـ499 ريال أو مجاني مقابل case study. يناسبك ديمو 12 دقيقة؟ - -### SaaS / تقنية صغيرة -هلا [الاسم]، رأيت إصدار النسخة الجديدة من منتجكم — مبروك. -نشتغل على مدير نمو عربي يطلع 10 فرص B2B عبر LinkedIn Lead Forms (لا scraping)، ويكتب الرسائل بالعربي. -أبغى أجربه مع شركة SaaS سعودية واحدة. يناسبك ديمو 12 دقيقة؟ - -### خدمات بقاعدة واتساب -هلا [الاسم]، عندكم قاعدة عملاء واتساب نشطة، صحيح؟ -Dealix ينظف القائمة، يصنف الـ opt-in، يحظر cold WhatsApp تلقائياً، ويكتب رسائل عربية للحملات الآمنة + Proof Pack شهري. -List Intelligence بـ499–1,500 ريال. يناسبك أعطيك تشخيص مجاني أولاً؟ - ---- - -## Social Posts المعتمدة - -### LinkedIn — Founder voice -بعد عام من التطوير، Dealix جاهز كـ Saudi Revenue Execution OS. - -ليس CRM. ليس bot. ليس scraper. -هو نظام يُشغّل النمو لشركتك أو وكالتك: -• 10 فرص B2B + رسائل عربية + متابعة + Proof Pack -• Approval-first في كل قناة -• PDPL-aware: لا cold WhatsApp - -أفتح 5 Pilots بـ499 ريال هذا الأسبوع. مهتم؟ - -### X/Twitter — أقصر -Dealix Private Beta متاحة: -- 10 فرص B2B + رسائل عربية خلال 7 أيام -- Approval-first — لا cold WhatsApp ولا scraping -- 499 ريال أو مجاني مقابل case study - -أبحث عن 5 شركات سعودية B2B لتجربة محدودة. DM للتفاصيل. - ---- - -## ما يتوافق مع الـ tone - -- ✅ "نطلع لك" بدلاً من "نضمن لك". -- ✅ "خلال 7 أيام" بدلاً من "خلال ساعات". -- ✅ "بدون scraping" بدلاً من "نسحب كل البيانات". -- ✅ "بموافقتك" بدلاً من "تلقائياً". -- ✅ "Pilot 499 ريال" بدلاً من "تجربة مجانية مفتوحة". - ---- - -## Slogan-bank (لاستخدام لاحق) - -- "نمو محسوب، لا وعود." -- "كل قرار له Proof." -- "تشغّل، نوافق، نتابع، نثبت." -- "Saudi-first. Service-led. Approval-first." - ---- - -## القاعدة - -كل copy يدخل الإنتاج يجب أن يمر: -1. `safety_eval()` — لا "ضمان 100%" / "آخر فرصة". -2. `saudi_tone_eval()` — score ≥50. -3. مراجعة بشرية ضد `PROHIBITED_CLAIMS.md`. -4. `quality_review.review_service_before_launch` للخدمة المرتبطة. - -أي copy يفشل → لا يخرج. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md b/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md index c985b957..886a2360 100644 --- a/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md +++ b/dealix/docs/AUTONOMOUS_REVENUE_COMPANY_OS.md @@ -1,200 +1,34 @@ -# Dealix Autonomous Revenue Company OS +# Dealix — Autonomous Revenue Company OS -> **الفئة الجديدة:** Dealix ليس CRM ولا أداة واتساب ولا AI agent ولا lead scraper. -> هو **شركة نمو رقمية ذاتية التشغيل** تدخل أي بزنس، تفهمه، تبني خطة نمو، تشغّل الخدمات المناسبة، تطلب موافقات، تنسق القنوات، تفتح شراكات، ترتب اجتماعات، تجهز مدفوعات، وتثبت العائد. +> **الفئة:** ليس CRM ولا بوت واتساب ولا لوحة عادية — نظام تشغيل نمو وإيرادات **عربي سعودي** يربط الإشارة بالسياق ثم الخدمة ثم سير العمل ثم المخاطر ثم المسودة ثم الموافقة ثم التصدير/التنفيذ ثم الـ Proof ثم التعلم والترقية. +> **التنفيذ في الريبو:** طبقات API وحزم `auto_client_acquisition` **deterministic** في MVP؛ إرسال حي وشحن واتساب بارد **غير مفعّل** افتراضياً — انظر [`SAFE_TOOL_GATEWAY_POLICY.md`](SAFE_TOOL_GATEWAY_POLICY.md) و[`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md). ---- +## الاثنا عشر طبقة ومواءمتها مع الكود -## 1. القيم الأساسية للنظام +| # | الطبقة | الدور | أين في الريبو (مرجع) | +|---|--------|--------|----------------------| +| 1 | Autonomous Service Operator | نية → خدمة → intake → مسودة → موافقة → proof | [`autonomous_service_operator/`](../auto_client_acquisition/autonomous_service_operator/)، `GET/POST /api/v1/operator/*` | +| 2 | Service Tower | خدمات منتَجة للبيع | [`service_tower/`](../auto_client_acquisition/service_tower/)، `/api/v1/services/*` | +| 3 | Service Excellence OS | جودة ودرجة وbacklog | [`service_excellence/`](../auto_client_acquisition/service_excellence/)، `/api/v1/service-excellence/*` | +| 4 | Targeting & Acquisition OS | فرص آمنة، بدون scraping | [`targeting_os/`](../auto_client_acquisition/targeting_os/)، `/api/v1/targeting/*` | +| 5 | Growth Control Tower | كروت قرار، command feed | [`innovation/command_feed`](../auto_client_acquisition/innovation/command_feed.py)، `/api/v1/innovation/command-feed/demo`، `/api/v1/platform/inbox/feed` | +| 6 | Safe Tool Gateway | سياسة أداة قبل أي تنفيذ | [`copilot/safe_actions`](../auto_client_acquisition/copilot/safe_actions.py)، [`security_curator/`](../auto_client_acquisition/security_curator/)، [`tool_action_planner`](../auto_client_acquisition/autonomous_service_operator/tool_action_planner.py) | +| 7 | Agent Runtime | وكلاء بحدود وأدوات | [`agents/`](../auto_client_acquisition/agents/)، [`v3`](../api/routers/v3.py)، orchestrator | +| 8 | Durable Workflow Engine | مسارات طويلة + HITL | حالياً: حالة جلسة في الذاكرة + موافقات؛ **LangGraph** فقط بعد موافقة صريحة — [`AGENT_WORKFLOW_ARCHITECTURE.md`](AGENT_WORKFLOW_ARCHITECTURE.md) | +| 9 | Revenue Graph | كيانات وعلاقات | [`revenue_graph/`](../auto_client_acquisition/revenue_graph/)، [`revenue_memory/`](../auto_client_acquisition/revenue_memory/) | +| 10 | Proof Ledger | أحداث إثبات | [`innovation` proof ledger](../api/routers/innovation.py)، [`fetch_proof_ledger_weekly.py`](../scripts/fetch_proof_ledger_weekly.py) | +| 11 | Self-Improving Layer | تقارير أسبوعية وbacklog | [`growth_curator/`](../auto_client_acquisition/growth_curator/)، [`revenue_company_os/self_improvement_loop`](../auto_client_acquisition/revenue_company_os/self_improvement_loop.py) | +| 12 | Revenue Launch System | عروض، pipeline، دفع يدوي | [`revenue_launch/`](../auto_client_acquisition/revenue_launch/)، `/api/v1/revenue-launch/*` | -``` -Signal → Context → Service Recommendation → Workflow → -Risk Check → Draft → Approval → Execution/Export → -Outcome → Proof → Learning → Upgrade -``` +## Draft مقابل Live (ملخّص) -كل event داخل Dealix يمر بهذه السلسلة. لا توجد فجوة بين "إشارة" و"إيراد". +- **Draft / suggest / approval_required:** المسار الافتراضي في MVP (مسودات Gmail، روابط دفع شكلية، كروت موافقة). +- **Live send / charge / calendar insert:** يتطلب إعدادات صريحة + موافقة بشرية؛ الكثير منها **محظور** في العروض التجريبية — راجع سياسة البوابة والاختبارات. ---- +## وثائق مرتبطة -## 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. يبيع نتائج منظمة.** +- [`AUTONOMOUS_SERVICE_OPERATOR.md`](AUTONOMOUS_SERVICE_OPERATOR.md) — المشغّل والنية والجلسة. +- [`SERVICE_BUNDLES.md`](SERVICE_BUNDLES.md) — الباقات التجارية. +- [`REVENUE_WORK_UNITS.md`](REVENUE_WORK_UNITS.md) — وحدات عمل الإيراد (RWU). +- [`CEO_COMMAND_CENTER.md`](CEO_COMMAND_CENTER.md)، [`AGENCY_PARTNER_MODE.md`](AGENCY_PARTNER_MODE.md)، [`SAFE_TOOL_GATEWAY_POLICY.md`](SAFE_TOOL_GATEWAY_POLICY.md)، [`SELF_IMPROVING_REVENUE_LOOP.md`](SELF_IMPROVING_REVENUE_LOOP.md). +- [`DEALIX_100_PERCENT_LAUNCH_PLAN.md`](DEALIX_100_PERCENT_LAUNCH_PLAN.md) — جاهزية الإطلاق الشاملة. diff --git a/dealix/docs/AUTONOMOUS_SERVICE_OPERATOR.md b/dealix/docs/AUTONOMOUS_SERVICE_OPERATOR.md new file mode 100644 index 00000000..6fd90cd2 --- /dev/null +++ b/dealix/docs/AUTONOMOUS_SERVICE_OPERATOR.md @@ -0,0 +1,31 @@ +# Autonomous Service Operator + +> واجهة منتج موحّدة: **نية المستخدم → تصنيف → خدمة موصى بها → intake → مسودة → موافقة → Proof** — بدون LLM إلزامي في الموجة الأولى. + +## الكود + +- الحزمة: [`auto_client_acquisition/autonomous_service_operator/`](../auto_client_acquisition/autonomous_service_operator/) +- الـ API: `GET|POST /api/v1/operator/*` — انظر [`API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md). + +## المكوّنات + +| ملف | وظيفة | +|-----|--------| +| `intent_classifier.py` | تصنيف قواعد عربي/إنجليزي | +| `service_orchestrator.py` | ربط النية بـ `service_id` من Service Tower | +| `conversation_router.py` | `handle_message` — نقطة دخول المحادثة | +| `session_state.py` | جلسات in-memory (MVP) | +| `approval_manager.py` | موافقة / تعديل / تخطي | +| `workflow_runner.py` | حالات intake → draft → pending_approval → proof | +| `intake_collector.py` | حقول مطلوبة من كتالوج الخدمة | +| `tool_action_planner.py` | مصفوفة Safe Tool Gateway | +| `proof_pack_dispatcher.py` | هيكل Proof Pack | +| `upsell_engine.py` | ترقية من `upgrade_path` في الكتالوج | +| `whatsapp_renderer.py` | نصوص مسودة واتساب (لا إرسال) | +| `operator_memory.py` | سجل أدوار المحادثة | +| `service_bundles.py` | باقات Growth Starter وغيرها | +| `executive_mode.py` / `client_mode.py` / `agency_mode.py` | أولويات العرض حسب الدور | + +## الامتثال + +لا تنفيذ live للأدوات المحظورة — [`SAFE_TOOL_GATEWAY_POLICY.md`](SAFE_TOOL_GATEWAY_POLICY.md). diff --git a/dealix/docs/CEO_COMMAND_CENTER.md b/dealix/docs/CEO_COMMAND_CENTER.md new file mode 100644 index 00000000..ed95b8fc --- /dev/null +++ b/dealix/docs/CEO_COMMAND_CENTER.md @@ -0,0 +1,13 @@ +# CEO Command Center + +رؤية المدير التنفيذي: **٣ قرارات، فرص، مخاطر، موافقات** — عربي، أزرار ≤٣ في بطاقات الواجهة. + +## API ذات صلة + +- `GET /api/v1/services/ceo/daily-brief` — Service Tower. +- `GET /api/v1/revenue-os/company-os/command-feed/demo` — دمج Command Feed مع بطاقات Company OS. +- `GET /api/v1/operator/bundles` و`POST /api/v1/operator/chat/message` — مشغّل الخدمات. + +## الوضع التنفيذي + +MVP: عرض deterministic؛ التنفيذ الخارجي يبقى draft/approval — [`SAFE_TOOL_GATEWAY_POLICY.md`](SAFE_TOOL_GATEWAY_POLICY.md). diff --git a/dealix/docs/COMMERCIAL_LAUNCH_MASTER_PLAN.md b/dealix/docs/COMMERCIAL_LAUNCH_MASTER_PLAN.md index 07dc5071..ba303e46 100644 --- a/dealix/docs/COMMERCIAL_LAUNCH_MASTER_PLAN.md +++ b/dealix/docs/COMMERCIAL_LAUNCH_MASTER_PLAN.md @@ -1,22 +1,17 @@ -# الخطة الرئيسية للتدشين التجاري — Dealix +# خطة الإطلاق التجاري — Dealix (مستوى عام) -**تنفيذ يوم التدشين:** [`LAUNCH_DAY_RUNBOOK_AR.md`](LAUNCH_DAY_RUNBOOK_AR.md) — **نطاق التسمية (بيتا vs عام):** [`LAUNCH_SCOPE_AND_NAMING.md`](LAUNCH_SCOPE_AND_NAMING.md). -**تتبع Go/No-Go:** [`PUBLIC_LAUNCH_GO_NO_GO_TRACKER.md`](PUBLIC_LAUNCH_GO_NO_GO_TRACKER.md) — **تدوير أسرار بعد تسريب:** [`SECURITY_SECRET_ROTATION_CHECKLIST.md`](SECURITY_SECRET_ROTATION_CHECKLIST.md). +## ما قبل الإطلاق العام -## المراحل +1. بيتا خاصة مغلقة مع ٥–١٠ عملاء (انظر [`PRIVATE_BETA_LAUNCH_TODAY.md`](PRIVATE_BETA_LAUNCH_TODAY.md))؛ مواءمة رسالة المنتج مع فئة **Autonomous Revenue Company OS** — [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md). +2. Staging مستقر: `/health`، smoke staging، مراقبة أخطاء. +3. سياسات: لا إرسال حي افتراضياً، لا واتساب بارد، لا LinkedIn scraping (انظر [`TARGETING_ACQUISITION_OS.md`](TARGETING_ACQUISITION_OS.md)). +4. Service Tower + Service Excellence: جاهزية خدمة ≥٨٠ أو وسم `beta_only` (انظر [`SERVICE_EXCELLENCE_OS.md`](SERVICE_EXCELLENCE_OS.md)). -1. **Post-merge verification** — [`POST_MERGE_VERIFICATION.md`](POST_MERGE_VERIFICATION.md) -2. **Staging** — [`STAGING_DEPLOYMENT.md`](STAGING_DEPLOYMENT.md) + `scripts/smoke_staging.py` -3. **Compliance baseline** — [`DATA_MAP.md`](DATA_MAP.md)، [`PRIVACY_PDPL_READINESS.md`](PRIVACY_PDPL_READINESS.md)، DPA pilot -4. **Observability + evals** — [`OBSERVABILITY_ENV.md`](OBSERVABILITY_ENV.md)، [`EVALS_RUNBOOK.md`](EVALS_RUNBOOK.md) -5. **WhatsApp beta** — [`WHATSAPP_OPERATOR_FLOW.md`](WHATSAPP_OPERATOR_FLOW.md)، [`WHATSAPP_PRODUCTION_CUTOVER.md`](WHATSAPP_PRODUCTION_CUTOVER.md) -6. **Billing** — [`BILLING_RUNBOOK.md`](BILLING_RUNBOOK.md) -7. **Private beta** — [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md) -8. **Paid beta metrics** — [`PAID_BETA_SCORECARD.md`](PAID_BETA_SCORECARD.md) -9. **Go / No-Go عام** — [`PUBLIC_LAUNCH_GO_NO_GO.md`](PUBLIC_LAUNCH_GO_NO_GO.md) +## الإطلاق العام (عندما) -## قاعدة التسمية +- اشتراكات أو pilots موثقة، DPA، دعم، تتبع تكلفة LLM، وProof Pack حقيقي لعدة أسابيع. -حتى تتحقق مدفوعات واستقرار وتشغيل: الإطلاق هو **Paid Private Beta** أو **Launch Candidate** وليس “Public Launch” كاملاً. +## مسارات مرجعية -بعد الاستقرار التشغيلي للبيتا: راجع [`INNOVATION_STRATEGY.md`](INNOVATION_STRATEGY.md) كطبقة تمييز منتجي/تسويقي (Growth Factory + مسارات `/api/v1/innovation/*` التجريبية) مقابل ما يبقى تنفيذاً عميقاً على بيانات العملاء. +- `GET /api/v1/launch/go-no-go` — قائمة فحص آلية (demo). +- [`DEALIX_100_PERCENT_LAUNCH_PLAN.md`](DEALIX_100_PERCENT_LAUNCH_PLAN.md) — الخطة الشاملة. diff --git a/dealix/docs/CONNECTOR_CATALOG.md b/dealix/docs/CONNECTOR_CATALOG.md index 32fbe328..4e080e9a 100644 --- a/dealix/docs/CONNECTOR_CATALOG.md +++ b/dealix/docs/CONNECTOR_CATALOG.md @@ -1,43 +1,27 @@ -# Connector Catalog — كتالوج التكاملات +# Connector Catalog -> 14 تكامل، كل واحد له launch_phase + risk_level + allowed/blocked actions + Arabic risk notes. +مصدر الحقيقة للقائمة: `auto_client_acquisition/connectors/connector_catalog.py` — الدالة `build_connector_catalog()`. -## 1. القائمة +## HTTP -| key | الحالة | المرحلة | المخاطر | ملاحظة | -|-----|--------|---------|---------|--------| -| whatsapp_cloud | beta | phase_1 | high | PDPL: لا cold بدون opt-in | -| gmail | beta | phase_1 | high | drafts فقط افتراضياً | -| google_calendar | beta | phase_1 | medium | إدراج بموافقة | -| google_meet | beta | phase_2 | high | transcripts بموافقة الجميع | -| moyasar | beta | phase_1 | high | لا تخزّن بطاقات | -| linkedin_lead_forms | coming_soon | phase_2 | medium | leads مصرّح بها | -| google_business_profile | coming_soon | phase_2 | medium | ردود بموافقة | -| x_api | coming_soon | phase_3 | high | حسب خطة الـAPI | -| instagram_graph | coming_soon | phase_3 | high | لا auto-publish | -| google_sheets | beta | phase_1 | low | append بموافقة | -| crm_generic | beta | phase_2 | medium | اقرأ أولاً | -| website_forms | live | phase_1 | low | مصدر العميل | -| composio | coming_soon | phase_4 | medium | خلف Tool Gateway | -| mcp_gateway | coming_soon | phase_4 | high | allowlist + audit | +`GET /api/v1/connectors/catalog` — يعيد `{ "connectors": [...], "count", "demo": true }`. -## 2. Launch Phases +## الموصلات الحالية (MVP) -- **Phase 1** (الإطلاق الخاص): WhatsApp + Gmail + Calendar + Moyasar + Sheets + Website Forms. -- **Phase 2** (Beta موسّع): LinkedIn Lead Forms + Google Business + Meet + CRM. -- **Phase 3** (السوشيال): X + Instagram. -- **Phase 4** (التوسع): Composio + MCP Gateway. +| id | ملاحظة | +|----|--------| +| `whatsapp` | مسودات ومعاينة قوالب؛ إرسال حي محظور في الكتالوج | +| `gmail` | إنشاء مسودة؛ إرسال حي محظور | +| `google_calendar` | مسودة حدث؛ إدراج حي محظور | +| `google_meet` | قراءة transcript مخطط؛ تسجيل بلا موافقة محظور | +| `linkedin_lead_forms` | استيعاب leads | +| `x_api`, `instagram_graph` | مسجّل فقط — لا firehose | +| `google_business_profile` | مسودة رد على مراجعة | +| `google_sheets` | قراءة مخطط | +| `crm` | مزامنة مسودة | +| `moyasar` | مسودة رابط دفع؛ شحن حي محظور | +| `website_forms` | webhook ingest؛ منع scraping | -## 3. Endpoints +## اختبارات -``` -GET /api/v1/connector-catalog/catalog -GET /api/v1/connector-catalog/summary -GET /api/v1/connector-catalog/status -GET /api/v1/connector-catalog/risks -GET /api/v1/connector-catalog/{connector_key} -``` - -## 4. القاعدة الذهبية - -كل tool action يمر من Tool Gateway في `platform_services` → Action Policy → draft/approval_required. الـCatalog هنا توثّق فقط ما هو متاح، **لا تنفّذ**. +`tests/test_growth_tower_stack.py` — `test_connector_catalog_has_twelve_plus`. diff --git a/dealix/docs/CONNECTOR_SETUP_GUIDES.md b/dealix/docs/CONNECTOR_SETUP_GUIDES.md index 7713da4c..0dcc01f8 100644 --- a/dealix/docs/CONNECTOR_SETUP_GUIDES.md +++ b/dealix/docs/CONNECTOR_SETUP_GUIDES.md @@ -1,204 +1,11 @@ -# Connector Setup Guides +# أدلة ربط الموصلات (ملخص) -> دليل مرجعي لربط كل قناة. **القاعدة:** `draft_only` افتراضياً. لا live action قبل env flag صريح + اعتماد بشري. +- **واتساب:** opt-in واضح؛ الإرسال الحي behind flag وسياسة؛ مراجعة قوائم قبل أي حملة. +- **Gmail:** مسودات أولاً (`gmail.compose` لاحقاً عبر OAuth) — لا إرسال من Dealix افتراضياً. +- **Calendar:** مسودات/موافقة قبل `events.insert`. +- **Moyasar:** sandbox أو فاتورة يدوية من لوحة التحكم؛ لا تخزين بيانات بطاقة داخل Dealix في MVP. +- **LinkedIn Lead Gen:** إعلانات + نماذج رسمية؛ لا scraping ولا auto-DM. ---- +**مرجع حالة تجريبية:** `GET /api/v1/customer-ops/connectors/status` -## 11 Connectors المدعومة - -| Key | Default Mode | Phase | Blocking للـ first service | -|-----|--------------|------:|--------------------------| -| gmail | draft_only | 1 | لا | -| google_calendar | draft_only | 1 | لا | -| google_sheets | approved_execute | 1 | لا | -| moyasar | manual | 1 | لا | -| whatsapp_cloud | draft_only | 1 | **نعم** | -| website_forms | approved_execute | 1 | لا | -| linkedin_lead_forms | ingest_only | 2 | لا | -| google_business_profile | draft_only | 2 | لا | -| crm_generic | draft_only | 2 | لا | -| google_meet | ingest_only | 2 | لا | -| instagram_graph | ingest_only | 3 | لا | - ---- - -## 1. Gmail (drafts فقط افتراضياً) - -**Scopes المطلوبة:** -- `gmail.compose` (لإنشاء drafts) -- `gmail.modify` (لإدارة الـ labels — read-only labels فقط في Phase 1) - -**خطوات:** -1. Google Cloud Console → Create OAuth client. -2. أضف Dealix كـ application authorized. -3. منح الصلاحيات على scopes أعلاه فقط. -4. Dealix يستلم refresh_token + access_token. -5. وضع التشغيل: `connected_draft_only`. - -**Live send:** يتطلب `GMAIL_ALLOW_LIVE_SEND=true` env + اعتماد بشري للرسالة. - ---- - -## 2. Google Calendar (drafts فقط) - -**Scopes:** -- `calendar.events` (drafts only) - -**خطوات:** -1. نفس OAuth client من Gmail. -2. أضف scope الـ calendar. -3. Dealix يبني draft events. -4. لا insert إلا بعد: - - `CALENDAR_ALLOW_LIVE_INSERT=true` - - اعتماد بشري لكل event. - ---- - -## 3. Google Sheets (read + append بموافقة) - -**Scopes:** -- `sheets.readonly` للقراءة -- `sheets` للكتابة (append فقط) - -**خطوات:** -1. نفس OAuth. -2. حدد الـ Spreadsheet ID المستخدم لـ Pilot. -3. Dealix يقرأ leads + يكتب Proof Pack. - -**Live append:** يحتاج اعتماد للحقول الحساسة. لا overwrite تلقائي. - ---- - -## 4. Moyasar (manual فقط في Phase 1) - -**عملية الإعداد:** -1. حساب Moyasar dashboard. -2. **لا** إدخال API keys في Dealix. -3. عند طلب دفع: - - Dealix يولّد invoice instructions (halalas-correct). - - المؤسس يدخل Moyasar manually + ينشئ invoice. - - يضع invoice URL في Dealix. -4. تأكيد paid: يدوي عبر Moyasar dashboard ثم تحديث pipeline_tracker. - -**Phase 2:** ربط API + auto-invoice (مع env flag + audit). - ---- - -## 5. WhatsApp Cloud (Blocking — drafts فقط) - -**هذا أهم connector.** بدون WhatsApp opt-in audit، Dealix لا يفعّل first service. - -**خطوات:** -1. Meta Developer Account → WhatsApp Business Cloud. -2. Phone number verification. -3. Webhook URL = Dealix endpoint. -4. **مهم:** opt-in audit أولاً عبر `whatsapp_strategy.requires_opt_in`. - -**Live send:** يتطلب: -- `WHATSAPP_ALLOW_LIVE_SEND=true` -- opt-in موثّق لكل رقم. -- اعتماد بشري للرسالة. -- موافقة العميل على template. - ---- - -## 6. Website Forms (آمنة) - -**خطوات:** -1. أضف form على موقع العميل. -2. Webhook URL = Dealix endpoint. -3. كل form submission يدخل كـ `form.submitted` event. -4. Dealix يبني opportunity card تلقائياً. - -**Live send:** auto-acknowledgment email/WhatsApp مسموح بعد opt-in في الـ form. - ---- - -## 7. LinkedIn Lead Gen Forms (Phase 2) - -**القاعدة:** lead forms فقط — **لا scraping** ولا auto-DM. - -**خطوات:** -1. LinkedIn Campaign Manager → Lead Gen Form. -2. Hidden fields: `campaign_name`, `sector`, `sales_owner`. -3. Webhook إلى Dealix. -4. كل lead → `linkedin_lead_form` source = safe. - ---- - -## 8. Google Business Profile (Phase 2) - -**Scopes:** -- `business.manage` -- `reviews.read` - -**خطوات:** -1. ربط GBP location. -2. Dealix يقرأ reviews. -3. يبني draft reply لكل review. -4. **Live publish** يحتاج اعتماد + `GBP_ALLOW_LIVE_REPLY=true`. - ---- - -## 9. CRM Generic (Phase 2) - -**Supported:** HubSpot, Salesforce, Zoho, Close. - -**خطوات:** -1. OAuth حسب الـ CRM. -2. Read-only في الأسبوع الأول. -3. Write مع approval بعد الأسبوع الأول. -4. لا overwrite owner تلقائي. - ---- - -## 10. Google Meet (Phase 2) - -**Scopes:** -- `meetings.space.readonly` -- `conferenceRecords.transcripts.readonly` - -**خطوات:** -1. OAuth. -2. ingest transcripts بعد موافقة كل المشاركين. -3. Dealix يستخرج objections + next steps + buyer intent. -4. **لا** real-time listening في Phase 2. - ---- - -## 11. Instagram Graph (Phase 3) - -**Phase 3 connector.** ingest only لـ comments + DMs + insights. - ---- - -## Acceptance Criteria للـ connector - -كل connector يُعتبر مُعدّ بنجاح إذا: -1. State = `connected_draft_only` أو `connected_approved_execute`. -2. Test successful (Dealix قرأ شيء أو كتب draft). -3. لا secrets exposed في الـ logs/traces. -4. Audit entry في Action Ledger. - ---- - -## Troubleshooting - -| مشكلة | الحل | -|------|------| -| OAuth callback failed | recheck redirect_uri في Google/Meta console | -| WhatsApp Webhook 401 | تحقق من verify_token | -| Moyasar invoice URL لم يصل | تحقق من dashboard email settings | -| Sheets quota exceeded | خفض الـ append rate أو ربط second Sheet | -| Calendar conflicts | استخدم `freebusy.query` قبل draft event | - ---- - -## Endpoints - -``` -GET /api/v1/customer-ops/connectors/catalog -POST /api/v1/customer-ops/connectors/summary -POST /api/v1/customer-ops/connectors/update -GET /api/v1/customer-ops/connectors/demo -``` +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/CUSTOMER_SUCCESS_PLAYBOOK.md b/dealix/docs/CUSTOMER_SUCCESS_PLAYBOOK.md index 5d8c7e47..53b5ae9b 100644 --- a/dealix/docs/CUSTOMER_SUCCESS_PLAYBOOK.md +++ b/dealix/docs/CUSTOMER_SUCCESS_PLAYBOOK.md @@ -1,141 +1,9 @@ -# Customer Success Playbook +# نجاح العميل — أسبوع Pilot -> **القاعدة:** كل عميل له cadence محسوب بحسب الـ bundle. كل تدهور في النشاط يولّد at-risk alert تلقائياً. +- **يوم ١–٢:** تشغيل أول مهمة وتسليم عينة مخرجات (فرص/مسودات/مخاطر). +- **يوم ٣–٤:** مراجعة الموافقات المعلقة؛ تعديل الرسائل حسب feedback العميل. +- **يوم ٥–٧:** Proof Pack أسبوعي؛ قرار الترقية (Growth OS / List Intelligence / شراكات). ---- +**Cadence:** `GET /api/v1/customer-ops/success/cadence` -## Cadence Types - -``` -weekly_check_in -monthly_proof_review -quarterly_business_review -at_risk_alert -renewal_30_day -renewal_7_day -``` - ---- - -## Endpoints - -``` -POST /api/v1/customer-ops/cs/weekly-check-in -POST /api/v1/customer-ops/cs/at-risk-alert -POST /api/v1/customer-ops/cs/success-plan -``` - ---- - -## Weekly Check-in Agenda (25 دقيقة) - -1. مراجعة آخر Proof Pack (5 دقائق). -2. أبرز فرصة في الـ pipeline (5 دقائق). -3. أبرز خطر في القنوات (5 دقائق). -4. خطة الأسبوع القادم (5 دقائق). -5. أي مساعدة من فريقنا؟ (5 دقائق). - -**Talking points** (تتولد آلياً من metrics): -- "اعتمدتم {drafts_approved} رسالة هذا الأسبوع، ووصلكم {replies} رد." -- "تم تجهيز {meetings} اجتماع." -- "تم منع {risks_blocked} مخاطر تلقائياً." -- "Pipeline متأثر بقيمة {pipeline_sar:.0f} ريال." - ---- - -## At-Risk Detection - -النظام يحسب `risk_score` (0..100) من: - -| العامل | النقاط | -|--------|-------:| -| غير نشط ≥14 يوم | +40 | -| غير نشط ≥7 يوم | +20 | -| ≥10 drafts معلقة | +25 | -| ≥5 drafts معلقة | +10 | -| آخر Proof Pack ≥14 يوم | +30 | - -### Severity -- ≥60 → high → إيميل مؤسس + QBR هذا الأسبوع. -- ≥30 → medium → Proof Pack ملخص + ديمو خدمة جديدة. -- <30 → low → weekly check-in عادية. - ---- - -## Cadence per Bundle - -### Growth Starter -- Day 1: kick-off call + intake. -- Day 3: review first 3 opportunities + drafts. -- Day 7: deliver Proof Pack v1. -- Day 14: weekly check-in + upsell offer. -- Day 30: monthly proof review + renewal/upgrade decision. - -### Executive Growth OS -- Day 1: onboarding + connect channels. -- Day 7: first weekly Proof Pack. -- Day 14: weekly check-in + Founder Shadow Board v1. -- Day 21: monthly proof review. -- Day 30: QBR + annual upgrade conversation. - -### Partnership Growth -- Day 1: partner ICP intake. -- Day 5: 20 partners list + 10 outreach drafts. -- Day 10: 5 partner meetings booked. -- Day 14: weekly check-in. -- Day 30: partner scorecard + revenue share setup. - ---- - -## QBR (Quarterly Business Review) - -عند 90 يوم من اشتراك Growth OS: -1. مراجعة 3 Proof Packs السابقة. -2. حساب ROI: pipeline_x + closed_won_x. -3. مقارنة مع benchmarks القطاع (من `growth_memory`). -4. اقتراح تجارب الـ quarter القادم. -5. مراجعة الـ pricing tier. - ---- - -## Renewal Flow - -### 30-day-out -- إرسال Proof Pack الشهري + رسالة ودية. -- "نلاقيك في QBR لمراجعة العام القادم؟" - -### 7-day-out -- إذا لم يجدّد: إيميل من المؤسس + خصم سنوي 15%. -- إذا renewal at risk: at-risk alert تلقائي. - -### Renewal day -- إرسال invoice + شكر. -- بدء plan الـ quarter القادم. - ---- - -## Health Score Formula - -``` -csat (0..10) × 5 -+ pipeline_sar / 1000 -+ meetings × 8 -+ approval_rate × 50 -- days_inactive × 2 -- drafts_pending × 1 -``` - -``` -≥75 = healthy -50–74 = watch -<50 = at-risk -``` - ---- - -## ما لا يحدث في CS - -- لا "عرض ترقية" قبل تسليم أول Proof Pack. -- لا spam check-ins (max 1 في الأسبوع). -- لا تخطي الـ at-risk alert إذا تجاوز high. -- لا تعديل cadence بدون موافقة العميل. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md index cbfc7741..ff7712cb 100644 --- a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md +++ b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md @@ -148,234 +148,68 @@ OAuth Gmail/Calendar، حصص، سياسات. وعد منتجي مركزي: من مدخلات شركة/قطاع/مدينة/عرض/هدف إلى قائمة ١٠ فرص مع Why Now ومستوى مخاطرة ومسودات عربية **بانتظار الموافقة فقط** — **`POST /api/v1/innovation/opportunities/ten-in-ten`**؛ وصف المهمة في `GET /api/v1/innovation/growth-missions`؛ الاستراتيجية في [`INNOVATION_STRATEGY.md`](INNOVATION_STRATEGY.md)؛ الإطار التشغيلي بجانب `GET /api/v1/business/gtm/first-10` عند التوسع. -## 32. Platform Services Layer — برج التحكم بالنمو +## 32. Growth Control Tower — ترتيب الموجات (0→6) -طبقة موحدة multi-channel فوق `growth_operator` تحوّل Dealix من قناة WhatsApp إلى منصة: +رؤية منتج: **برج تحكم بالنمو** — Inbox موحّد، سياسة قنوات، ذكاء تشغيلي خفيف، ومسودات تكاملات (بدون live) حتى اكتمال البوابات. -- **11 قناة** (`whatsapp, gmail, google_calendar, moyasar, linkedin_lead_forms, x_api, instagram_graph, google_business_profile, google_sheets, crm, website_forms`). -- **Action Policy Engine**: block_cold_whatsapp / block_payment_no_confirm / block_secrets / external_send_needs_approval / high_value_deal_review. -- **Tool Gateway** هو المخرج التنفيذي الوحيد — كل أداة تمر منه. Live env flags افتراضياً OFF. -- **Unified Inbox**: 8 أنواع بطاقات، ≤3 أزرار، عربية. -- **Action Ledger** + **Proof Ledger** (أثر فعلي مقاس بالقناة). -- **12 خدمة قابلة للبيع** (`growth_operator_subscription`, `channel_setup_service`, `lead_intelligence_service`, `partnership_sprint`, `email_revenue_rescue`, `social_growth_os`, `local_business_growth`, `ai_visibility_aeo_sprint`, `revenue_proof_pack_service`, `customer_success_operator`, `payments_collections_operator`, `outreach_approval_service`). +| موجة | المحتوى | وثائق / كود | +|------|---------|-------------| +| **0** | تثبيت: `compileall`، `pytest`، `print_routes`، `smoke_inprocess` | [`POST_MERGE_VERIFICATION.md`](POST_MERGE_VERIFICATION.md) | +| **1** | استراتيجية Platform + Intelligence | [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md)، [`INTELLIGENCE_LAYER_STRATEGY.md`](INTELLIGENCE_LAYER_STRATEGY.md) | +| **2** | `platform_services` + `/api/v1/platform/*` | `auto_client_acquisition/platform_services/` | +| **3** | `intelligence_layer` + `/api/v1/intelligence/*` | `auto_client_acquisition/intelligence_layer/` | +| **4** | Gmail / Calendar / Moyasar — **payload فقط** | `auto_client_acquisition/integrations/` | +| **5** | Ingest نماذج leads (MVP) + قنوات اجتماعية **مسجّلة فقط** في السجل | `ingest/lead-form` + `channel_registry` | +| **6** | وكلاء متينة (مفاهيم) — **بدون** LangGraph في `requirements.txt` حتى موافقة | [`AGENT_WORKFLOW_ARCHITECTURE.md`](AGENT_WORKFLOW_ARCHITECTURE.md) | -**Endpoints:** `/api/v1/platform/{services/catalog, channels, policy/rules, actions/evaluate, tools/invoke, events/ingest, inbox/feed, identity/resolve, ledger/summary, proof-ledger/demo}`. **التفصيل:** [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md). +الالتزام بالبيتا الخاصة والـ PDPL كما في الأقسام أعلاه؛ لا إرسال حي من مسارات المنصة في هذه الموجات. -## 33. Intelligence Layer — الشبكة العصبية للنمو +## 33. مواءمة مسارات API (canonical و aliases) -طبقة فوق Platform Services تجعل Dealix يتعلم ويقترح ويحاكي: +جدول يوحّد أسماء المسارات بين المنتج والكود: [`docs/architecture/API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md) — يُحدَّث عند إضافة مسار جديد أو alias. -- **Growth Brain** لكل عميل + `is_ready_for_autopilot()` (≥30 signals + ≥40% accept). -- **Command Feed**: 9 أنواع بطاقات يومية (opportunity / revenue_leak / partner_suggestion / meeting_prep / review_response / competitive_move / customer_reactivation / ai_visibility_alert / action_required). -- **Action Graph** (10 أنواع حواف): signal → action → outcome. -- **Mission Engine**: 7 ميشنات، **Kill Feature: `first_10_opportunities`**. -- **Decision Memory**: تعلّم من Accept/Skip/Edit/Block. -- **Trust Score** مركب لكل رسالة (safe ≥70 / needs_review 40-69 / blocked <40). -- **Revenue DNA**: best_channel / best_segment / best_angle / common_objection / avg_cycle_days. -- **Opportunity Simulator** (9 قطاعات سعودية): توقع replies/meetings/deals/pipeline_sar + risk_score. -- **Competitive Move Detector**: 8 أنواع حركات + recommended_action_ar. -- **Founder Shadow Board**: موجز أسبوعي (3 قرارات + 3 فرص + 3 مخاطر + علاقة + تجربة + مؤشر). +## 34. Growth Neural Network — طبقات Curator و Meeting و Router (MVP) -**Endpoints:** `/api/v1/intelligence/{growth-brain/build, command-feed/demo, missions, missions/recommend, trust-score, revenue-dna/demo, revenue-dna, simulate-opportunity, competitive-move/analyze, board-brief/demo, decisions/record, decisions/preferences}`. **التفصيل:** [`INTELLIGENCE_LAYER_STRATEGY.md`](INTELLIGENCE_LAYER_STRATEGY.md). +بعد موجات **Growth Control Tower** (أقسام 32–33)، أُضيفت وحدات داعمة تحت `auto_client_acquisition/` مع راوترات FastAPI: **security_curator**، **growth_curator**، **meeting_intelligence**، **model_router**، **connectors**، **agent_observability**، و**growth_operator** (aliases منتجية فقط). التفاصيل التشغيلية والامتثال (مسودات، عدم إرسال حي، عدم شحن تلقائي) كما في [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md) و[`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md). -## 34. Self-Improving Agent Platform (Hermes-inspired) +**وثائق موجهة للتنفيذ:** [`EXECUTION_ROADMAP_AR.md`](EXECUTION_ROADMAP_AR.md)، [`AGENT_SECURITY_CURATOR.md`](AGENT_SECURITY_CURATOR.md)، [`GROWTH_CURATOR_STRATEGY.md`](GROWTH_CURATOR_STRATEGY.md)، [`MEETING_INTELLIGENCE.md`](MEETING_INTELLIGENCE.md)، [`MODEL_PROVIDER_ROUTER.md`](MODEL_PROVIDER_ROUTER.md)، [`AGENT_OBSERVABILITY_EVALS.md`](AGENT_OBSERVABILITY_EVALS.md)، [`CONNECTOR_CATALOG.md`](CONNECTOR_CATALOG.md). -طبقة "ذاتية التحسن" فوق Platform Services + Intelligence Layer. 6 modules جديدة + 6 routers جديدة + 76 اختبار: +## 35. Targeting & Acquisition OS -- **Security Curator** ([`AGENT_SECURITY_CURATOR.md`](AGENT_SECURITY_CURATOR.md)) — secret_redactor (11 نمط: GitHub/OpenAI/Anthropic/Supabase/WhatsApp/Moyasar/Sentry/Google/AWS) + patch_firewall (يحظر `.env` والـRSA keys في الـdiff) + trace_redactor (يخفي phones/emails) + tool_output_sanitizer. -- **Growth Curator** ([`GROWTH_CURATOR_STRATEGY.md`](GROWTH_CURATOR_STRATEGY.md)) — message_curator (يقيّم الرسائل العربية، يكشف 8 عبارات محظورة) + playbook_curator (winner/promising/needs_work/archive) + mission_curator + skill_inventory (20+ skill عبر 5 طبقات) + curator_report (تقرير عربي أسبوعي). -- **Meeting Intelligence** ([`MEETING_INTELLIGENCE.md`](MEETING_INTELLIGENCE.md)) — Pre-meeting brief (6 أقسام عربية) + transcript_parser (Google Meet entries أو نص) + objection_extractor (8 فئات) + followup_builder (email + WhatsApp drafts) + deal_risk (0..100). -- **Model Router** ([`MODEL_PROVIDER_ROUTER.md`](MODEL_PROVIDER_ROUTER.md)) — 7 providers (Claude Sonnet/Haiku, GPT-4, GPT-4o-mini, Gemini Pro, Azure OAI KSA-region, Local Qwen) × 10 task types + cost_policy + fallback_policy (KSA-region أولاً للحالات الحساسة). -- **Connector Catalog** ([`CONNECTOR_CATALOG.md`](CONNECTOR_CATALOG.md)) — 14 تكامل (WhatsApp Cloud, Gmail, Calendar, Meet, Moyasar, LinkedIn Lead Forms, Google Business Profile, X, Instagram, Sheets, CRM, Website Forms, Composio, MCP Gateway) كل واحد له launch_phase + risk_level + Arabic risks. -- **Agent Observability** ([`AGENT_OBSERVABILITY_EVALS.md`](AGENT_OBSERVABILITY_EVALS.md)) — trace_events (مع hash للـuser/company IDs) + safety_eval (7 قواعد) + saudi_tone_eval (إيجابيات/سلبيات/نسبة عربية) + eval_pack (5 cases) + cost_tracker. +طبقة [`targeting_os`](../auto_client_acquisition/targeting_os/) + مسارات `/api/v1/targeting/*` (انظر [`API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md)): استهداف حسابات، لجنة شراء، تقييم مصدر وجهة، خطة outreach، استراتيجية LinkedIn المتوافقة، تشخيص مجاني، وقوالب عقود **للمسودات فقط**. الوثيقة المرجعية: [`TARGETING_ACQUISITION_OS.md`](TARGETING_ACQUISITION_OS.md). -**Endpoints جديدة:** -- `/api/v1/security-curator/{demo, redact, inspect-diff, sanitize-output}` -- `/api/v1/growth-curator/{skills/inventory, messages/grade, messages/improve, messages/duplicates, missions/next, report/weekly, report/demo}` -- `/api/v1/meeting-intelligence/{brief, brief/demo, transcript/summarize, followup/draft, deal-risk}` -- `/api/v1/model-router/{providers, tasks, route, cost-class, usage/demo}` -- `/api/v1/connector-catalog/{catalog, summary, status, risks, {key}}` -- `/api/v1/agent-observability/{trace/build, safety/eval, tone/eval, evals/run}` +## 36. Service Tower + Service Excellence OS -## 35. Private Beta Launch — Today +- **Service Tower** ([`service_tower/`](../auto_client_acquisition/service_tower/)، مسارات `/api/v1/services/*`): كتالوج خدمات بيعية (تشخيص مجاني، ذكاء قوائم، ١٠ فرص، Growth OS، شراكات، …) مع wizard توصية، عروض سعر تقديرية، وبطاقات CEO — مدمج مع كتالوج المنصة في `GET .../catalog` دون تكرار منطق التنفيذ الخارجي. +- **Service Excellence OS** ([`service_excellence/`](../auto_client_acquisition/service_excellence/)، مسارات `/api/v1/service-excellence/*`): مصفوفة ميزات، درجة جاهزية، فجوات تنافسية، حزمة إطلاق، backlog تحسين — deterministic للعرض الداخلي. +- **وثائق:** [`SERVICE_TOWER_STRATEGY.md`](SERVICE_TOWER_STRATEGY.md)، [`SERVICE_EXCELLENCE_OS.md`](SERVICE_EXCELLENCE_OS.md). +- **Landing (عرض):** [`landing/services.html`](../landing/services.html)، [`landing/free-diagnostic.html`](../landing/free-diagnostic.html)، [`landing/first-10-opportunities.html`](../landing/first-10-opportunities.html)، [`landing/agency-partner.html`](../landing/agency-partner.html)، [`landing/list-intelligence.html`](../landing/list-intelligence.html)، [`landing/growth-os.html`](../landing/growth-os.html). -راجع: -- [`PRIVATE_BETA_LAUNCH_TODAY.md`](PRIVATE_BETA_LAUNCH_TODAY.md) — الخطة الكاملة للإطلاق. -- [`DEMO_SCRIPT_12_MINUTES.md`](DEMO_SCRIPT_12_MINUTES.md) — السكربت المعتمد للعرض. -- [`FIRST_20_OUTREACH_MESSAGES.md`](FIRST_20_OUTREACH_MESSAGES.md) — قوالب الرسائل العربية. -- `landing/private-beta.html` — صفحة العرض. +## 37. Launch Ops + جاهزية البيتا -**العرض:** Pilot 7 أيام بـ499 ريال أو مجاني مقابل case study. Paid Pilot 30 يوم بـ1,500–3,000 ريال. Growth OS اشتراك شهري بـ2,999 ريال. +- **Launch Ops** ([`launch_ops/`](../auto_client_acquisition/launch_ops/)، مسارات `/api/v1/launch/*`): عرض البيتا الخاصة، سكربت ديمو ١٢ دقيقة، قوالب أول ٢٠ تواصل، go/no-go، scorecard جاهزية. +- **Service Tower — إضافات:** `GET /api/v1/services/verticals` (ثلاثة أبواب)، `GET /api/v1/services/upgrade-paths`، `GET /api/v1/services/contracts/templates` (مسودات عقود — ليست استشارة قانونية). +- **وثائق:** [`PRIVATE_BETA_LAUNCH_TODAY.md`](PRIVATE_BETA_LAUNCH_TODAY.md)، [`DEMO_SCRIPT_12_MINUTES.md`](DEMO_SCRIPT_12_MINUTES.md)، [`FIRST_20_OUTREACH_MESSAGES.md`](FIRST_20_OUTREACH_MESSAGES.md)، [`COMMERCIAL_LAUNCH_MASTER_PLAN.md`](COMMERCIAL_LAUNCH_MASTER_PLAN.md)، [`FRONTEND_AND_API_MAP.md`](FRONTEND_AND_API_MAP.md) (واجهة HTML ↔ برج API + لغتين للبيتا)، [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md). -**ممنوع اليوم:** live WhatsApp send, live Gmail send, live Calendar insert, payment charge, scraping social, وعود "نضمن نتائج". +## 38. Revenue Today (تحصيل Pilot بدون أتمتة خطرة) -## 36. Targeting & Acquisition OS — نظام الاستهداف الذكي +- **الحزمة** [`revenue_launch/`](../auto_client_acquisition/revenue_launch/): عروض ٤٩٩ و Growth OS Pilot، شرائح أول ٢٠ تواصل، ديمو/إغلاق، مخطط pipeline، checklist تسليم Pilot، قالب Proof Pack، **تعليمات دفع يدوية** (Moyasar dashboard — لا charge من كود Dealix في هذه المرحلة). +- **المسارات** `GET /api/v1/revenue-launch/*` — انظر [`REVENUE_TODAY_PLAYBOOK.md`](REVENUE_TODAY_PLAYBOOK.md) و[`API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md). +- **أداة checklist:** `python scripts/launch_readiness_check.py` (اختياري `--base-url` مع httpx لفحص `/health` وعينات API). -طبقة جديدة (16 module + 20 endpoint + 47 اختبار) تجعل Dealix يستهدف بذكاء بدلاً من جمع عشوائي: +## 39. Autonomous Revenue Company OS (مشغّل + Company OS) -- **Account-first**: `account_finder` يحدد 10-25 شركة لكل (sector, city) مع `fit_score` و`why_now_ar`. -- **Buying Committee**: `buyer_role_mapper` بـ14 دور وخرائط حسب القطاع (training/saas/real_estate/...). -- **Contact Source Policy**: 12 مصدر (crm_customer → opt_out) مع risk_score + retention. -- **Contactability Matrix**: 5 action modes (suggest_only/draft_only/approval_required/approved_execute/blocked). -- **LinkedIn Strategy**: Lead Forms + Ads + Manual فقط — `linkedin_do_not_do()` يقفل scraping/auto-DM/auto-connect. -- **Email Strategy**: drafts + unsubscribe + pacing per domain reputation. -- **WhatsApp Strategy**: opt-in only، rejects cold + risky phrases تلقائياً. -- **Outreach Scheduler**: day-by-day plan + daily limits + opt-out enforcement. -- **Reputation Guard**: bounce/complaint/opt-out thresholds → healthy/watch/pause مع recovery actions. -- **Daily Autopilot**: Arabic brief + 7 today actions + EOD report. -- **Self-Growth Mode**: 5 ICP focuses لـ Dealix نفسه + daily brief + weekly learning. -- **Free Growth Diagnostic**: العرض المجاني الذي يجلب Pilots. -- **Contract Drafts**: Pilot/DPA/Referral/Agency outlines (legal review required, PDPL-aware). +- **الرؤية والطبقات الاثنا عشر:** [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md) — يربط الفئة «نظام تشغيل نمو» بالمجلدات والمسارات الفعلية. +- **المشغّل (نية → خدمة → موافقة):** حزمة [`autonomous_service_operator/`](../auto_client_acquisition/autonomous_service_operator/) ومسارات `GET|POST /api/v1/operator/*` (عرض deterministic، بدون LLM إلزامي في الموجة الأولى). +- **Company OS (حدث → بطاقة → RWU):** [`revenue_company_os/`](../auto_client_acquisition/revenue_company_os/) ومسارات `GET /api/v1/revenue-os/company-os/*` بجانب `revenue_os` الحالي — لا يستبدل `POST /events` بل يكمّل تحت `company-os`. -**Endpoints:** `/api/v1/targeting/{accounts/recommend, buying-committee/map, contacts/evaluate, uploaded-list/analyze, outreach/plan, daily-autopilot/demo, self-growth/demo, reputation/status, linkedin/strategy, drafts/email, drafts/whatsapp, free-diagnostic, services, contracts/templates, ...}`. **التفصيل:** [`TARGETING_ACQUISITION_OS.md`](TARGETING_ACQUISITION_OS.md). +## 40. Positioning Lock + Customer Ops + صفحات الشركات/المسوقين -## 37. Service Tower — برج الخدمات الذاتية - -**12 Productized Service** + Wizard + Pricing Engine + Scorecard + WhatsApp CEO Control + Upgrade Paths (8 modules + 20 endpoint + 38 اختبار): - -| الخدمة | السعر | النوع | -|--------|------|------| -| Free Growth Diagnostic | مجاني | one_time | -| List Intelligence | 499–1,500 | one_time | -| First 10 Opportunities Sprint | 499–1,500 | sprint | -| Self-Growth Operator | 999/شهر | monthly | -| Growth OS Monthly | 2,999/شهر | monthly | -| Email Revenue Rescue | 1,500–5,000 | one_time | -| Meeting Booking Sprint | 1,500–5,000 | sprint | -| Partner Sprint | 3,000–7,500 | sprint | -| Agency Partner Program | 10,000–50,000 | one_time | -| WhatsApp Compliance Setup | 1,500–4,000 | one_time | -| LinkedIn Lead Gen Setup | 2,000–7,500 | one_time | -| Executive Growth Brief | 499–999/شهر | monthly | - -**3 أبواب للعميل:** -1. أريد عملاء جدد. -2. عندي بيانات وأبغى أستفيد منها. -3. أبغى توسع وشراكات. - -**Endpoints:** `/api/v1/services/{catalog, recommend, {id}/start, {id}/workflow, {id}/quote, {id}/scorecard, {id}/upgrade-path, ceo/daily-brief, ceo/approval-card, ...}`. **التفصيل:** [`SERVICE_TOWER_STRATEGY.md`](SERVICE_TOWER_STRATEGY.md). - -## 38. Service Excellence OS — مصنع الخدمات الممتازة - -**8 modules + 22 endpoint + 33 اختبار** يضمنون أن كل خدمة تطلق بـ score ≥80 وتجاوز 4 quality gates، وتستمر في التحسين الأسبوعي: - -- **Feature Matrix** — 12 must-have لكل خدمة + advanced/premium/future. -- **Service Scoring** — 10 أبعاد × 10 = 100 → launch_ready/beta_only/needs_work. -- **Quality Review** — 4 gates: proof / approval / pricing / channels. -- **Competitor Gap** — مقارنة بـ7 فئات منافسين (CRM, WA tools, email assistants, LinkedIn tools, agencies, revenue intelligence, generic AI). -- **Proof Metrics** — ROI estimate (pipeline_x + closed_won_x). -- **Research Lab** — brief شهري + hypotheses + experiments. -- **Improvement Backlog** — feedback → backlog → prioritized weekly tasks. -- **Launch Package** — landing + sales script + 12-min demo + 5-day onboarding. - -**Endpoints:** `/api/v1/service-excellence/{id}/{feature-matrix, score, quality-review, proof-metrics, gap-analysis, research-brief, experiments, monthly-review, backlog, launch-package, sales-script, demo-script}` + `/review/all`. **التفصيل:** [`SERVICE_EXCELLENCE_OS.md`](SERVICE_EXCELLENCE_OS.md). - -## 39. Landing Pages - -- `landing/services.html` — 3 أبواب + 12 خدمة productized. -- `landing/free-diagnostic.html` — العرض المجاني. -- `landing/first-10-opportunities.html` — Kill Feature. -- `landing/agency-partner.html` — برنامج الوكالة الشريكة. -- `landing/private-beta.html` — Private Beta launch. -- `landing/list-intelligence.html` — تحليل القوائم. -- `landing/growth-os.html` — اشتراك Growth OS الشهري. - -## 40. Launch Ops — برج إطلاق الـ Private Beta - -5 modules + 11 endpoints + 25 اختبار. كل ما يحتاجه إطلاق Private Beta اليوم: - -- `private_beta`: عرض اليوم (499 ريال × 7 أيام) + safety notes + FAQ عربي. -- `demo_flow`: 12-min Arabic demo + discovery Qs + objection bank + close script. -- `outreach_messages`: 4 segments × 5 prospects + per-segment رسائل + 3 follow-ups + 6 reply handlers. -- `go_no_go`: 10-gate readiness + critical gates (no_secrets / live_sends_disabled / staging_health) + verdict + concrete next-actions. -- `launch_scorecard`: daily/weekly metrics بـ11 event types + targets (20 outreach/5 ردود/3 ديمو/1 pilot يومياً). - -**Endpoints:** `/api/v1/launch/{private-beta/offer, demo/flow, outreach/first-20, outreach/message, outreach/followup, go-no-go, readiness, scorecard/event, scorecard/daily, scorecard/weekly, scorecard/demo}`. - -## 41. Revenue Launch — تحويل Dealix إلى دخل - -7 modules + 18 endpoints + 31 اختبار. **التفصيل:** [`REVENUE_TODAY_PLAYBOOK.md`](REVENUE_TODAY_PLAYBOOK.md). - -- `offer_builder`: 4 عروض (Private Beta / 499 Pilot / Growth OS Pilot / Free Case Study) + recommend per segment. -- `pipeline_tracker`: 8 stages (identified→contacted→replied→demo_booked→diagnostic_sent→pilot_offered→paid/lost) + Sheet schema + summarize. -- `outreach_sequence`: re-export with revenue-tier extensions. -- `demo_closer`: re-export single source of truth. -- `pilot_delivery`: 24-hour delivery template + intake form (12 fields) + per-service delivery (First 10 / List Intel / Free Diagnostic). -- `proof_pack_template`: 5-line client summary + ROI x-multiples + next-step recommendation (upsell / iterate / extend). -- `payment_manual_flow`: Moyasar invoice instructions (halalas-correct) + payment-link message + confirmation checklist. **No API charge ever**. - -**Endpoints:** `/api/v1/revenue-launch/{offers, offers/recommend, outreach/first-20, outreach/followup, demo-flow, pipeline/schema, pipeline/summarize, pilot-delivery/intake-form, pilot-delivery/24h-plan, pilot-delivery/first-10, pilot-delivery/list-intelligence, pilot-delivery/free-diagnostic, payment/invoice-instructions, payment/link-message, payment/confirmation-checklist, proof-pack/template, proof-pack/client-summary, proof-pack/next-step}`. - -## 42. Service Tower extensions - -- `contract_templates.py` — re-export targeting_os contracts + new SLA outline. -- `vertical_service_map.py` — 6 verticals (B2B SaaS, agencies, training/consulting, real estate, healthcare/local, retail/ecommerce) → recommended service stack + buyer roles + common pains. - -## 43. Scripts - -- `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 شهري. - -## 45. Positioning Lock + Customer Ops + Companies/Marketers - -**8 modules + 20 endpoints + 44 tests + 2 modes + 7 docs**. التفاصيل: - -### Positioning Lock (3 docs) -- [`POSITIONING_LOCK.md`](POSITIONING_LOCK.md) — category, one-liner, primary buyers (شركات + وكالات), wedge, 5 approved claims, 5 modes, 5 bundles. -- [`PROHIBITED_CLAIMS.md`](PROHIBITED_CLAIMS.md) — 8 categories of forbidden marketing language + how they're enforced technically. -- [`APPROVED_MARKET_MESSAGING.md`](APPROVED_MARKET_MESSAGING.md) — tagline, elevator pitch, headlines, 4 outreach segments, social posts, slogan bank. - -### Customer Ops (6 modules) -- `onboarding_checklist`: 8-step Pilot onboarding with progress tracking. -- `connector_setup_status`: 11 connectors (Gmail/Calendar/Sheets/Moyasar/WhatsApp/Forms/LinkedIn/GBP/CRM/Meet/Instagram) with state machine + ready_for_first_service gate. -- `support_ticket_router`: 4-tier P0/P1/P2/P3 classification + Arabic first-response templates. -- `sla_tracker`: per-priority SLA targets + breach detection + weekly health (healthy/watch/critical). -- `customer_success_cadence`: 6 cadence types + at-risk alerts (risk_score 0..100) + per-bundle 30-day plans + QBR. -- `incident_router`: SEV1/SEV2/SEV3 triage + auto-SEV1 on data leak / unauthorized send + canonical response plans (PDPL-aware). - -### Operator Modes (2 new) -- `self_growth_mode` — Dealix uses its own OS to find pilots (re-exports targeting_os.self_growth_mode + operator-tier reminders). -- `service_delivery_mode` — runs client services + tracks SLA + post-delivery handoff to Customer Success. - -### Customer-facing Pages (1 new + 1 updated) -- `landing/companies.html` — Saudi B2B companies. Approval-first, no scraping, 4 bundles. -- `landing/marketers.html` (updated) — agencies/marketers Agency Growth OS path. - -### Customer Ops Docs (5 new) -- [`ONBOARDING_RUNBOOK.md`](ONBOARDING_RUNBOOK.md) — day-by-day kick-off → first Proof Pack. -- [`SUPPORT_SLA.md`](SUPPORT_SLA.md) — P0..P3 + classifier keywords. -- [`INCIDENT_RESPONSE.md`](INCIDENT_RESPONSE.md) — SEV1..SEV3 + post-mortem templates + Arabic comms. -- [`CUSTOMER_SUCCESS_PLAYBOOK.md`](CUSTOMER_SUCCESS_PLAYBOOK.md) — weekly check-ins, at-risk detection, QBR, renewal. -- [`CONNECTOR_SETUP_GUIDES.md`](CONNECTOR_SETUP_GUIDES.md) — 11 connectors with scopes + steps + troubleshooting. - -**Endpoints:** `/api/v1/customer-ops/*` (20). +- **تثبيت الرسالة:** [`POSITIONING_LOCK.md`](POSITIONING_LOCK.md)، [`PROHIBITED_CLAIMS.md`](PROHIBITED_CLAIMS.md)، [`APPROVED_MARKET_MESSAGING.md`](APPROVED_MARKET_MESSAGING.md). +- **Customer Ops (API):** حزمة [`customer_ops/`](../auto_client_acquisition/customer_ops/) ومسارات `GET|POST /api/v1/customer-ops/*` — قائمة onboarding، SLA، حالة موصلات تجريبية، cadence نجاح عميل، playbook حوادث، وتوجيه تذكرة دعم (بدون تنفيذ خارجي). +- **صفحات عرض:** [`landing/companies.html`](../landing/companies.html) (مسار الشركات)، [`landing/marketers.html`](../landing/marketers.html) (مسار الوكالات/المسوقين) — تربط بباقي صفحات الـ landing الحالية. +- **تشغيل المشغّل:** أوضاع إضافية في [`self_growth_mode.py`](../auto_client_acquisition/autonomous_service_operator/self_growth_mode.py) و[`service_delivery_mode.py`](../auto_client_acquisition/autonomous_service_operator/service_delivery_mode.py) بجانب الأوضاع الموجودة. --- -**الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً. الإطلاق اليوم = Private Beta + Pilots + Proof Pack، ليس Public Launch. اليوم Dealix هو **Saudi Revenue Execution OS** بـ45 طبقة وثائقية، 949 اختبار ناجح، CI أخضر. +**الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً. diff --git a/dealix/docs/DEMO_SCRIPT_12_MINUTES.md b/dealix/docs/DEMO_SCRIPT_12_MINUTES.md index dd0c2d3f..e2ef8b68 100644 --- a/dealix/docs/DEMO_SCRIPT_12_MINUTES.md +++ b/dealix/docs/DEMO_SCRIPT_12_MINUTES.md @@ -1,84 +1,14 @@ -# Demo Script — 12 دقيقة +# ديمو Dealix — ١٢ دقيقة -## الدقيقة 0–2 — الفكرة الكبرى +مرجع تنفيذي يطابق `GET /api/v1/launch/demo-script` في الكود. -> "Dealix ليس CRM ولا أداة واتساب. Dealix يقول لك من تكلم اليوم، لماذا، ماذا تقول، وماذا حدث بعد ذلك. كل قناة (واتساب، إيميل، تقويم، مدفوعات) تتحول إلى كرت قرار عربي، أنت توافق أو ترفض، ثم Proof Pack." +| الدقائق | المحتوى | API اختياري | +|--------|---------|-------------| +| 0–2 | المشكلة والوعد: ليس CRM وليس بوت واتساب فقط — إشارة → قرار → موافقة → Proof | — | +| 2–4 | Daily Brief للمدير | `GET /api/v1/personal-operator/daily-brief` | +| 4–6 | مهمات النمو / ١٠ فرص | `GET /api/v1/growth-operator/missions` | +| 6–8 | Inbox موحّد (كروت عربية) | `GET /api/v1/platform/inbox/feed` | +| 8–10 | برج الخدمات والأسعار التقديرية | `GET /api/v1/services/catalog` | +| 10–12 | Pilot، Proof Pack، الخطوة التالية | `GET /api/v1/launch/private-beta/offer` | -اعرض الصفحة الرئيسية: -``` -GET / -``` - -## الدقيقة 2–4 — Daily Brief - -``` -GET /api/v1/personal-operator/daily-brief -``` - -اعرض: -- 3 قرارات اليوم. -- فرص مفتوحة. -- مخاطر. -- launch readiness. - -> "كل صباح، Dealix يبني لك هذه القائمة. ما تفتح 8 تطبيقات." - -## الدقيقة 4–6 — Command Feed (Intelligence Layer) - -``` -GET /api/v1/intelligence/command-feed/demo -``` - -اعرض 6 بطاقات: opportunity / revenue_leak / partner_suggestion / meeting_prep / review_response / competitive_move. - -> "كل بطاقة فيها: لماذا الآن، الإجراء المقترح، الأثر المتوقع، 3 أزرار: قبول/تخطي/تعديل." - -## الدقيقة 6–8 — 10 فرص في 10 دقائق - -``` -GET /api/v1/intelligence/missions -POST /api/v1/intelligence/missions/recommend -``` - -> "هذه أول مهمة لكل عميل: 10 فرص B2B مناسبة بالعربي مع why-now ورسائل، خلال 10 دقائق." - -## الدقيقة 8–10 — Trust + Simulator + Proof - -``` -POST /api/v1/intelligence/trust-score -POST /api/v1/intelligence/simulate-opportunity -GET /api/v1/growth-operator/proof-pack/demo -``` - -اعرض: -- قبل أي إرسال، Trust Score (safe / needs_review / blocked). -- Simulator يحاكِ 100 جهة → expected pipeline + risk. -- Proof Pack: leads, drafts, meetings, risks_blocked, revenue_influenced. - -> "هذا هو الفرق: لا نرسل بدون trust. لا ننفّذ بدون simulator. لا نختفي بدون proof." - -## الدقيقة 10–12 — الأمان + التكاملات - -``` -GET /api/v1/security-curator/demo -GET /api/v1/connector-catalog/catalog -``` - -اعرض: -- أي token يدخل → يخرج كـ`***`. -- 14 تكامل، كل واحد له launch_phase + risk_level + blocked_actions. -- WhatsApp يحظر cold send افتراضياً. -- Moyasar لا يخزّن بطاقات. - -> "Dealix مبني على قاعدة: لا نضرّ سمعة العميل. هذه أهم نقطة بيع للسوق السعودي." - -## الإغلاق - -> "Pilot 7 أيام: 499 ريال أو مجاني مقابل case study. خلال أسبوع: 10 فرص + رسائل + متابعة + Proof Pack. مستعد نبدأ يوم الأحد؟" - -## القاعدة - -- لا تظهر API keys على الشاشة. -- لا تعرض staging credentials. -- لا تعد بأرقام لم تُحقَّق. -- لا تشغّل live WhatsApp send في الـdemo. +**جملة إغلاق:** لا نعد نتائج مضمونة — نعد مسودات، موافقات، وتقارير قياس. diff --git a/dealix/docs/EXECUTION_ROADMAP_AR.md b/dealix/docs/EXECUTION_ROADMAP_AR.md new file mode 100644 index 00000000..4a4227cd --- /dev/null +++ b/dealix/docs/EXECUTION_ROADMAP_AR.md @@ -0,0 +1,67 @@ +# خارطة تنفيذ — برج النمو (Growth Control Tower) — عربي + +> مرجع تنفيذي يربط الكود الحالي بالخطوات التالية. **MVP الحالي: مسودات، سياسات، عروض deterministic — بدون إرسال حي أو شحن تلقائي.** + +## المرحلة 0 — تثبيت (يوم 0–2) + +1. `python -m compileall` على جذر `dealix`. +2. `pytest` (الريبو يفعّل `--cov` افتراضياً؛ للسرعة: `pytest --no-cov tests/test_growth_tower_stack.py`). +3. `python scripts/print_routes.py` للتحقق من تضمين الراوترات الجديدة. +4. نشر staging ثم `python scripts/smoke_staging.py` (المسارات موسّعة في السكربت). + +## المرحلة 1 — دمج الواجهة والعرض (أسبوع 1–3) + +| مخرج | إجراء | +|------|--------| +| Inbox و Command cards | ربط الواجهة بـ `GET /api/v1/platform/inbox/feed` و`GET /api/v1/intelligence/command-feed/demo` حسب التصميم. | +| Proof موحّد | `GET /api/v1/platform/proof/overview` + روابط demo من `innovation` و`business`. | +| Growth operator في الوثائق | استخدام `GET /api/v1/growth-operator/*` للعرض مع احترام `canonical_route` في الاستجابة. | +| Targeting OS | [`TARGETING_ACQUISITION_OS.md`](TARGETING_ACQUISITION_OS.md) و`POST /api/v1/targeting/*` لاستهداف الحسابات والقوائم بشكل متوافق. | + +## المرحلة 2 — Curator + Security في مسار المراجعة (أسبوع 2–4) + +1. **قبل** أي لصق diff من الوكيل: `POST /api/v1/security-curator/inspect-diff`. +2. **قبل** تخزين/إرسال نصوص طويلة: `POST /api/v1/security-curator/redact`. +3. رسائل الصادر: `POST /api/v1/growth-curator/messages/grade` كبوابة جودة (لا تستبدل المراجعة البشرية للبيتا). +4. تنقية traces قبل المراقبة: `POST /api/v1/security-curator/trace/sanitize`. + +## المرحلة 3 — اجتماعات ومتابعة (أسبوع 3–6) + +- تدفق: لصق transcript → `POST /api/v1/meeting-intelligence/transcript/summarize` → اعتراضات → `POST .../followup/draft`. +- Pre-call: `POST .../brief/pre-meeting` مع JSON منظّم (شركة، جهة، فرصة). +- لا ربط تقويم حي من هذه المسارات في MVP. + +## المرحلة 4 — موجه النماذج والمزودين (متوازي) + +- استخدام `POST /api/v1/model-router/route` كطبقة **تلميحات** فقط؛ التوجيه الحقيقي يبقى في `core/llm/router.py` عند التشغيل. +- توثيق المزودين في الكود: `GET /api/v1/model-router/providers`. + +## المرحلة 5 — Connectors وبوابة الأدوات + +1. عرض للعملاء: `GET /api/v1/connectors/catalog` (مخاطر و`blocked_actions` واضحة). +2. تنفيذ أدوات من الوكيل فقط عبر `POST /api/v1/platform/tools/execute` — راجع `tool_gateway` لحالات `blocked` و`approval_required`. + +## المرحلة 6 — مراقبة وتقييم (staging → prod) + +- تشغيل evals على عينات: `POST /api/v1/agent-observability/eval/safety` و`.../eval/saudi-tone`. +- بناء أحداث trace متوافقة مع Langfuse: `POST .../trace/build` ثم ربط SDK في التطبيق عند الجاهزية. +- راجع [`AI_OBSERVABILITY_AND_EVALS.md`](AI_OBSERVABILITY_AND_EVALS.md). + +## المرحلة 7 — إطلاق بيتا خاص (قرار go/no-go) + +- [`PRIVATE_BETA_LAUNCH_TODAY.md`](PRIVATE_BETA_LAUNCH_TODAY.md) و[`BETA_PRIVATE_GATES_CHECKLIST.md`](BETA_PRIVATE_GATES_CHECKLIST.md). +- صفحة عرض: [`landing/private-beta.html`](../landing/private-beta.html). + +## المرحلة 8 — ما بعد البيتا (90 يوم) + +OAuth كامل لـ Gmail/Calendar، Moyasar live خلف موافقة وتدقيق، واتساب إنتاجي مع `WHATSAPP_ALLOW_LIVE_SEND`، أحداث platform في DB، وتقارير PDF من الـ ledger — كما في [`DEALIX_100_PERCENT_LAUNCH_PLAN.md`](DEALIX_100_PERCENT_LAUNCH_PLAN.md) القسم 3 و21. + +## تتبع المهام المقترح + +| أسبوع | تركيز | +|-------|--------| +| 1 | smoke + ربط UI للـ inbox وproof | +| 2–3 | security + growth curator في workflow المراجعة | +| 4–6 | meeting intelligence + نماذج أولى من pilot | +| 7–10 | connectors أولوية pilot (whatsapp, gmail, moyasar) مع OAuth | +| 11+ | Langfuse، SLOs، PDPL تشغيلية | diff --git a/dealix/docs/FIRST_20_OUTREACH_MESSAGES.md b/dealix/docs/FIRST_20_OUTREACH_MESSAGES.md index 8c486fa6..89feda1f 100644 --- a/dealix/docs/FIRST_20_OUTREACH_MESSAGES.md +++ b/dealix/docs/FIRST_20_OUTREACH_MESSAGES.md @@ -1,124 +1,14 @@ -# First 20 Outreach Messages — قوالب جاهزة +# أول ٢٠ رسالة تواصل (قوالب) -> كل رسالة عربية، طبيعية، تحت 80 كلمة، بدون "ضمان 100%"، ولا "آخر فرصة". -> كل رسالة تمر من `safety_eval` و`saudi_tone_eval` قبل الإرسال. +المصدر البرمجي: `GET /api/v1/launch/outreach/first-20` (`launch_ops/outreach_messages.py`). ---- +**قاعدة:** نسخ يدوي فقط — لا إرسال تلقائي من Dealix. -## 1. مؤسس → مؤسس (واتساب أو لينكدإن) +1. **مؤسس B2B:** تجربة بيتا — ١٠ فرص خلال أسبوع، موافقة قبل التواصل. +2. **وكالة:** Pilot مشترك على عميل واحد + Proof Pack. +3. **تدريب:** فرص شركات + لماذا الآن + تشخيص مجاني مختصر. +4. **SaaS صغير:** قائمة leads → تصنيف ومخاطر — بدون واتساب بارد. +5. **متابعة ١:** تذكير لطيف — قطاع + مدينة لعينة ٢٤ ساعة. +6. **متابعة ٢:** إغلاق مهذب أو جدولة لاحقة. -``` -هلا [الاسم]، أبني Dealix كـ مدير نمو عربي للشركات السعودية. -الفكرة: خلال 7 أيام نطلع لك 10 فرص B2B مناسبة، نكتب الرسائل بالعربي، -وأنت توافق أو ترفض قبل أي تواصل. -أفتح 5 مقاعد Pilot هذا الأسبوع. -يناسبك أعرض لك ديمو 12 دقيقة؟ -``` - -## 2. وكالة B2B (لينكدإن) - -``` -هلا [الاسم]، عندي فكرة ممكن تفيد وكالتكم وعملاءكم. -Dealix يطلع فرص B2B، يكتب الرسائل بالعربي، ويجهز متابعة وProof Pack — -بدون إرسال عشوائي. أبغى أجربها مع وكالة شريك Pilot: -نختار عميل عندكم ونطلع له 10 فرص خلال أسبوع. -مهتم تشوف ديمو 15 دقيقة؟ -``` - -## 3. شركة تدريب B2B (إيميل) - -``` -الموضوع: 10 فرص شركات لـ [اسم الشركة] خلال أسبوع - -هلا [الاسم]، -لاحظت أن [اسم الشركة] فتحت برامج جديدة للشركات. -نشتغل على Dealix كـ مدير نمو عربي: -- 10 فرص B2B مناسبة لقطاعكم -- رسائل عربية جاهزة -- متابعة 7 أيام -- Proof Pack بعد الأسبوع - -Pilot بـ 499 ريال أو مجاني مقابل case study. -يناسبك مكالمة 15 دقيقة الأسبوع القادم؟ -``` - -## 4. SaaS سعودية (لينكدإن DM) - -``` -هلا [الاسم]، رأيت إصدار النسخة الجديدة من [اسم المنتج] — -مبروك على التحديث. -نشتغل على مدير نمو عربي يطلع 10 فرص B2B خلال أسبوع -ويكتب الرسائل بالعربي ويتابع. -أبغى أجربه مع شركة SaaS سعودية واحدة. -يناسبك ديمو 12 دقيقة؟ -``` - -## 5. شركة عقار (واتساب) - -``` -هلا [الاسم]، نشتغل على Dealix: -نظام يطلع leads عقار مناسبين + يكتب رسائل تأهيل بالعربي -+ يحجز معاينات. أنت توافق على كل رسالة قبل الإرسال. -عندي 3 مقاعد Pilot للعقار هذا الشهر. -تحب أعرض لك ديمو سريع؟ -``` - -## 6. عيادة/متجر (واتساب) - -``` -هلا [الاسم]، نشتغل على نظام عربي للعيادات/المتاجر: -- يستعيد العملاء الخاملين -- يرد على تقييمات Google -- يجهز رسائل واتساب للحملات (بعد موافقتك) -- تقرير شهري بالعائد - -Pilot 7 أيام بـ 499 ريال. تحب نجرب؟ -``` - -## 7. مستشار نمو (لينكدإن) - -``` -هلا [الاسم]، متابع كتاباتك عن نمو B2B في السعودية. -أبني Dealix: مدير نمو عربي يطلع 10 فرص أسبوعياً، يكتب الرسائل، -ويجهز Proof Pack. أبغى رأيك في الـoffer قبل الإطلاق العام. -يناسبك مكالمة 20 دقيقة هذا الأسبوع؟ -``` - ---- - -## Follow-up 1 (بعد 3 أيام بدون رد) - -``` -هلا [الاسم]، لو الفكرة لا تناسب الآن خبرني وأرتاح. -وإذا فيه شي معين تبغى تشوفه قبل، قلي وأرسله لك. -``` - -## Follow-up 2 (بعد 7 أيام) - -``` -هلا [الاسم]، أعرف أن وقتك مزدحم. -سؤال أخير: لو طلعت لك 3 فرص B2B بالعربي مجاناً هذا الأسبوع، -تعطيني 15 دقيقة feedback؟ -``` - -## Follow-up 3 (إغلاق نهائي بعد أسبوعين) - -``` -هلا [الاسم]، أعتذر على الإلحاح. -أرشّفها وأكون موجود لو احتجتني لاحقاً. -شاكر لك. -``` - ---- - -## القواعد - -1. **اسم محدد** بدلاً من "العميل العزيز". -2. **سبب واضح** للتواصل ("لاحظت" / "رأيت" / "متابع"). -3. **سؤال مفتوح** في النهاية. -4. **لا** "ضمان 100%" أو "آخر فرصة". -5. **عرض محدد** (سعر + مدة + مخرجات). -6. **مخرج آمن** ("لو ما تناسب الآن خبرني"). -7. **حد أقصى 3 follow-ups** ثم أرشفة. - -كل رسالة تمر من `POST /api/v1/agent-observability/safety/eval` و`tone/eval` قبل الإرسال. +كرّر وأكيّف ٤–٦ لباقي الـ ٢٠ جهة بنفس النبرة الدافئة. diff --git a/dealix/docs/FIRST_PILOT_DELIVERY_WORKFLOW.md b/dealix/docs/FIRST_PILOT_DELIVERY_WORKFLOW.md index 2839468e..55e333da 100644 --- a/dealix/docs/FIRST_PILOT_DELIVERY_WORKFLOW.md +++ b/dealix/docs/FIRST_PILOT_DELIVERY_WORKFLOW.md @@ -1,260 +1,93 @@ -# First Pilot Delivery Workflow (48 ساعة) +# أول Pilot — مسار التسليم خلال 48 ساعة -> **القاعدة:** كل Pilot 499 ريال يُسلَّم خلال 48 ساعة. لا يتجاوز. لا live send. لا Moyasar API. لا scraping. كل خطوة approval-first. +**الغرض:** عند موافقة أول عميل على **Pilot 7 أيام — 499 ريال** (أو ما يعادله من الكتالوج)، تنفيذ متسلسل واضح — **مسودات أولاً**، **موافقة قبل أي تواصل حساس**، **Proof Pack** في النهاية. + +**مرتبط بـ:** [`PAID_BETA_OPERATING_PLAYBOOK.md`](PAID_BETA_OPERATING_PLAYBOOK.md)، [`CUSTOMER_SUCCESS_PLAYBOOK.md`](CUSTOMER_SUCCESS_PLAYBOOK.md)، واجهات API مثل `GET /api/v1/customer-ops/onboarding/checklist`. --- -## 1. الإطار العام +## المرحلة A — Intake (يوم 0، نفس يوم الدفع أو قبله بساعات) -``` -T+0 intake (15 دقيقة) -T+24 Free Diagnostic (3 فرص + رسالة + مخاطرة + توصية) -T+48 Pilot Delivery (10 فرص + رسائل + متابعة + Proof Pack) -T+7 Follow-up Wave (نتائج + اقتراح Growth OS أو case study) -``` +اجمع الحد الأدنى التالي (يمكن نسخه إلى نموذج أو Sheet): -**الهدف الفعلي:** أن يقول العميل "هذا أفضل من شغل وكالتنا الحالية" خلال 48 ساعة. +| الحقل | ملاحظة | +|--------|--------| +| `company_name` | | +| `website` | | +| `sector` | | +| `city` | | +| `offer` | العرض الرئيسي بجملة واحدة | +| `ideal_customer` | من هو المشتري المثالي | +| `average_deal_value` | تقدير نطاق | +| `has_contact_list` | نعم/لا — إن نعم، شكل الملف والمصدر | +| `preferred_channels` | إيميل / اجتماع / واتساب opt-in / غيره | +| `current_sales_process` | جملتان تكفيان | + +**بعد تأكيد الدفع (يدوي):** أرسل للعميل قائمة المطلوب أعلاه + توقيت التسليم (24 ساعة للتشخيص، 48 ساعة لمحتوى الـ 499 إن كان ذلك هو النطاق المتفق عليه). --- -## 2. T+0 — Intake (15 دقيقة) +## المرحلة B — التشخيص (خلال 24 ساعة من اكتمال الـ intake) -### الحقول المطلوبة +**المخرجات:** -``` -company_name مثال: "حلول الراحة للأثاث" -sector construction | clinics | logistics | f&b | retail | edtech | software | other -city الرياض / جدة / الدمام / الخبر / مكة / المدينة / الطائف -ticket_size_sar 5_000 | 25_000 | 100_000 | 500_000+ -contact_name اسم صاحب القرار -contact_role owner | gm | head_of_sales | head_of_marketing | other -icp_today وصف العميل المثالي اليوم (3 أسطر) -last_3_clients اسم القطاع + المدينة + حجم الصفقة (إن وُجد) -channels_used whatsapp | gmail | linkedin_lead_forms | website_forms | calls -data_in_hand crm | sheet | none -why_now لماذا الآن؟ (3 أسطر — أول inbound dropped, slow Q, agency churn, ...) -red_flags قطاعات/مناطق/أنواع لا يخدمها -opt_in_status هل عنده WhatsApp opt-in موثق؟ نعم/لا -``` +- **3 فرص** مؤهّلة مع «لماذا الآن» مختصر. +- **1 رسالة عربية** مسودة للمراجعة (لا إرسال تلقائي). +- **1 مخاطرة قناة** (مثلاً: contactability / سياسة القناة). +- **1 توصية خدمة** تالية (ترقية محتملة بعد الـ Pilot). -### مصدر الـ intake -- نموذج Google Form بسيط، أو -- محادثة WhatsApp/Email مكتوبة (نسخها يدوياً). - -### بعد الـ intake -1. سجّل العميل في `PRIVATE_BETA_OPERATING_BOARD.md`. -2. أنشئ مجلد `pilots//` فيه: `intake.md`, `diagnostic.md`, `pilot.md`, `proof_pack.md`. -3. أرسل تأكيد عربي: - > وصلني intake. سأرسل لك Free Diagnostic خلال 24 ساعة. فيه 3 فرص محددة + رسالة جاهزة + مخاطرة موجودة + توصية. بدون أي إرسال خارجي بدون موافقتك. +يمكن الاستعانة بمسارات المنتج التجريبية (`free_growth_diagnostic` في الكتالوج) كمرجع تنظيمي — التسليم للعميل يكون **ملفاً أو بريداً منظماً** حسب اتفاقك. --- -## 3. T+24 — Free Diagnostic +## المرحلة C — Pilot 499 (خلال 48 ساعة من بداية الـ Pilot المتفق عليها) -### المحتوى المطلوب -1. **3 فرص B2B محددة بأسماء حقيقية** - - اسم الشركة + قطاعها + مدينتها + سبب الاهتمام (why_now). - - كل فرصة لها صاحب قرار مرشح (اسم + دور). - - كل فرصة لها قناة موصى بها (whatsapp opt-in / gmail / website_form / linkedin lead form / call). +**المخرجات (نطاق Growth Starter):** -2. **رسالة عربية جاهزة (تحت 80 كلمة)** - - نبرة سعودية طبيعية (لا "تحية طيبة وبعد"، لا synergy). - - تستخدم اسم العميل + قطاعه + سبب التواصل. - - تنتهي بـ CTA واضح (مكالمة 12 دقيقة / لقاء قهوة / تجربة مجانية). - - تمر `safety_eval` + `saudi_tone_eval` قبل التسليم. - -3. **مخاطرة موجودة الآن** - - تسريب data، WhatsApp بدون opt-in، إيميل bounce rate عالي، رسالة فيها claim طبي/مالي ممنوع. - - مع توصية إصلاح من `incident_router` أو `support_sla`. - -4. **توصية خدمة واحدة من Service Tower** - - First 10 Opportunities Sprint (499) أو - - Growth Diagnostic Pro (1,500) أو - - Partnership Sprint (2,500) أو - - Growth OS Monthly (2,999/شهر). - -### Endpoints المستخدمة -``` -POST /api/v1/customer-ops/onboarding/checklist -POST /api/v1/service-excellence/review/all -POST /api/v1/operator/bundles -GET /api/v1/launch/private-beta/offer -``` - -### قالب Diagnostic (عربي) - -``` -Diagnostic — - -أهم 3 فرص لك هذا الأسبوع: -1. <اسم الشركة 1> — <قطاع> — <مدينة> - لماذا الآن: ... - صاحب القرار: ... - القناة: ... -2. ... -3. ... - -رسالة عربية جاهزة (تحت 80 كلمة): -"<الرسالة>" - -مخاطرة موجودة الآن: -- ... -التوصية: ... - -الخدمة الموصى بها: -- First 10 Opportunities Sprint — 499 ريال — يبدأ غداً. -- نسلّم: 10 فرص + 10 رسائل + خطة متابعة 7 أيام + Proof Pack. - -— Bassam -``` - -### بعد الإرسال -- حدّث `Operating Board`: `diagnostic_sent = today`, `next_step = pilot_offer`. -- تابع بعد 24 ساعة بقالب Follow-up #1. +- **10 فرص** مع سبب اختيار مختصر لكل منها. +- **why-now** لكل فرصة (سطر أو سطران). +- **صاحب القرار المحتمل** (لقب/دور — بدون تخمين غير مسؤول). +- **القناة المقترحة** (إيميل، طلب اجتماع، إلخ) — دائماً مع ذكر أن الإرسال يتطلب موافقته. +- **رسالة عربية** مسودة لكل فرصة أو مجموعات منطقية. +- **risk / contactability** لكل هدف حيث ينطبق. +- **خطة متابعة 7 أيام** (جدول أيام + إجراء مقترح يدوي). +- **Proof Pack مختصر** (انظر أدناه). --- -## 4. T+48 — Pilot Delivery 499 +## مرحلة D — Proof Pack (مع كل تسليم رئيسي) -### المحتوى المطلوب -1. **10 فرص B2B** - - كل فرصة فيها: company_name, sector, city, decision_maker, role, channel, why_now (3 أسطر), رسالة عربية جاهزة (تحت 80 كلمة), risk_score (0..100), contactability (1..5). +يُفضّل أن يتضمن القالب: -2. **خطة متابعة 7 أيام** - - يوم 1: الرسالة الأولى. - - يوم 3: رسالة متابعة #1 (لو ما رد). - - يوم 5: رسالة متابعة #2 (تحويل قناة لو احتاج — مثلاً WhatsApp إلى Email). - - يوم 7: قرار: keep / drop / nurture. +- `opportunities_created` (عدد / معرفات مختصرة) +- `drafts_created` +- `approvals_needed` (ما يحتاج قرار العميل) +- `risks_blocked` (ما تم رفضه أو تعليمه كـ unsafe) +- `recommended_next_action` +- `upgrade_offer` (خدمة تالية مقترحة بلا ضغط مبالغ فيه) -3. **Proof Pack مختصر** - ``` - opportunities_created: 10 - drafts_created: 10 - approvals_needed: 10 - risks_blocked: - recommended_next_action: - upgrade_offer: "نواصل شهرياً مقابل 2,999 ريال — أول شهر بسعر 1,999." - ``` - -### Endpoints المستخدمة -``` -POST /api/v1/operator/chat/message -POST /api/v1/customer-ops/connectors/summary -POST /api/v1/revenue-launch/payment/invoice-instructions -POST /api/v1/revenue-launch/proof-pack/template -GET /api/v1/service-excellence/review/all -``` - -### قالب Pilot Delivery (عربي مختصر) - -``` -First 10 Opportunities Sprint — - -10 فرص أولى لك: -1. ... | ... | ... | "<رسالة جاهزة>" -2. ... -... -10. ... - -خطة متابعة 7 أيام: -- يوم 1: إرسال أول دفعة (5 رسائل) بعد اعتمادك. -- يوم 3: متابعة الرسائل بدون رد. -- يوم 5: تحويل قناة لمن لم يرد (Email → WhatsApp opt-in). -- يوم 7: قرار keep/drop/nurture. - -Proof Pack: -- opportunities_created: 10 -- drafts_created: 10 -- approvals_needed: 10 (تنتظر اعتمادك) -- risks_blocked: - -التوصية بعد 7 أيام: -- Growth OS Monthly (2,999 ر.س/شهر) — نواصل من حيث وقفنا. -- أو Case Study مجاني مقابل اقتباس. - -— Bassam -``` - -### بعد الإرسال -- حدّث Operating Board: `pilot_offered = today`, `price = 499`, `paid = pending`. -- أرسل Moyasar invoice manual (URL يدوي). -- تابع تأكيد الدفع. +**لا تُدخل PII خام** في أدوات مراقبة عامة؛ التزم بـ redaction الداخلي. --- -## 5. T+7 — Follow-up Wave + Proof Pack النهائي +## نص اقتراح للعميل بعد الموافقة على Pilot 499 -### المحتوى -1. **Proof Pack نهائي** - - leads_count + drafts_approved + replies + meetings_booked + pipeline_sar + risks_blocked. - - chart مبسط: messages_sent vs replies vs meetings. - - أهم 3 رسائل اعتمدها العميل (مع التعديلات إن وُجدت). - - أهم 3 مخاطر تم منعها تلقائياً. +> تمام، نبدأ Pilot لمدة 7 أيام بـ 499 ريال. +> يشمل: 10 فرص مناسبة، رسائل عربية جاهزة للمراجعة، فحص مخاطر القنوات، خطة متابعة، Proof Pack مختصر. +> بعد الدفع أحتاج: رابط موقعكم، القطاع المستهدف، المدينة، العرض الرئيسي، وهل عندكم قائمة عملاء أو نبدأ بدون قائمة. -2. **جلسة مراجعة 30 دقيقة** - - "ما الذي اشتغل؟ ما الذي لم يشتغل؟" - - "نواصل شهرياً، ولا نوقف، ولا نحوّل لـ case study؟" - -3. **3 مسارات للترقية** - - **Growth OS Monthly** (2,999 ر.س/شهر) — استمرار شهري. - - **Partnership Sprint** (2,500 ر.س لمرة) — لو فيه شراكات قابلة. - - **Case Study + Referral** — مقابل اسم وكالة/عميل آخر يحتاج Pilot. - -### Endpoints المستخدمة -``` -POST /api/v1/customer-ops/cs/weekly-check-in -POST /api/v1/customer-ops/cs/success-plan -POST /api/v1/revenue-launch/proof-pack/template -GET /api/v1/service-excellence/review/all -``` +عدّل الصياغة لتطابق هويتك وعقدك؛ لا تعد نتائج مالية مضمونة. --- -## 6. ما لا يحدث في Pilot Delivery +## API تشغيلية (ديمو / داخلي) -- لا live WhatsApp send بدون env flag + اعتماد. -- لا live Gmail send بدون env flag + اعتماد. -- لا Moyasar charge من API — invoice/payment-link manual فقط. -- لا scraping LinkedIn — Lead Gen Forms + استرشادي فقط. -- لا cold WhatsApp بدون opt-in — PDPL hard-block. -- لا تجاوز 48 ساعة — لو فيه عذر، نعتذر بدل أن نتأخر. -- لا تخفيض السعر بدون موافقة المؤسس — 499 ثابت. -- لا توسيع scope في الـ Pilot — 10 فرص فقط، الزيادة في Growth OS. +للتحقق من حالة التشغيل اليومية للفريق: + +- `GET /api/v1/customer-ops/onboarding/checklist` +- `GET /api/v1/customer-ops/connectors/status` +- `GET /api/v1/customer-ops/support/sla` --- -## 7. شروط نجاح Pilot - -- [ ] Diagnostic سُلّم خلال 24 ساعة. -- [ ] Pilot سُلّم خلال 48 ساعة. -- [ ] العميل اعتمد ≥3 رسائل من العشرة. -- [ ] Proof Pack وصل خلال 7 أيام. -- [ ] جلسة مراجعة 30 دقيقة تمت. -- [ ] 1+ case study أو 1+ Growth OS subscription. -- [ ] CSAT ≥ 8/10. - ---- - -## 8. مقاييس Pilot في Operating Board - -| Metric | Target | -|--------|-------:| -| Pilot delivered ≤ 48h | 100% | -| Drafts approved (من 10) | ≥3 | -| Replies received | ≥1 | -| Meetings booked | ≥1 | -| Risks blocked | ≥1 | -| Upsell offered | 100% | -| Upsell accepted | ≥30% | -| CSAT | ≥8/10 | - ---- - -## 9. القرار النهائي - -``` -Pilot ليس "محاولة بيع". -Pilot هو "أول إثبات أن Dealix يعمل لشركتك". -لو سُلّم في 48 ساعة + ≥3 رسائل اعتمدت + 1 رد + 1 اجتماع = -هذا Growth OS subscription بأعلى احتمال. -``` +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/FRONTEND_AND_API_MAP.md b/dealix/docs/FRONTEND_AND_API_MAP.md new file mode 100644 index 00000000..9ac0717f --- /dev/null +++ b/dealix/docs/FRONTEND_AND_API_MAP.md @@ -0,0 +1,57 @@ +# خريطة الواجهة (Landing) وواجهة البرج والتحكم — Dealix + +> الغرض: يعرف المشغّل أو الشريك التقني **أي صفحة HTML تغذيها أي مسارات API**، دون البحث في الريبو. المسارات المرجعية الكاملة: [`architecture/API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md). + +## مبدأ البنية + +| الطبقة | الموقع | الدور | +|--------|--------|--------| +| واجهة تسويق / عرض | [`landing/*.html`](../landing/) | HTML ثابت، غالباً `lang="ar"` و`dir="rtl"`. نسخ إنجليزية مختصرة: `*-en.html`. | +| برج النمو (Growth Control Tower) | طبقات `auto_client_acquisition` + مسارات `/api/v1/platform/*`، `/api/v1/intelligence/*`، `/api/v1/growth-operator/*`، `/api/v1/services/*`، `/api/v1/targeting/*` | قرار، Inbox، مهام، خدمات، استهداف — **بدون SPA واحد** في الريبو. | +| لوحة إيرادات (عرض) | [`command_center`](../api/routers/command_center.py) `GET /api/v1/command-center/*` | تغذية لوحة الإيرادات؛ صفحة [`command-center.html`](../landing/command-center.html) توثّق المسار `snapshot` كنموذج. | +| نظام v3 | [`v3`](../api/routers/v3.py) `/api/v1/v3/*` | طبقة «Revenue OS» تجريبية: وكلاء، امتثال، رادار، ذاكرة. | +| إطلاق البيتا والتحصيل | [`launch_ops`](../api/routers/launch_ops.py)، [`revenue_launch`](../api/routers/revenue_launch.py) | عروض، go/no-go، دفع يدوي، Proof — انظر [`REVENUE_TODAY_PLAYBOOK.md`](REVENUE_TODAY_PLAYBOOK.md). | + +## جدول: صفحة → مسارات API ذات صلة + +| صفحة (landing) | لغة | مسارات API (أمثلة) | ملاحظة | +|----------------|-----|----------------------|--------| +| [`index.html`](../landing/index.html) | ar | `/health`، روابط عامة | بوابة الموقع. | +| [`private-beta.html`](../landing/private-beta.html) | ar | `GET /api/v1/launch/private-beta/offer`، `GET /api/v1/revenue-launch/offer`، `GET /api/v1/growth-operator/missions`، `POST /api/v1/operator/chat/message`، `GET /api/v1/revenue-os/company-os/command-feed/demo` | تدشين Pilot + **Company OS**؛ [`private-beta-en.html`](../landing/private-beta-en.html) للإنجليزية. | +| [`private-beta-en.html`](../landing/private-beta-en.html) | en | نفس المسارات أعلاه | نسخة مختصرة LTR. | +| [`services.html`](../landing/services.html) | ar | `GET /api/v1/services/catalog`، `POST /api/v1/services/recommend` | برج الخدمات؛ [`services-en.html`](../landing/services-en.html). | +| [`services-en.html`](../landing/services-en.html) | en | نفس المسارات | | +| [`command-center.html`](../landing/command-center.html) | ar (محتوى مختلط في العناوين) | `GET /api/v1/command-center/snapshot?customer_id=...`، باقي مسارات [`command_center.py`](../api/routers/command_center.py) | عرض تسويقي؛ [`command-center-en.html`](../landing/command-center-en.html) مركز روابط EN. | +| [`command-center-en.html`](../landing/command-center-en.html) | en | نفس `command-center` + `GET /api/v1/v3/command-center/snapshot` إن وُجد في smoke | | +| [`free-diagnostic.html`](../landing/free-diagnostic.html) | ar | `POST /api/v1/targeting/free-diagnostic` | | +| [`first-10-opportunities.html`](../landing/first-10-opportunities.html) | ar | `POST /api/v1/intelligence/missions/first-10-opportunities` أو `POST /api/v1/innovation/opportunities/ten-in-ten` | انظر aliases. | +| [`list-intelligence.html`](../landing/list-intelligence.html) | ar | `POST /api/v1/targeting/uploaded-list/analyze` | | +| [`growth-os.html`](../landing/growth-os.html) | ar | `GET /api/v1/services/catalog`، مسارات Growth OS في الخدمات | | +| [`agency-partner.html`](../landing/agency-partner.html) | ar | `GET /api/v1/services/contracts/templates` | | +| [`launch-readiness.html`](../landing/launch-readiness.html) | ar | `GET /api/v1/launch/go-no-go`، `scripts/launch_readiness_check.py` | | + +## لغة الـ API التجريبية + +- **`GET /api/v1/revenue-launch/offer?lang=en`**: يضيف حقول `title_en` / `summary_en` (وأمثلة مشابهة) **إلى جانب** الحقول العربية `_ar` — لا يزيل الحقول العربية. + +## تشغيل محلي للتحقق + +من مجلد `dealix`: + +```bash +python scripts/smoke_inprocess.py +python scripts/launch_readiness_check.py +``` + +Staging (يتطلب `STAGING_BASE_URL`): + +```bash +python scripts/smoke_staging.py --base-url https:// +``` + +## وثائق مرتبطة + +- [`STAGING_DEPLOYMENT.md`](STAGING_DEPLOYMENT.md) +- [`PRIVATE_BETA_LAUNCH_TODAY.md`](PRIVATE_BETA_LAUNCH_TODAY.md) +- [`DEALIX_100_PERCENT_LAUNCH_PLAN.md`](DEALIX_100_PERCENT_LAUNCH_PLAN.md) +- [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md) — فئة المنتج والمسارات `/api/v1/operator/*` و`/api/v1/revenue-os/company-os/*` diff --git a/dealix/docs/GROWTH_CURATOR_STRATEGY.md b/dealix/docs/GROWTH_CURATOR_STRATEGY.md index 18670a16..9754f279 100644 --- a/dealix/docs/GROWTH_CURATOR_STRATEGY.md +++ b/dealix/docs/GROWTH_CURATOR_STRATEGY.md @@ -1,98 +1,25 @@ -# Growth Curator Strategy — مدير التحسين الذاتي للنمو +# Growth Curator Strategy -> **الفكرة (مستلهمة من Hermes Curator):** كل أسبوع، Dealix يراجع ما كتبه ونفذه، يدمج المتشابه، يأرشف الضعيف، ويقترح المهمة التالية. لا يحتاج المالك أن يفكر كل أسبوع "ماذا أحسّن؟". +هدف الطبقة: **رفع جودة الصادر** (واتساب، إيميل، اجتماع متابعة) دون المطالبة بوعود غير قابلة للدفاع أو انتهاك سياسات القناة. -## 1. الوحدات +## كود -| الوحدة | الدور | -|--------|------| -| `message_curator` | يقيّم كل رسالة عربية (0..100) ويحدد publish/needs_edit/reject. يكشف العبارات المخاطرة + يقترح صيغة بديلة. | -| `playbook_curator` | يقيّم playbooks بناءً على outcomes (accept/reply/meeting/deal) ويُصنّف winner/promising/needs_work/candidate_archive. | -| `mission_curator` | يقيّم نتائج الميشن (TTV, opportunities, drafts, meetings, revenue) ويقرر ship_it_widely/iterate/rework. | -| `skill_inventory` | فهرس deterministic لكل قدرات Dealix (20+ skill عبر 5 طبقات). | -| `curator_report` | تقرير عربي أسبوعي يجمع الكل. | +- `auto_client_acquisition/growth_curator/message_curator.py` — `grade_message` +- `auto_client_acquisition/growth_curator/playbook_curator.py` — قواعد مساعدة للقطاعات +- `auto_client_acquisition/growth_curator/curator_report.py` — `build_weekly_curator_report` -## 2. Message Grading +## API -`grade_message(text, sector, channel)` يفحص: -- محتوى عربي (≥30%). -- طول معقول (12-80 كلمة). -- خلوّ من 8 عبارات محظورة (ضمان 100%, آخر فرصة، ...). -- إشارات أسلوب طبيعي سعودي (تحية + لاحظت/شفت + يناسبك/تحب). -- WhatsApp: لا "عميل عزيز" ولا "لجميع العملاء". -- bonus لذكر القطاع. +- `GET /api/v1/growth-curator/report/demo` — تقرير أسبوعي شكلي للعرض +- `POST /api/v1/growth-curator/messages/grade` — `{ "message_ar": "...", "sector": "", "channel": "whatsapp" }` +- `GET /api/v1/growth-curator/skills/demo` — جرد مهارات/قوالب (درجات MVP) +- `GET /api/v1/growth-curator/missions/curate/demo` — اقتراحات دمج/أرشفة أسبوعية (لا حذف تلقائي) -## 3. Playbook Scoring +## تكامل مقترح -``` -score = 100 * ( - 0.10 * accept_rate -+ 0.20 * reply_rate -+ 0.30 * meeting_rate -+ 0.40 * deal_rate -) -``` +1. بعد توليد مسودة من الذكاء: استدعاء `grade_message`؛ إذا انخفض `score` عن عتبة المنتج، أظهر تحذيراً ولا تعرض زر «إرسال» في البيتا. +2. ربط العتبات مع `channel_registry.blocked_actions` (منع جماعي بارد، إلخ). -تيرز: -- ≥70: **winner** -- ≥40: **promising** -- ≥20: **needs_work** -- <20: **candidate_archive** +## اختبارات -استراتيجية الـrecommend: **promising أولاً** (winners مشبعة)، ثم winner، ثم بقية الـtiers. - -## 4. Mission Scoring - -`score_mission` يجمع: -- opportunities × 2 (max 20) -- drafts_approved × 4 (max 20) -- meetings_booked × 5 (max 20) -- revenue / 5,000 (max 20) -- risks_blocked × 5 (max 10) -- TTV ≤10min: +10، ≤60min: +5 - -## 5. Mission Recommender - -- لو ما شُغّل `first_10_opportunities` → ابدأ به. -- لو الأولوية `fill_pipeline` → `meeting_booking_sprint`. -- لو `rescue_lost_revenue` → `revenue_leak_rescue`. -- لو `expand_partners` → `partnership_sprint`. -- الافتراضي: `customer_reactivation`. - -## 6. Weekly Curator Report - -`build_weekly_curator_report(messages, playbooks, missions, sector)` يُرجع: - -```json -{ - "summary_ar": [ - "تمت مراجعة 24 رسالة، 5 playbook، و2 مهمة هذا الأسبوع.", - "تم اقتراح أرشفة 4 رسالة ضعيفة الجودة.", - "تم اكتشاف 3 أزواج رسائل متشابهة (للدمج).", - ], - "messages": {"total", "publishable", "needs_edit", "to_archive", "duplicate_pairs"}, - "playbooks": {"total", "winners", "promising", "to_merge_groups"}, - "missions": {"total", "ship_it_widely", "iterate", "rework_or_retire"}, - "next_playbook": {"recommended_id", "title_ar", "reason_ar"}, - "recommended_next_action_ar": "..." -} -``` - -## 7. Endpoints - -``` -GET /api/v1/growth-curator/skills/inventory -POST /api/v1/growth-curator/messages/grade -POST /api/v1/growth-curator/messages/improve -POST /api/v1/growth-curator/messages/duplicates -POST /api/v1/growth-curator/missions/next -POST /api/v1/growth-curator/report/weekly -GET /api/v1/growth-curator/report/demo -``` - -## 8. حدود - -- لا يصدر LLM call. -- لا يحذف playbooks تلقائياً — يقترح فقط. -- لا يدمج بدون موافقة. -- التقرير يبقى actionable: ≤7 أسطر summary. +`tests/test_growth_tower_stack.py` — `test_grade_message_detects_guarantee`. diff --git a/dealix/docs/INCIDENT_RESPONSE.md b/dealix/docs/INCIDENT_RESPONSE.md index 2bcf4147..606ef1e5 100644 --- a/dealix/docs/INCIDENT_RESPONSE.md +++ b/dealix/docs/INCIDENT_RESPONSE.md @@ -1,111 +1,11 @@ -# Dealix Incident Response +# الاستجابة للحوادث (Pilot) -> **القاعدة:** أي incident يمر بـ triage → severity → response plan → audit. أي تسريب بيانات أو إرسال غير معتمد = SEV1 تلقائي. +1. تصنيف P0–P3 وفق [`SUPPORT_SLA.md`](SUPPORT_SLA.md). +2. إيقاف أي إجراء live مشبوه حتى التحقق. +3. توثيق الوقت والتأثير والخطوات (بدون أسرار أو PII خام في السجلات العامة). +4. إشعار العميل بلغة واضحة وخطة تعافي. +5. مراجعة لاحقة وتحديث السياسات أو الاختبارات إن لزم. ---- +**مرجع API:** `GET /api/v1/customer-ops/incidents/playbook` -## Severities - -| Severity | الوصف | First Action | Comm Cadence | -|----------|------|-------------:|-------------:| -| **SEV1** | تسريب أمان / إرسال غير معتمد / تعطل كامل | 15 دقيقة | كل 30 دقيقة | -| **SEV2** | خدمة معطلة لـ ≥5 عملاء | 30 دقيقة | كل ساعة | -| **SEV3** | تأثير محدود (عميل واحد / degraded) | 2 ساعة | كل 4 ساعات | - ---- - -## Triage Logic - -```python -if has_data_leak or has_unauthorized_send: - severity = "SEV1" -elif affected_customers >= 5: - severity = "SEV2" -else: - severity = "SEV3" -``` - -**Endpoints:** -- `POST /api/v1/customer-ops/incidents/triage` -- `GET /api/v1/customer-ops/incidents/response-plan/{severity}` - ---- - -## Canonical Response Plan (مشترك) - -1. **تجميد** الـ live actions على القناة المعنية فوراً. -2. **إخطار** المؤسس + on-call operator. -3. **إنشاء** incident channel مع timeline. -4. **مراجعة** Action Ledger للأفعال المرتبطة. -5. **إذا تسريب**: إخطار العملاء المتأثرين خلال 72 ساعة (PDPL). - ---- - -## SEV1 Additional Steps - -6. تواصل مباشر مع المؤسس + خلية أزمة. -7. كتابة post-mortem خلال 24 ساعة. -8. مراجعة قانونية إن لزم (DPA + PDPL implications). - ---- - -## SEV2 Additional Steps - -6. تحديث العملاء المتأثرين كل 60 دقيقة. -7. post-mortem خلال 48 ساعة. - ---- - -## SEV3 Additional Steps - -6. تحديث العميل المتأثر مع كل خطوة. -7. post-mortem اختياري (موصى به للأنماط المتكررة). - ---- - -## Post-Mortem Template - -``` -1. ملخص الحادث -2. timeline (timestamps) -3. السبب الجذري -4. ما اشتغل صح -5. ما اشتغل غلط -6. الـ action items للوقاية -7. الـ owner لكل action item -8. الـ deadline -``` - ---- - -## Communication Templates (Arabic) - -### SEV1 — أول ساعة -> اكتشفنا حدث أمني/تشغيلي يتعلق بـ [نوع الحادث]. أوقفنا الـ live actions على القناة المتأثرة. نتواصل معك خلال 30 دقيقة بتحديث. - -### SEV1 — تسريب بيانات -> نأسف. اكتشفنا تسريب بيانات يتعلق بـ [نوع البيانات]. نراجع الأثر الآن وسنتواصل معك خلال 24 ساعة بتفاصيل + خطوات الحماية. PDPL يلزم بالإبلاغ خلال 72 ساعة لذا سنحرص على إعلامك بكل ما نعرفه. - -### SEV2 -> خدمة [اسم الخدمة] متعطلة جزئياً. الفريق يعمل على الإصلاح ونتوقع الاستعادة خلال [وقت]. سنحدثك كل ساعة. - ---- - -## Auto-actions - -- **Dealix يجمد القناة تلقائياً** عند detection على: - - bounce_rate > 5% - - complaint_rate > 0.3% - - block_rate WhatsApp > 3% -- **Dealix يخطر المؤسس** على أي SEV1 detected. -- **Dealix يضيف entry لـ Action Ledger** لكل incident. - ---- - -## Permission to publish - -- Post-mortems خاصة لـ SEV1 لا تُنشر علناً إلا بعد: - - مراجعة قانونية. - - موافقة العملاء المتأثرين. - - إزالة كل PII. -- Post-mortems لـ SEV2/SEV3 يمكن نشرها كـ engineering blog لو مفيدة. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md b/dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md index b0eff651..70d3b1ac 100644 --- a/dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md +++ b/dealix/docs/INTELLIGENCE_LAYER_STRATEGY.md @@ -1,269 +1,34 @@ -# Intelligence Layer Strategy — الشبكة العصبية للنمو -## (Dealix Growth Neural Network) +# استراتيجية Intelligence Layer — Growth Control Tower -> **الهدف:** تحويل Dealix من "منصة multi-channel" إلى **شبكة عصبية للنمو** تتعلم من قرارات صاحب النشاط، تستخرج DNA الإيرادات، وتعمل ميشنات نمو ذاتية بدلاً من الانتظار للمستخدم. +## الهدف ---- +طبقة رفيعة تحت `/api/v1/intelligence/*` تُخرج هياكل JSON **deterministic** (بدون LLM إلزامي في MVP): Growth Brain، Trust Score، Revenue DNA، محاكاة فرص، موجز للمجلس، حركات تنافسية، و**Intel Command Feed** منفصل عن مسار `innovation/command-feed` لتفادي التعارض في الأسماء. -## 1. لماذا Intelligence Layer؟ +## مكوّنات الكود -Platform Services أعطتنا **القنوات + الأمان + الـledgers**. لكن: -- لا تتذكر ما يفضله المستخدم. -- لا تستخرج رؤى من الفائزين/الخاسرين. -- لا تقترح بطاقات قرار جاهزة كل صباح. -- لا تحاكي قبل ما ترسل. +| وحدة | وظيفة MVP | +|------|-----------| +| `growth_brain` | بناء ملف شركة/نمو من JSON مدخل | +| `intel_command_feed` | بطاقات إضافية: تسرب إيراد، موجز مجلس — من بيانات ثابتة/مدخلات | +| `trust_score` | درجة ثقة 0–100 مع عوامل | +| `revenue_dna` | لقطات بنية إيراد (قنوات، دورة، مخاطر) | +| `opportunity_simulator` | سيناريوهات رقمية بسيطة | +| `board_brief` | فقرة تنفيذية للإدارة | +| `competitive_moves` | قائمة مقترحات تنافسية آمنة عرضياً | +| `action_graph` | مخطط signal→proof عبر `POST /api/v1/intelligence/action-graph/demo` | +| `mission_engine` | كتالوج مهمات + روابط canonical عبر `GET /api/v1/intelligence/missions/catalog` | +| `decision_memory` | سجل قرارات in-memory عبر `.../decision-memory/*` | -Intelligence Layer هي الطبقة التي تجعل المنصة "تشتغل لوحدها أثناء نوم المستخدم". +## الربط بـ Innovation / Proof ---- +- **Ten-in-ten:** المسار `POST /api/v1/innovation/opportunities/ten-in-ten` يبقى المصدر؛ يمكن للـ intelligence استدعاء `build_ten_opportunities` داخلياً عند الحاجة (`include_ten_in_ten` اختياري في جسم الطلب). +- **Proof:** مفاهيم «إثبات الإيراد» تبقى متسقة مع [`INNOVATION_STRATEGY.md`](INNOVATION_STRATEGY.md) ودفتر الابتكار؛ طبقة الذكاء **لا تستبدل** سجل الأحداث الدائم. -## 2. الوحدات (10 modules) +## Action Graph -| # | الوحدة | الدور | -|---|--------|------| -| 1 | `growth_brain` | Brain لكل عميل: قطاع، قنوات، أهداف، تفضيلات، مؤشرات. `is_ready_for_autopilot()`. | -| 2 | `command_feed` | بطاقات قرار يومية بالعربي (opportunity / revenue_leak / partner_suggestion / meeting_prep / review_response / competitive_move). | -| 3 | `action_graph` | رسم بياني للنوع: signal → action → outcome (10 أنواع حواف). | -| 4 | `mission_engine` | 7 ميشنات نمو، أهمها **Kill Feature: "10 فرص في 10 دقائق"**. | -| 5 | `decision_memory` | يتعلم من Accept / Skip / Edit / Block ويخرج preferences. | -| 6 | `trust_score` | مقياس مركّب لكل رسالة (source + opt_in + channel + content + freq + approval). | -| 7 | `revenue_dna` | يستخرج: أفضل قناة، أفضل segment، أفضل angle، أكثر اعتراض، متوسط دورة البيع. | -| 8 | `opportunity_simulator` | محاكي إلى الأمام: target_count → expected_replies/meetings/deals/pipeline_sar. | -| 9 | `competitive_moves` | رصد + رد على حركات المنافسين (price_change / new_offer / hire / funding / launch...). | -| 10 | `board_brief` | Founder Shadow Board — موجز أسبوعي: قرارات، فرص، مخاطر، علاقة، تجربة، مؤشر. | +رسم بياني موجّه (عُقد/حواف) يُعاد كـ JSON في MVP عبر `action-graph/demo`. التنفيذ المتين لاحقاً مع [`AGENT_WORKFLOW_ARCHITECTURE.md`](AGENT_WORKFLOW_ARCHITECTURE.md) دون LangGraph حتى موافقة صريحة. ---- +## مخاطر -## 3. Growth Brain - -`build_growth_brain(payload)` يبني سجل لكل عميل: -``` -customer_id, sector, regions, channels_connected, -preferred_tone, growth_priorities, -learning_signal_count, accept_rate_30d -``` - -**الجاهزية للأوتوبايلوت:** -``` -ready = (learning_signal_count ≥ 30) - AND (accept_rate_30d ≥ 0.40) - AND (≥ 1 قناة موصولة) -``` - -قبل الجاهزية → **draft + approval فقط**. - ---- - -## 4. Command Feed (يومي) - -بطاقات بالعربي مع ≤3 أزرار، 9 أنواع: -``` -opportunity, revenue_leak, partner_suggestion, -meeting_prep, review_response, ai_visibility_alert, -competitive_move, customer_reactivation, action_required -``` - -`build_command_feed_demo()` يرجع 6 بطاقات تجريبية واقعية. - ---- - -## 5. Action Graph - -أنواع الحواف الـ10: -``` -signal_created_opportunity, message_triggered_reply, -reply_led_to_meeting, meeting_led_to_proposal, -proposal_led_to_payment, partner_suggestion_taken, -review_response_recovered_customer, approval_allowed_send, -blocked_action_prevented_risk, content_generated_lead -``` - -`what_works_summary(customer_id)` يُرجع: مجموع الحواف + توزيعها بالنوع → "ما الذي يعمل فعلاً". - ---- - -## 6. Mission Engine — 7 ميشنات - -| ID | الاسم | ملاحظات | -|----|-------|---------| -| **first_10_opportunities** ⭐ | 10 فرص في 10 دقائق | **Kill Feature** — يبدأ من 0 ويُسلم 10 leads بالعربي قبل أن يعتاد المستخدم على المنصة. | -| revenue_leak_rescue | استعادة الإيرادات المتسربة | عملاء توقفوا، فواتير معلقة. | -| partnership_sprint | سبرنت شراكات | Partner Graph — اقتراحات تكامل. | -| customer_reactivation | إعادة تنشيط عملاء | فترة سكون → رسالة دافئة. | -| meeting_booking_sprint | حجز اجتماعات | drafts للجدولة + اعتماد. | -| ai_visibility_sprint | Answer Engine Optimization | ظهور النشاط في Perplexity / ChatGPT / Gemini. | -| competitive_response | الرد على حركات المنافسين | يُفعّل عند رصد price_change / launch / funding. | - -`recommend_missions(brain, limit=3)` يرتّب بحسب توافق القطاع + القنوات + الأولويات. - ---- - -## 7. Decision Memory - -يتعلم من 4 قرارات: `accept / skip / edit / block`. - -`preferences()` يُرجع: -``` -accept_rate, samples, -preferred_channels, preferred_tones, preferred_sectors, -rejected_action_types -``` - -يستخدمها `mission_engine` لرفع/خفض ترتيب البطاقات → الـ "warm-up" loop. - ---- - -## 8. Trust Score - -نتيجة 0..100 + verdict (`safe ≥70` / `needs_review 40-69` / `blocked <40`). - -العوامل: -- `source_quality` (customer / opt_in_lead / referral / cold / unknown). -- `opt_in` (boolean). -- `channel` risk (whatsapp risk أعلى من email). -- محتوى الرسالة (عبارات محظورة: "ضمان 100%", "آخر فرصة"...). -- `frequency_count_this_week` vs `weekly_cap`. -- `approval_status`. - -تطبيق فوري: قبل أي `tool_gateway.invoke_tool` → بطاقة في الـCommand Feed بدلاً من الإرسال. - ---- - -## 9. Revenue DNA - -`extract_revenue_dna(customer_id, won_deals, replies, objections)` يُرجع: -``` -best_channel, best_segment, best_message_angle, -common_objection, avg_cycle_days, -deals_observed, next_experiment_ar -``` - -استعمال: ميشن `revenue_dna_demo` يُري المالك "هذا ما يفوز فعلاً عندك". - ---- - -## 10. Opportunity Simulator - -`simulate_opportunity(target_count, sector, avg_deal_value_sar, channel, cold_pct, quality_lift)`: - -يُرجع: -``` -expected_replies, expected_meetings, expected_deals, -expected_pipeline_sar, risk_score (0..100), -risks_ar, rates_used, approval_required=True -``` - -9 قطاعات سعودية مهيّأة (real_estate, saas, retail, food, education, healthcare, logistics, fintech, contracting). - -**استعمال حرج:** تحاكِ قبل ما تنفّذ → "مع 100 جهة، النتيجة المتوقعة 6 صفقات بقيمة 300K، مخاطرة PDPL متوسطة لو 60% بارد". - ---- - -## 11. Competitive Moves - -8 أنواع حركات: `price_change, new_offer, new_hire, funding, launch, partnership, layoffs, expansion`. - -`analyze_competitive_move(competitor_name, move_type, payload)` → urgency + Arabic recommended_action + approval_required. - -مثال: price_change بـ-25% → urgency `high` + اقتراح بطاقة "أرسل عرض مضاد للعملاء المترددين". - ---- - -## 12. Board Brief — Founder Shadow Board - -`build_board_brief()` يُرجع موجز أسبوعي: -``` -decisions_required_ar (3), -top_opportunities_ar (3), -top_risks_ar (3), -key_relationship_ar, -experiment_to_run_ar, -metric_to_watch_ar, -money_summary -``` - -استعمال: ميل أسبوعي يومي الأحد 7:00 ص → "هذا ما يحتاج قراركم هذا الأسبوع، وهذا ما يكشفه الذكاء الاصطناعي". - ---- - -## 13. Endpoints (`/api/v1/intelligence/...`) - -``` -POST /growth-brain/build -GET /command-feed/demo -GET /missions -POST /missions/recommend -POST /trust-score -GET /revenue-dna/demo -POST /revenue-dna -POST /simulate-opportunity -POST /competitive-move/analyze -GET /board-brief/demo -POST /decisions/record -GET /decisions/preferences -``` - ---- - -## 14. اختبارات - -`tests/unit/test_intelligence_layer.py` — تغطية لكل الوحدات الـ10: -- growth brain autopilot threshold -- command feed Arabic + ≤3 buttons + critical types -- action graph add/summary + unknown edge type raises -- missions list + kill feature + recommend -- decision memory records/aggregates/empty/invalid -- trust score (cold blocked, safe, risky phrases, freq cap lowers) -- revenue DNA best channel + defaults -- simulator pipeline + cold_pct warning + unknown sector default -- competitive move urgency + unknown type + funding action -- board brief structure (3 من كل: قرار/فرصة/مخاطرة) - ---- - -## 15. ما لا تفعله هذه الطبقة - -- **لا** ترسل أي شيء فعلياً (تحت سقف tool_gateway). -- **لا** تتجاوز سياسات platform_services. -- **لا** تستخدم بيانات بدون consent. -- **لا** تنفذ ميشن بدون اعتماد المالك (إلا بعد `is_ready_for_autopilot()`). - ---- - -## 16. الاندماج مع Platform Services - -``` -Platform Services Intelligence Layer -──────────────── ──────────────────── -event_bus ←→ action_graph (يستهلك الأحداث) -identity ←→ growth_brain (هوية → سياق) -channel_registry ←→ simulator (rates_used per channel) -action_policy ←→ trust_score (verdict → policy gate) -tool_gateway ←→ command_feed (cards تُنفّذ عبر gateway) -unified_inbox ←→ command_feed (نفس البنية، طبقة أعلى) -action_ledger ←→ decision_memory (يقرأ الـledger) -proof_ledger ←→ board_brief (money_summary مصدره proof) -service_catalog ←→ mission_engine (الميشنات → خدمات قابلة للبيع) -``` - ---- - -## 17. الـ Kill Feature - -**"10 فرص في 10 دقائق"** — `first_10_opportunities`: - -1. عند بدء العميل، نسأل: قطاع + منطقة + قناة مفضلة. -2. خلال 10 دقائق نُسلم 10 بطاقات `opportunity` بالعربي مع `recommended_action_ar`. -3. كل بطاقة draft → اعتماد → تنفيذ. -4. إذا قبل المالك ≥4 → نزيد signal_count + accept_rate → نقترب من autopilot. - -هذه الميزة تكسر "blank canvas problem" وتُري قيمة فورية قبل أن يفتح المستخدم WhatsApp Web. - ---- - -## 18. ما يلي - -- ربط `command_feed` بإشارات حقيقية (Gmail / WA Business / GBP / website forms). -- استبدال الـin-memory `_MEMORY` بـ Supabase. -- جدولة `board_brief` يوم الأحد 7 ص (Cron + email/WhatsApp). -- شحن أول 100 عميل تحت "Approval-First" لجمع أول 3,000 قرار → تدريب decision_memory الحقيقي. +- ازدواج مع `innovation`: يُحل بالأسماء (`intel_command_feed`) واختبارات عقد API. +- توقعات زائدة: التسمية «Intelligence» لا تعني نماذج توليدية في هذا الإصدار. diff --git a/dealix/docs/MEETING_INTELLIGENCE.md b/dealix/docs/MEETING_INTELLIGENCE.md index 78a40bf0..2164e99c 100644 --- a/dealix/docs/MEETING_INTELLIGENCE.md +++ b/dealix/docs/MEETING_INTELLIGENCE.md @@ -1,94 +1,23 @@ -# Meeting Intelligence — ذكاء الاجتماعات +# Meeting Intelligence -> Pre-meeting brief + transcript summary + objection extraction + post-meeting follow-up + deal risk. كله Arabic، deterministic، approval-required. +تحويل **نص** (transcript أو ملاحظات) إلى ملخص عربي، استخراج اعتراضات، ومتابعة بعد الاجتماع — **بدون** إدراج أحداث تقويم حي من هذه المسارات. -## 1. الوحدات +## كود -| الوحدة | الدور | -|--------|------| -| `transcript_parser` | يقبل Google Meet entries أو نصاً عادياً، يحوّل إلى `speaker_turns`. | -| `meeting_brief` | يبني pre-meeting brief بـ6 أقسام عربية: هدف، أسئلة، اعتراضات محتملة، عرض، خطوة تالية. | -| `objection_extractor` | يستخرج 8 فئات اعتراضات (السعر، التوقيت، صانع القرار، الأمان، التكامل، البديل، إثبات النتائج، التعقيد). | -| `followup_builder` | يبني drafts للـemail + WhatsApp بدون إرسال حي. | -| `deal_risk` | يحسب risk_score (0..100) بناءً على الاعتراضات وغياب صاحب القرار وعدم تحديد خطوة تالية. | +| وحدة | ملاحظة | +|------|--------| +| `transcript_parser.py` | تلخيص نص طويل | +| `objection_extractor.py` | اعتراضات من النص | +| `followup_builder.py` | مسودة متابعة | +| `meeting_brief.py` | brief قبل الاجتماع من هياكل JSON | +| `deal_risk.py` | إشارات مخاطرة صفقة (تجريبي) | -## 2. Pre-Meeting Brief +## API -`build_pre_meeting_brief(company, contact, opportunity, sector)` يعطي: -- objective_ar -- 5 questions_ar -- 5 likely_objections_ar -- offer_skeleton_ar (Pilot 7 أيام، 499 ريال) -- next_step_ar +- `POST /api/v1/meeting-intelligence/transcript/summarize` — `{ "text": "..." }` +- `POST /api/v1/meeting-intelligence/followup/draft` — `{ "summary_ar": "...", "next_steps": [] }` +- `POST /api/v1/meeting-intelligence/brief/pre-meeting` — `{ "company": {}, "contact": {}, "opportunity": {} }` -## 3. Transcript Summarizer +## خصوصية -`parse_transcript_entries` يدعم: -- list of `{participantId, text}` (Google Meet shape) -- plain text "Speaker: line" - -`summarize_meeting(parsed)` يعطي ملخصاً عربياً + أسئلة مرشحة + `approval_required=True`. - -Google Meet API يدعم قراءة transcripts عبر `conferenceRecords.transcripts.entries.list` — لكن يلزم موافقة كل المشاركين (PDPL). - -## 4. Objection Extractor - -8 فئات regex + Arabic gloss: -``` -price → "غالي|مرتفع|الميزانية|expensive|cost" -timing → "ليس\s+الآن|بعد\s+شهر|next\s+quarter" -authority → "المدير|صاحب\s+القرار|need\s+approval" -trust → "بيانات|خصوصية|أمان|PDPL|security|privacy" -integration → "CRM|نظامنا|الربط|migration" -competitor → "نستخدم|بديل|أداة\s+ثانية|alternative" -results → "نتائج|مضمون|guarantee|ROI|دليل" -complexity → "معقد|صعب|تدريب|onboarding" -``` - -النتيجة: قائمة `{category, label_ar, snippet}` مع snippet ±40 حرف. - -## 5. Follow-up Builder - -`build_post_meeting_followup(summary, next_steps, contact_name, company_name, objections)`: -- Email draft (subject_ar + body_ar) -- WhatsApp draft (body_ar) -- كلا الـdraftsْ: `live_send_allowed=False, approval_required=True` - -عندما تكون فيه objections، يضيف فقرة "رجعت بعد الاجتماع وفكرت في النقاط التي ذكرتها: ..." - -## 6. Deal Risk - -``` -+20 price objection -+15 timing objection -+25 authority not present -+20 trust/security objection -+10 integration concern -+15 competitor in play -+25 next step NOT set -+10 decision maker absent -+10 days_since_last_touch > 14 -``` - -تيرز: ≥70 high, ≥40 medium, <40 low. - -`recommended_action_ar`: -- high: اجتماع ثانٍ مع صاحب القرار خلال 5 أيام + مادة إثبات قيمة قصيرة. -- medium: متابعة خلال 3 أيام مع خطوة تالية محددة. -- low: نفّذ الخطوة التالية المتفق عليها. - -## 7. Endpoints - -``` -POST /api/v1/meeting-intelligence/brief -GET /api/v1/meeting-intelligence/brief/demo -POST /api/v1/meeting-intelligence/transcript/summarize -POST /api/v1/meeting-intelligence/followup/draft -POST /api/v1/meeting-intelligence/deal-risk -``` - -## 8. حدود - -- لا realtime listening (مرحلة لاحقة). -- لا يرسل follow-up — drafts فقط. -- لا يقرأ transcript بدون موافقة جميع الأطراف. +لا تُرسل تسجيلات حساسة إلى نماذج خارجية من مسارات الـ API الحالية دون موافقة DPA؛ المعالجة هنا deterministic على النص المُدخل. diff --git a/dealix/docs/MODEL_PROVIDER_ROUTER.md b/dealix/docs/MODEL_PROVIDER_ROUTER.md index 20bc451b..5eec6a40 100644 --- a/dealix/docs/MODEL_PROVIDER_ROUTER.md +++ b/dealix/docs/MODEL_PROVIDER_ROUTER.md @@ -1,57 +1,22 @@ -# Model Provider Router — موجّه النماذج +# Model Provider Router -> 7 providers، 10 task types. كل مهمة تذهب لمزود مناسب مع fallback chain. لا تعتمد على مزود واحد. +طبقة **تكوين وتلميحات** لربط نوع المهمة بمزود/نموذج مقترح. التنفيذ الفعلي لاستدعاء LLM يبقى في `core/llm/` وإعدادات البيئة. -## 1. Task Types +## كود -``` -strategic_reasoning, arabic_copywriting, classification, -compliance_guardrail, meeting_analysis, vision_analysis, -extraction, summarization, coding_project_understanding, -low_cost_bulk -``` +- `auto_client_acquisition/model_router/task_router.py` — `route_task`, `list_tasks` +- `auto_client_acquisition/model_router/provider_registry.py` — `list_providers` -## 2. Providers +## API -| key | family | cost | latency | privacy | الاستخدام | -|-----|--------|------|---------|---------|----------| -| claude_sonnet | anthropic | mid | balanced | vendor | استراتيجية + كتابة عربية + امتثال | -| claude_haiku | anthropic | low | fast | vendor | تصنيف + استخراج كثيف | -| gpt_4_class | openai | high | balanced | vendor | استراتيجية + رؤية | -| gpt_4o_mini | openai | low | fast | vendor | تصنيف رخيص | -| gemini_pro | google | mid | balanced | vendor | اجتماعات + رؤية | -| azure_oai_ksa | azure | mid | balanced | **ksa_region** | الحالات الحساسة (PDPL) | -| local_qwen_ar | local | low | balanced | **self_hosted** | حالات شديدة الحساسية | +- `GET /api/v1/model-router/tasks` +- `POST /api/v1/model-router/route` — `{ "task_type": "..." }` +- `GET /api/v1/model-router/providers` -## 3. Cost Policy +## استخدام -``` -bulk=True → low -output_tokens > 1500 → high -input_tokens > 8000 → high -strategic/vision/compl. → mid -arabic_copywriting → mid -default → low -``` +استخدم الاستجابة لعرض «لماذا هذا المزود» في لوحة المشرف أو لتمرير metadata إلى `core/llm/router.py` عند توحيد السلوك لاحقاً. -## 4. Fallback Strategy +## اختبارات -- لو `sensitivity="high"`: الترتيب حسب `privacy_tier` أولاً (self_hosted > ksa_region > vendor). -- وإلا: الترتيب حسب `cost_class` (low > mid > high). -- لو `primary_provider` محدد ويدعم المهمة → يُرفع لرأس السلسلة. - -## 5. Endpoints - -``` -GET /api/v1/model-router/providers -GET /api/v1/model-router/tasks -POST /api/v1/model-router/route -POST /api/v1/model-router/cost-class -GET /api/v1/model-router/usage/demo -``` - -## 6. حدود - -- Router نفسه لا يستدعي LLM. يصدر قراراً فقط. -- التنفيذ الفعلي يبقى مسؤولية adapter منفصل. -- لا lock-in — جميع المزودين قابلون للاستبدال بدون تغيير API. +`tests/test_growth_tower_stack.py` — `test_model_router_compliance_guardrail`. diff --git a/dealix/docs/ONBOARDING_RUNBOOK.md b/dealix/docs/ONBOARDING_RUNBOOK.md index 5fc52732..449c6bdf 100644 --- a/dealix/docs/ONBOARDING_RUNBOOK.md +++ b/dealix/docs/ONBOARDING_RUNBOOK.md @@ -1,120 +1,10 @@ -# Dealix Onboarding Runbook +# تشغيل Onboarding — Pilot -> **الهدف:** نقل عميل جديد من "وافق على الـ Pilot" إلى "أول Proof Pack" خلال 5 أيام عمل، بدون خطأ تشغيلي. +1. **قبل الجلسة:** تأكيد الهدف، القطاع، المدينة، العرض، رابط الموقع، ووجود قائمة أو لا. +2. **الجلسة (٣٠ دقيقة):** شرح سياسة الموافقات وعدم الإرسال الحي الافتراضي؛ جمع القنوات المتاحة (إيميل، واتساب opt-in، CRM، نماذج). +3. **بعد الجلسة:** تشغيل أول مهمة (تشخيص / ١٠ فرص / List Intelligence) حسب الاتفاق؛ جدولة أول Proof Pack. +4. **مخرجات أول ٢٤–٤٨ ساعة:** حسب نافذة الـ Pilot — لا تعد نتائج مضمونة؛ وثّق المسودات والموافقات. ---- +**مرجع API:** `GET /api/v1/customer-ops/onboarding/checklist` -## 8 خطوات الـ onboarding (محسوبة) - -| # | الخطوة | المدة | الاعتماد | -|---|--------|------|---------| -| 1 | اختيار الهدف | 2د | لا | -| 2 | اختيار الباقة | 3د | نعم | -| 3 | بيانات الشركة | 5د | لا | -| 4 | ربط القنوات (drafts فقط) | 8د | نعم | -| 5 | رفع قائمة أو ربط مصدر leads | 5د | نعم | -| 6 | مراجعة المخاطر (PDPL + سمعة) | 4د | نعم | -| 7 | تشغيل أول خدمة | async | نعم | -| 8 | استلام أول Proof Pack | async | لا | - -**Endpoints:** -- `POST /api/v1/customer-ops/onboarding/checklist` -- `POST /api/v1/customer-ops/onboarding/update-step` -- `GET /api/v1/customer-ops/onboarding/checklist/demo` - ---- - -## Day-by-day - -### Day 1 — Kick-off (60 دقيقة) -- مكالمة 30 دقيقة مع المؤسس / Growth Manager. -- ملء الـ intake (الخطوات 1-3). -- توقيع Pilot Agreement draft + DPA draft (يحتاج محامي للحالة الإنتاجية). -- إنشاء session في `operator_memory` + customer_id. - -### Day 2 — Connectors -- ربط Gmail (drafts فقط) — `connector_setup_status` يتعقّب التقدم. -- ربط Google Calendar (drafts فقط). -- ربط Google Sheets للـ exports. -- WhatsApp Cloud (إذا لازم) — opt-in audit أولاً. - -### Day 3 — List + Risk Review -- رفع CSV / ربط CRM. -- تشغيل `targeting_os.analyze_uploaded_list_preview`. -- مراجعة الـ contactability (safe / needs_review / blocked). -- اعتماد القنوات الآمنة فقط. - -### Day 4 — أول خدمة -- تشغيل First 10 Opportunities Sprint أو List Intelligence. -- توليد 10 opportunity cards + رسائل عربية. -- إرسال Approval Pack للعميل (≤3 أزرار لكل بطاقة). - -### Day 5 — Proof Pack v1 -- استلام Proof Pack مختصر (PDF + JSON + WhatsApp summary). -- جلسة مراجعة 30 دقيقة. -- تفعيل Customer Success Cadence (weekly check-ins). - ---- - -## Connector Setup Status - -11 connectors معرّفة في `customer_ops.connector_setup_status`: - -| Connector | Default Mode | Phase | Blocking | -|-----------|--------------|------:|----------| -| gmail | draft_only | 1 | لا | -| google_calendar | draft_only | 1 | لا | -| google_sheets | approved_execute | 1 | لا | -| moyasar | manual | 1 | لا | -| whatsapp_cloud | draft_only | 1 | **نعم** | -| website_forms | approved_execute | 1 | لا | -| linkedin_lead_forms | ingest_only | 2 | لا | -| google_business_profile | draft_only | 2 | لا | -| crm_generic | draft_only | 2 | لا | -| google_meet | ingest_only | 2 | لا | -| instagram_graph | ingest_only | 3 | لا | - -`ready_for_first_service` = `True` فقط عندما لا يوجد blocking connector مفقود + ≥1 connector connected. - ---- - -## Connector States - -``` -not_started → configuring → connected_draft_only - → connected_approved_execute -configuring → failed (يحتاج إعادة محاولة) -configuring → skipped (إذا قرر العميل عدم الربط) -``` - ---- - -## ما لا يحدث بدون اعتماد - -- ربط Gmail لا يفعّل send. -- ربط Calendar لا يفعّل insert. -- ربط Moyasar لا يفعّل charge. -- ربط WhatsApp لا يفعّل cold send. - -كل live action يحتاج env flag صريح + اعتماد بشري. - ---- - -## Onboarding Failure Recovery - -| فشل | الإجراء | -|-----|--------| -| OAuth Gmail فشل | recheck scopes, retry, fallback to draft-only | -| Moyasar invoice غير موصول | استخدم dashboard manual | -| العميل لم يرفع قائمة | اعرض Free Diagnostic + recommend_accounts | -| Risk review كشف مشكلة | توقّف، أرسل تقرير للمؤسس | - ---- - -## Acceptance Criteria - -العميل onboarded إذا: -1. كل الـ 8 خطوات `completed=True` (إلا الـ async منها). -2. `ready_for_first_service=True`. -3. Proof Pack v1 تم تسليمه + اعتماده. -4. Customer Success cadence مفعّل. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/PAID_BETA_OPERATING_PLAYBOOK.md b/dealix/docs/PAID_BETA_OPERATING_PLAYBOOK.md index 0a29088b..64945f93 100644 --- a/dealix/docs/PAID_BETA_OPERATING_PLAYBOOK.md +++ b/dealix/docs/PAID_BETA_OPERATING_PLAYBOOK.md @@ -1,206 +1,100 @@ -# Paid Beta Operating Playbook +# Paid Beta — دليل التشغيل التجاري (Dealix) -> **القاعدة:** الجاهزية التقنية لا تعني دخل. هذا الـ playbook يحوّل GO_PRIVATE_BETA إلى أول 499 ريال خلال 7 أيام. +**الغرض:** تحويل **GO_PRIVATE_BETA** محلياً إلى **PAID_BETA_READY** على staging ثم إلى أول إيراد وتسليم Proof Pack — بدون توسيع تقني كبير وبدون وعود خطيرة. + +**مرجع:** [`APPROVED_MARKET_MESSAGING.md`](APPROVED_MARKET_MESSAGING.md)، [`PROHIBITED_CLAIMS.md`](PROHIBITED_CLAIMS.md)، [`POSITIONING_LOCK.md`](POSITIONING_LOCK.md)، [`STAGING_DEPLOYMENT.md`](STAGING_DEPLOYMENT.md)، [`FIRST_PILOT_DELIVERY_WORKFLOW.md`](FIRST_PILOT_DELIVERY_WORKFLOW.md)، [`PRIVATE_BETA_OPERATING_BOARD.md`](PRIVATE_BETA_OPERATING_BOARD.md). --- -## 1. الحالة الحالية +## 1. تعريف الحالات -``` -✅ Tests: 949 passed, 2 skipped -✅ CI green -✅ Service Tower + Service Excellence + Targeting OS + Customer Ops جاهزة -✅ Positioning Lock مفعّل -✅ Landing pages متوافقة مع POSITIONING_LOCK -🟡 Staging: ينتظر النشر الفعلي -🟡 First payment: ينتظر أول عميل -``` - -**الحالة:** `GO_PRIVATE_BETA` محلياً. الانتقال لـ `PAID_BETA_READY` يحتاج Staging شغّال + أول Pilot مدفوع. +| الحالة | المعنى | +|--------|--------| +| **Private Beta (محلي)** | `launch_readiness_check.py` بدون `--base-url` → `GO_PRIVATE_BETA`؛ CI أخضر؛ لا بيع مدفوع قبل staging إن كنت تعتمد على النشر. | +| **Paid Beta** | `STAGING_BASE_URL` + `python scripts/launch_readiness_check.py --base-url …` → **`PAID_BETA_READY`**؛ تحصيل يدوي (فاتورة / رابط / تحويل)؛ أول Pilot موقّع. | +| **Public Launch** | ليس الآن — انظر شروط الخروج في أسفل هذا الملف وفي [`PUBLIC_LAUNCH_GO_NO_GO.md`](PUBLIC_LAUNCH_GO_NO_GO.md). | --- -## 2. الانتقال من Private Beta إلى Paid Beta +## 2. Staging (خلال أيام قليلة) -### Gate الانتقال (لا تتجاوزه) - -```text -✅ Staging /health = 200 -✅ Service catalog يعرض 4+ خدمات -✅ landing/private-beta.html فيه 499 SAR + CTA -✅ no_secrets scan نظيف -✅ live_sends_disabled = true -✅ Moyasar invoice/payment-link manual flow جاهز -✅ أول 20 prospect معرّفون في Operating Board -``` - -### Smoke Commands - -```bash -export STAGING_BASE_URL="https://YOUR-STAGING-URL" -python scripts/smoke_staging.py --base-url "$STAGING_BASE_URL" -python scripts/launch_readiness_check.py --staging-url "$STAGING_BASE_URL" -python scripts/paid_beta_daily_scorecard.py --as-of today -``` - -المطلوب: `PAID_BETA_READY`. لو NO-GO → أصلح السبب قبل أي بيع. +1. انشر API على **Railway** أو **Render** (انظر [`ops/RAILWAY_AI_COMPANY_BIND.md`](ops/RAILWAY_AI_COMPANY_BIND.md)). +2. Start: `uvicorn api.main:app --host 0.0.0.0 --port $PORT` — الاستماع على `PORT` الذي يحقنه المزود. +3. Health: `GET /health` → 200. +4. تحقق: + ```bash + export STAGING_BASE_URL="https://YOUR-STAGING-URL" + python scripts/smoke_staging.py --base-url "$STAGING_BASE_URL" + python scripts/launch_readiness_check.py --base-url "$STAGING_BASE_URL" + ``` +5. **لا تبدأ عرضاً مدفوعاً عاماً** إذا كانت النتيجة `NO_GO` — عالج السبب أولاً. --- -## 3. خطة 7 أيام للوصول للدخل الأول +## 3. التحصيل اليدوي (بدون live billing من API) -### يوم 1 — Staging + Outreach -- نشر staging على Railway. -- تشغيل smoke + readiness checks. -- إرسال 10 رسائل (5 وكالات + 5 شركات). -- 1 منشور LinkedIn (founder voice). +- **Moyasar:** فاتورة يدوية أو رابط دفع (sandbox أو إنتاج حسب اتفاقك القانوني). +- **أو** تحويل بنكي مع مرجع واضح في العقد/الإيميل. +- رسالة اقتراح للعميل بعد الموافقة على النطاق (انظر [`FIRST_PILOT_DELIVERY_WORKFLOW.md`](FIRST_PILOT_DELIVERY_WORKFLOW.md) لنص Pilot 499). -**الهدف:** 2 ردود + 1 ديمو محجوز. - -### يوم 2 — Demos -- إرسال 10 رسائل أخرى. -- إجراء أول 1-2 ديمو. -- بدء أول Free Diagnostic لأي عميل اهتم. - -**الهدف:** 1 Free Diagnostic موعود. - -### يوم 3 — Diagnostic Delivery -- تسليم أول Free Diagnostic خلال 24 ساعة. -- 5 follow-ups. -- إرسال 5 رسائل جديدة. - -**الهدف:** 1 Pilot Offer. - -### يوم 4 — First Pilot Sale -- محادثة Pilot 499 مع المهتم. -- إنشاء Moyasar invoice manual. -- إرسال payment-link-message. - -**الهدف:** 1 invoice paid أو commitment مكتوب. - -### يوم 5 — Pilot Delivery Day 1 -- استلام intake من العميل. -- تشغيل First 10 Opportunities Sprint workflow. -- 10 opportunities + 10 رسائل عربية. - -**الهدف:** Approval Pack مرسل للعميل. - -### يوم 6 — Pilot Delivery Day 2 -- متابعة الموافقات. -- تشغيل follow-up sequence. -- أول 1-2 رد إيجابي. - -**الهدف:** اعتماد ≥3 رسائل + Proof Pack v1. - -### يوم 7 — Proof + Upsell -- تسليم Proof Pack. -- جلسة مراجعة 30 دقيقة. -- اقتراح ترقية لـ Growth OS Pilot. - -**الهدف:** Case study أو Pilot ثانٍ. +**ممنوع في التواصل:** ضمان مبيعات، واتساب بارد جماعي، «AI يبيع بدالك 100٪»، scraping أو أتمتة رسائل LinkedIn — انظر [`PROHIBITED_CLAIMS.md`](PROHIBITED_CLAIMS.md). --- -## 4. أهداف الأسبوع +## 4. حملة تواصل يدوية (7 أيام — أهداف مرجعية) -| Metric | Target | -|--------|-------:| -| Messages sent | 50–70 | -| Positive replies | 5–10 | -| Demos booked | 3–5 | -| Pilots offered | 2–3 | -| Payments requested | 1–2 | -| Payments received | 1+ | -| Proof packs delivered | 1+ | +| الموجه | هدف مرجعي | +|--------|------------| +| تواصل مباشر (وكالات / مسوقين / B2B) | 50–70 رسالة على مدار الأسبوع (مو شرط يوم واحد) | +| ديمو | 5–7 محجوزة | +| Pilot | 3 تشغيل؛ إغلاق **1–2 مدفوعين** | +| Proof Pack | أول تسليم موثّق لكل عميل مدفوع | + +**قنوات آمنة:** واتساب دافئ فقط مع سياق؛ إيميل مستهدف؛ LinkedIn يدوي للمؤسس — بدون أدوات سحب أو DMs آلية. --- -## 5. القواعد التشغيلية اليومية (لا تتنازل عنها) +## 5. قياس الـ funnel -1. **لا live WhatsApp send** بدون env flag + اعتماد بشري. -2. **لا live Gmail send** بدون env flag + اعتماد بشري. -3. **لا Calendar insert** بدون اعتماد. -4. **لا Moyasar charge** من API — invoice/payment-link manual فقط. -5. **لا scraping LinkedIn** ولا auto-DM — Lead Gen Forms + manual فقط. -6. **لا cold WhatsApp** بدون opt-in — PDPL hard-block. -7. **كل رسالة** تمر `safety_eval` + `saudi_tone_eval` قبل الإرسال. -8. **كل فعل** يُسجّل في Action Ledger. +استخدم لوحة [`PRIVATE_BETA_OPERATING_BOARD.md`](PRIVATE_BETA_OPERATING_BOARD.md) (Sheet أو نسخة Markdown). + +يومياً: سجّل `messages_sent`، الردود الإيجابية، الديمو، عروض الـ Pilot، طلبات الدفع، المستلم، Proof Packs. + +سكربت تذكيري: `python scripts/paid_beta_daily_scorecard.py` (مع وسائط أو ملف JSON — انظر تعليمات السكربت). --- -## 6. Daily Cadence +## 6. شروط الانتقال إلى Paid Beta (تشغيل) -### الصباح (60 دقيقة) -- شغّل `paid_beta_daily_scorecard.py`. -- راجع الـ Operating Board. -- اعتمد drafts اليوم (10–15 دقيقة). -- 5 follow-ups. +قبل أن تسمّي المرحلة «Paid Beta» تشغيلياً: -### الظهر (90 دقيقة) -- 1–2 ديمو. -- 10 رسائل جديدة (segments متنوعة). - -### العصر (60 دقيقة) -- تسليم deliverable لعميل واحد. -- إجابة support tickets (إن وجد). - -### آخر اليوم (30 دقيقة) -- تحديث Operating Board. -- تشغيل scorecard مرة أخرى. -- خطة الغد. +- [ ] **`PAID_BETA_READY`** على staging (`launch_readiness_check.py --base-url`). +- [ ] **3 ديمو** على الأقل محجوزة أو منجزة (حسب تعريفك). +- [ ] **1 Pilot مدفوع** أو التزام مكتوب واضح بالدفع. +- [ ] **أول Proof Pack** جاهز للتسليم (قالب + أرقام من المنتج/العملية). +- [ ] **لا إجراءات غير آمنة** (لا live send بدون موافقة، لا تجاوز لسياسة القنوات). +- [ ] **لا تسرّب أسرار** في الريبو أو في traces (انظر [`POST_MERGE_VERIFICATION.md`](POST_MERGE_VERIFICATION.md)). --- -## 7. ما لا تضيفه هذا الأسبوع +## 7. شروط ما بعد 3 pilots (تحليل لا بناء) -- لا ميزات تقنية جديدة. -- لا layers معمارية. -- لا modules جديدة. -- لا بريق landing. +بعد أول **3 pilots**، لا تضف ميزات كبيرة. راجع: -**التركيز كله:** عميل واحد يدفع 499 ريال. +- أي خدمة بيعت أسرع؟ +- أي صفحة أو رسالة جلبت ردّاً؟ +- مدة الـ onboarding؟ +- هل Proof Pack أقنع؟ +- هل السعر مناسب؟ هل الـ support مثقل؟ + +ثم **ضاعف** على: Growth Starter، أو مسار الوكالة، أو List Intelligence — حسب البيانات لا حسب الحدس. --- -## 8. شروط الانتقال إلى Public Launch +## 8. CI كبوابة دمج -لا انتقال قبل: -``` -5–10 pilots -2+ paid customers -0 unsafe sends -weekly proof packs delivered -support flow يعمل -funnel واضح من lead → demo → pilot → paid -14 يوم staging stable -billing live (Moyasar API webhook) -terms + privacy + DPA -``` +فعّل **required status checks** على الفرع المحمي في GitHub حتى لا يُدمج كود فاشل. قائمة الـ jobs المطلوبة وأسماء الـ checks: [`BRANCH_PROTECTION_AND_CI.md`](BRANCH_PROTECTION_AND_CI.md). مرجع خارجي: وثائق GitHub عن status checks. --- -## 9. Endpoints المهمة في Paid Beta - -``` -GET /api/v1/launch/private-beta/offer -POST /api/v1/launch/go-no-go -GET /api/v1/launch/scorecard/demo -GET /api/v1/operator/bundles -POST /api/v1/operator/chat/message -POST /api/v1/customer-ops/onboarding/checklist -POST /api/v1/customer-ops/connectors/summary -POST /api/v1/revenue-launch/payment/invoice-instructions -POST /api/v1/revenue-launch/proof-pack/template -GET /api/v1/service-excellence/review/all -``` - ---- - -## 10. القرار النهائي - -``` -لا تنتظر "كمال المنتج". المنتج كامل تقنياً. -أنت تنتظر "أول إيراد". -الإيراد يأتي من 50 رسالة يدوية + 5 ديمو + 1 invoice. -ابدأ. -``` +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/PLATFORM_SERVICES_STRATEGY.md b/dealix/docs/PLATFORM_SERVICES_STRATEGY.md index bad2410a..b1474902 100644 --- a/dealix/docs/PLATFORM_SERVICES_STRATEGY.md +++ b/dealix/docs/PLATFORM_SERVICES_STRATEGY.md @@ -1,196 +1,54 @@ -# Platform Services Strategy — برج التحكم بالنمو -## (Dealix Growth Control Tower) +# استراتيجية Platform Services — Growth Control Tower -> **الهدف:** تحويل Dealix من "WhatsApp Growth Operator" إلى **منصة نمو متعددة القنوات** تشتغل تحت سقف واحد، بسياسات أمان موحدة، ومسار اعتماد واحد، وبروتوكول أحداث موحد. +## الهدف ---- +طبقة موحّدة تحت `/api/v1/platform/*` تجمع: **أحداث موحّدة**، **سياسة قنوات**، **بوابة أدوات بدون إرسال حي**، **Inbox موحّد** (بطاقات عربية)، **سجل قرارات**، و**تلخيص Proof** متوافق مع `auto_client_acquisition/innovation/proof_ledger.py` — دون تكرار «مصدر الحقيقة» للأحداث الدائمة في DB. -## 1. لماذا Platform Services؟ +## مكوّنات الكود -كل قناة (WhatsApp, Gmail, Calendar, LinkedIn, X, Instagram, GBP, Sheets, CRM, Moyasar, Website Forms) تحتاج: -- تطبيع الإشارات (signal normalization). -- سياسة قبول/رفض موحدة (PDPL-aware). -- حل هوية متقاطع (cross-channel identity). -- مدخل تنفيذي موحد (single tool gateway) لمنع الإرسال البارد، تسريب الأسرار، أو الدفع بدون تأكيد. -- صندوق بريد موحد (unified inbox) ببطاقات قابلة للاعتماد. -- سجل أفعال (action ledger) للمراجعة (SDAIA / PDPL). -- سجل أثر (proof ledger) لتسويق "كم وفّرنا، كم سحبنا، كم منعنا من مخاطر". +| وحدة | مسؤولية | +|------|----------| +| `event_bus` | أنواع أحداث + تحقق من الحقول الإلزامية (يشمل أنماطاً موسّعة مثل `email.received`, `payment.paid`, `review.created`) | +| `channel_registry` | قدرات القناة، `beta_status`, `allowed_actions`, `blocked_actions`, `risk_level` | +| `action_policy` | قواعد deterministic: إرسال خارجي → موافقة؛ واتساب بارد → محظور؛ دفع → تأكيد؛ مصدر غير معروف → مراجعة | +| `tool_gateway` | لا شبكة ولا live — `draft_created` / `blocked` / `approval_required` / `unsupported` | +| `unified_inbox` | تحويل حدث → بطاقة (`title_ar`, `summary_ar`, أزرار ≤ 3)؛ يمكن دمج لمسات من `build_demo_command_feed` كمرجع عرض | +| `action_ledger` | سجل قرارات in-memory في MVP (قابل للاستبدال بـ DB) | +| `proof_summary` | تلخيص يستند إلى `build_demo_proof_ledger()` | +| `service_catalog` | خدمات قابلة للبيع كبيانات ثابتة + metadata | -بدون هذه الطبقة، كل ميزة جديدة تحتاج تكامل مخصص → فوضى أمنية + أمنية + قانونية. +## ما يُنفَّذ الآن مقابل مؤجل ---- +**الآن (MVP):** مسارات read-only / مسودات؛ `WHATSAPP_ALLOW_LIVE_SEND` يبقى افتراضياً `false` في [`core/config/settings.py`](../core/config/settings.py). -## 2. الوحدات (10 modules) +**مؤجل:** OAuth، توقيع webhook إنتاجي كامل، قاعدة أحداث دائمة لـ platform خارج نموذج الابتكار، تنفيذ فعلي لـ Gmail/Calendar/Moyasar. -| # | الوحدة | الدور | -|---|--------|------| -| 1 | `event_bus` | تصنيف موحد لـ27 نوع حدث (whatsapp/email/calendar/lead/payment/review/social/partner/sheet/crm/action). | -| 2 | `identity_resolution` | دمج phone + email + CRM ID + social handles → هوية موحدة. | -| 3 | `channel_registry` | 11 قناة، لكل واحدة capabilities + allowed/blocked actions + PDPL notes. | -| 4 | `action_policy` | محرك قواعد (block_cold_whatsapp, block_payment_no_confirm, block_secrets, external_send_needs_approval...). | -| 5 | `tool_gateway` | المخرج التنفيذي الوحيد. كل أداة تمر من هنا → سياسة → draft / approval_required / blocked / ready. | -| 6 | `unified_inbox` | بطاقات قرار (≤3 أزرار، عربية، type+risk+recommended_action). | -| 7 | `action_ledger` | سجل كل فعل بمراحله (requested → approved → executed). | -| 8 | `proof_ledger` | عدّاد أثر (leads, meetings, drafts, sends, payments, revenue, risks_blocked, time_saved). | -| 9 | `service_catalog` | 12 خدمة قابلة للبيع تحت Dealix Operator OS. | -| 10 | (router + tests) | `api/routers/platform_services.py` + اختبارات شاملة. | +## Inbox و Command Cards ---- +- كل بطاقة: عنوان عربي، ملخص، **ثلاثة أزرار كحد أقصى** (`label_ar` + `action_id`). +- مصادر الحدث: نماذج leads موثّقة، أحداث داخلية، لاحقاً قنوات مسجّلة فقط. -## 3. القنوات الـ11 +## امتثال PDPL / opt-in -``` -whatsapp, gmail, google_calendar, moyasar, linkedin_lead_forms, -x_api, instagram_graph, google_business_profile, google_sheets, -crm, website_forms -``` +- لا تخزين بيانات حساسة في سجل الـ MVP بدون سياسة موثّقة. +- أي إرسال جماعي أو بارد يمر عبر `action_policy` + موافقة بشرية؛ راجع [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md) و[`BETA_PRIVATE_GATES_CHECKLIST.md`](BETA_PRIVATE_GATES_CHECKLIST.md). -كل قناة لها: -- `capabilities` -- `beta_status` (`live` / `beta` / `coming_soon`) -- `allowed_actions` / `blocked_actions` -- `risk_level` -- `notes_ar` +## العلاقة بـ Innovation -مثال: WhatsApp **يحظر** `cold_send_without_consent`. Gmail يستخدم `gmail.compose` فقط (drafts). Calendar `live_inserted=False` حتى يربط OAuth. +`innovation` يبقى مسار العرض والـ Kill features (`ten-in-ten`, command feed demo/live). Platform **تلتف** على الدوال التجريبية للـ Proof حيث يلزم، ولا تعيد تعريف أحداث الـ ledger الدائمة. ---- +## طبقات «برج التحكم» الإضافية (كود حالي) -## 4. سياسة الأمان (Action Policy) +| طبقة | مجلد | مسارات API رئيسية | +|------|------|---------------------| +| Security curator | `auto_client_acquisition/security_curator/` | `/api/v1/security-curator/*` — redact، inspect-diff | +| Growth curator | `auto_client_acquisition/growth_curator/` | `/api/v1/growth-curator/*` — grade، تقرير أسبوعي demo | +| Meeting intelligence | `auto_client_acquisition/meeting_intelligence/` | `/api/v1/meeting-intelligence/*` — تلخيص نص، متابعة، brief | +| Model router | `auto_client_acquisition/model_router/` | `/api/v1/model-router/*` — tasks، route، providers | +| Connectors | `auto_client_acquisition/connectors/` | `GET /api/v1/connectors/catalog` | +| Agent observability | `auto_client_acquisition/agent_observability/` | `/api/v1/agent-observability/*` — evals، trace shape | +| Growth operator (aliases) | `api/routers/growth_operator.py` | `/api/v1/growth-operator/missions`، `.../proof-pack/demo` | -**قواعد block أساسية:** -1. WhatsApp بارد بدون consent → **blocked** (PDPL). -2. أي charge/refund بدون `user_confirmed=true` → **blocked**. -3. أي payload يحوي `api_key/secret/token/...` → **blocked**. +**Growth operator** لا يضاعف المنطق: يضيف `canonical_route` للإشارة إلى مصدر الحقيقة في `innovation` و`business`. -**قواعد approval_required:** -- أي إرسال خارجي (`send_*`) → اعتماد إنساني. -- إدراج موعد في تقويم → اعتماد. -- DM على سوشل → اعتماد + opt-in. -- صفقة قيمتها ≥ 200,000 ريال → اعتماد. - -**default:** allow (للـ read-only data ops). - ---- - -## 5. Tool Gateway - -كل أداة (`whatsapp.send_message`, `gmail.compose`, `calendar.insert_event`, `moyasar.refund`, `gbp.reply_review`, ...) **يجب** تمر من `invoke_tool()`. - -النتائج المحتملة: -- `unsupported` — أداة غير مسجلة. -- `blocked` — السياسة منعت. -- `approval_required` — تحتاج قبول إنساني. -- `draft_created` — افتراضياً (live env flag = OFF). -- `ready_for_adapter` — جاهز للتنفيذ الحقيقي إذا اشتغل live env flag. - -**Live env flags** (افتراضياً كلها OFF): -``` -WHATSAPP_ALLOW_LIVE_SEND -GMAIL_ALLOW_LIVE_SEND -CALENDAR_ALLOW_LIVE_INSERT -MOYASAR_ALLOW_LIVE_CHARGE -GBP_ALLOW_LIVE_REPLY -``` - ---- - -## 6. صندوق البريد الموحد (Unified Inbox) - -8 أنواع بطاقات: -``` -opportunity, email_lead, whatsapp_reply, payment, -meeting_prep, review_response, partner_suggestion, action_required -``` - -كل بطاقة: -- ≤3 أزرار (تطبيق قيد WhatsApp Reply Buttons). -- عربية (title_ar, summary_ar, why_it_matters_ar, recommended_action_ar). -- `risk_level` (low/medium/high). - -البطاقات تُبنى تلقائياً من `PlatformEvent` عبر `build_card_from_event()`. - ---- - -## 7. Proof Ledger - -عدّاد يقيس الأثر العملي للمنصة: -``` -leads_created, meetings_booked, drafts_approved, -messages_sent, payments_initiated, payments_paid, -revenue_influenced_sar, risks_blocked, time_saved_hours, -partner_opportunities, by_channel -``` - -هذا هو **Marketing Asset** — لتُري العميل: "في 30 يوم، نحن ساعدناك تعمل X، منعنا Y مخاطر، وفرنا Z ساعة". - ---- - -## 8. خدمات قابلة للبيع (Service Catalog) - -12 خدمة تجارية: -1. `growth_operator_subscription` — اشتراك شهري للمنصة. -2. `channel_setup_service` — ربط القنوات (one-time). -3. `lead_intelligence_service` — إثراء + تأهيل لقاءات. -4. `outreach_approval_service` — drafts + approval workflow. -5. `partnership_sprint` — فرص تعاون عبر Partner Graph. -6. `email_revenue_rescue` — استعادة عملاء إيميل. -7. `social_growth_os` — تنبيهات + drafts + جدولة. -8. `local_business_growth` — GBP + reviews + visibility. -9. `ai_visibility_aeo_sprint` — Answer Engine Optimization. -10. `revenue_proof_pack_service` — تقرير أثر لمستثمرين / عملاء. -11. `customer_success_operator` — خفض churn + توسيع. -12. `payments_collections_operator` — تذكير + تحصيل (Moyasar). - ---- - -## 9. Endpoints (`/api/v1/platform/...`) - -``` -GET /services/catalog -GET /channels -GET /channels/{channel_key} -GET /policy/rules -POST /actions/evaluate -POST /actions/approve -GET /ledger/summary -POST /events/ingest -GET /inbox/feed -POST /identity/resolve -GET /identity/resolve-demo -POST /tools/invoke -GET /proof-ledger/demo -``` - ---- - -## 10. اختبارات - -`tests/unit/test_platform_services.py` — تغطية لكل الوحدات الـ10: -- catalog completeness -- channel coverage + cold-send blocked -- event validation -- policy (cold WA blocked, secrets blocked, payment confirmation, external send approval, high-value review) -- gateway (unsupported / blocked / draft default / live flag check) -- identity multi-signal merge -- inbox card validation (≤3 buttons + valid type) -- action ledger summary -- proof ledger structure - ---- - -## 11. ما لا تفعله هذه الطبقة - -- **لا** ترسل واتساب فعلياً (افتراضياً draft). -- **لا** ترسل Gmail فعلياً. -- **لا** تدرج موعد في Google Calendar. -- **لا** تأخذ أو تعيد دفعة بدون user_confirmed. -- **لا** تخزن مفاتيح API في payload. - ---- - -## 12. ما يلي - -- ربط Adapters حقيقية (WhatsApp Cloud, Gmail, Calendar) خلف الـenv flags. -- استبدال in-memory ledgers بـ Supabase. -- تشغيل `proof_ledger` على بيانات إنتاج مع تجربة عميل واحد. +لخطة تنفيذ بالعربية: [`EXECUTION_ROADMAP_AR.md`](EXECUTION_ROADMAP_AR.md). diff --git a/dealix/docs/POSITIONING_LOCK.md b/dealix/docs/POSITIONING_LOCK.md index 912af177..ad94c666 100644 --- a/dealix/docs/POSITIONING_LOCK.md +++ b/dealix/docs/POSITIONING_LOCK.md @@ -1,123 +1,35 @@ -# Dealix Positioning Lock +# Dealix — تثبيت الرسالة (Positioning Lock) -> **هذا الملف ثابت.** أي تغيير في الـ positioning يحتاج اعتماد المؤسس فقط. -> لا تكتب landing page ولا رسالة بيع ولا ديمو يخالف هذا الملف. +**الفئة:** نظام تشغيل نمو وإيرادات عربي للشركات السعودية والوكالات (Saudi Revenue Execution OS). ---- +**الجملة الأساسية:** Dealix يحوّل إشارات السوق وبيانات الشركة إلى فرص ورسائل واجتماعات مع موافقة بشرية وتقرير أثر (Proof Pack)، بدون إرسال عشوائي وبدون وعود مبالغ فيها. -## Category +## ليس Dealix -**Saudi Revenue Execution OS** +- ليس CRM عاماً يحتاج إدخال يدوي كاملاً للبيانات. +- ليس «بوت واتساب» للإرسال الجماعي. +- ليس أداة scraping أو جمع بيانات أشخاص من LinkedIn أو غيره بطرق مخالفة. +- ليس وكالة تقليدية فقط (المنصة تُكمّل الوكالة وتكرّر التشغيل). +- ليس مساعد ذكاء اصطناعي عاماً بلا سياسات وموافقات. -ليس CRM. ليس بوت واتساب. ليس lead scraper. ليس AI chatbot عام. ليس وكالة تقليدية. +## المشترون الأساسيون ---- +- شركات B2B سعودية وخليجية (خدمات، SaaS، تدريب، استشارات، تقنية). +- وكالات تسويق ومبيعات تريد تشغيل نمو لعملائها مع Proof Pack. -## One-liner (الجملة الرسمية الوحيدة) +## الـ Wedge (أول ما نبيعه) -> Dealix يشغّل النمو للشركات السعودية: يكتشف الفرص، يكتب الرسائل، يطلب الموافقات، ينسق الاجتماعات، ويثبت العائد. +- **First 10 Opportunities** + **Proof Pack** + **موافقات قبل أي تواصل حساس**. ---- +## المسارات التجارية -## Primary buyers (مساران فقط) +1. **Dealix للشركات:** فرص، رسائل عربية، تقييم قابلية التواصل، مسودات، موافقات، تقرير أثر. +2. **Dealix للوكالات:** تشخيص، تشغيل خدمة لعميل الوكالة، Proof Pack قابل للمشاركة مع العميل النهائي (حسب الاتفاق). -### 1. الشركات (B2B services / SaaS / training / clinics / real estate) -- مؤسس / CEO / Growth Manager. -- يحتاج فرص + رسائل + اجتماعات + Proof Pack. -- ميزانية 499–10,000 ريال شهرياً. +## قواعد لا تُخالَف في أي نسخة -### 2. الوكالات والمسوقين -- صاحب وكالة / Head of Sales في وكالة. -- يبحث عن Growth OS لعملائها. -- يبحث عن revenue share + co-branded Proof Pack. +- لا إرسال حي (واتساب/إيميل/تقويم/دفع) إلا بعد سياسة وموافقة صريحة حيث ينطبق. +- لا واتساب بارد افتراضياً؛ التواصل عبر واتساب يتطلب opt-in أو inbound أو علاقة واضحة. +- لا scraping لـ LinkedIn ولا أتمتة DM/Connect. ---- - -## Wedge (نقطة الدخول للسوق) - -**First 10 Opportunities + Proof Pack** — خدمة Sprint 7 أيام بـ 499 ريال. - -كل شيء آخر ينمو من هذه النقطة. - ---- - -## Approved Claims (5 — ما يُسمح بقوله) - -1. "نطلع لك 10 فرص B2B + رسائل عربية + خطة متابعة + Proof Pack خلال 7 أيام." -2. "Approval-first — لا نرسل أي شيء بدون موافقتك." -3. "PDPL-aware — لا cold WhatsApp بدون opt-in." -4. "Multi-channel orchestration: Email + WhatsApp + Calendar + Sheets + LinkedIn Lead Forms." -5. "Saudi Tone — رسائل عربية طبيعية، ليست ترجمة." - ---- - -## Prohibited Claims (ما لا يُقال أبداً) - -راجع [`PROHIBITED_CLAIMS.md`](PROHIBITED_CLAIMS.md). - ---- - -## Headlines - -### Homepage -> Dealix — Saudi Revenue Execution OS - -### Companies page -> حوّل بياناتك وقنواتك إلى فرص ورسائل واجتماعات وProof Pack. - -### Agencies / marketers page -> شغّل Dealix لعملائك كـ Agency Growth OS. - -### Private Beta page -> 10 فرص في 10 دقائق + متابعة + Proof Pack — وأنت توافق قبل أي تواصل. - ---- - -## Modes (الـ 5 أوضاع التشغيلية) - -1. **CEO Mode** — قرارات يومية، اعتمادات، Proof، مخاطر، 3 خطوات تالية. -2. **Growth Manager Mode** — حملات، أهداف، متابعات، اجتماعات، صحة القنوات. -3. **Agency Partner Mode** — إضافة عميل، diagnostic، Proof Pack مشترك العلامة، revenue share. -4. **Self-Growth Mode** — Dealix يستهدف عملاءه بنفس النظام. -5. **Service Delivery Mode** — تشغيل خدمات العميل، SLA، deliverables، proof. - ---- - -## Bundles (5 — ما يُعرض للعميل) - -1. Growth Starter — 499 ريال. -2. Data to Revenue — 1,500 ريال. -3. Executive Growth OS — 2,999 ريال شهرياً. -4. Partnership Growth — 3,000–7,500 ريال. -5. Full Growth Control Tower — Custom. - ---- - -## What Dealix is NOT (إنفاذ صارم) - -- **ليس CRM** — لا نخزّن جميع بيانات العملاء، لا نحاول استبدال HubSpot. -- **ليس bot** — لا نتظاهر بأن AI يفعل كل شيء؛ هناك human approval في كل خطوة. -- **ليس lead scraper** — مصادر مصرّح بها فقط. -- **ليس وكالة بشرية** — نظام قابل للتكرار بـ Proof Pack محسوب. -- **ليس AI agent عام** — لكل خدمة sandbox + policy + budget. -- **ليس Tier-1 enterprise platform** — SMB-first، Saudi-first. - ---- - -## Core Workflow (لا يتغير) - -``` -Signal → Context → Service Recommendation → Workflow → -Risk Check → Draft → Approval → Execution/Export → -Outcome → Proof → Learning → Upgrade -``` - -كل event داخل Dealix يمر بهذه السلسلة. - ---- - -## Why this lock matters - -السوق يدعم هذا الاتجاه (HubSpot Growth Context, Gong Revenue AI OS, Salesforce Agentic Work Units), لكن **Dealix لا يحاول أن يكون أكبر منهم**. يحاول أن يكون **أوضح، أسرع، أعمق محلياً، وأقرب للإيراد** للشركات السعودية والوكالات. - -أي تشتت في الـ positioning يكلّفنا 3 أشهر من الإنتاجية. لذلك هذا الملف ثابت. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/POST_MERGE_VERIFICATION.md b/dealix/docs/POST_MERGE_VERIFICATION.md index e04e9d72..4cf2648f 100644 --- a/dealix/docs/POST_MERGE_VERIFICATION.md +++ b/dealix/docs/POST_MERGE_VERIFICATION.md @@ -27,19 +27,37 @@ Optional secret-pattern scan (adapt to your environment): rg "ghp_|github_pat_|sk_live_" --glob '!htmlcov/**' --glob '!.venv/**' ``` +## Growth Control Tower — Wave 0 (baseline) + +بعد إضافة مسارات `/api/v1/platform/*` و`/api/v1/intelligence/*` أعد تشغيل نفس الأوامر أعلاه وتأكد من `ROUTE_CHECK_OK`. Staging: [`STAGING_DEPLOYMENT.md`](STAGING_DEPLOYMENT.md) و[`ops/RAILWAY_AI_COMPANY_BIND.md`](ops/RAILWAY_AI_COMPANY_BIND.md). + +## Launch Ops + Service Tower (خرائط ثابتة) + +- تأكد من: `GET /api/v1/launch/private-beta/offer`، `GET /api/v1/launch/go-no-go`، `GET /api/v1/services/verticals`، `GET /api/v1/services/contracts/templates`. +- `scripts/smoke_staging.py` يتضمن هذه المسارات عند التحقق من staging. + +## Frontend map + لغتين (Landing / EN) + +- مرجع المشغّل: [`FRONTEND_AND_API_MAP.md`](FRONTEND_AND_API_MAP.md) — يربط `landing/*.html` بمسارات البرج والتحكم. +- صفحات إنجليزية مختصرة: `landing/private-beta-en.html`، `services-en.html`، `command-center-en.html` مع روابط من العربية. +- **Revenue offer i18n:** `GET /api/v1/revenue-launch/offer?lang=en` يضيف `title_en` / `summary_en` (وحقول إنجليزية موازية للقوائم حيث وُجدت) مع الإبقاء على الحقول العربية. + ## Last recorded run (workspace snapshot) | Step | Result | |------|--------| -| Git HEAD | `2d776cb` on branch `dealix-v3-autonomous-revenue-os` (re-run on `main` after merge) | -| compileall | OK (`api`, `auto_client_acquisition`, `integrations`, `dealix`) | -| pytest | `516 passed`, `6 skipped`, `0 failed` (`APP_ENV=test`, dummy LLM keys) — re-run after your merge | +| Git HEAD | `16e8ba2` on branch `ai-company` (re-run on `main` after merge) | +| compileall | OK (`api`, `auto_client_acquisition`, `db`, `core`) | +| pytest | `652 passed`, `6 skipped`, `0 failed` (`APP_ENV=test`, dummy LLM keys) — 2026-05-01 بعد `launch_readiness` + تكامل الواجهات؛ أعد التشغيل بعد الدمج | | `print_routes.py` | `ROUTE_CHECK_OK no duplicate method+path` | -| `smoke_inprocess.py` | `SMOKE_INPROCESS_OK` | +| `smoke_inprocess.py` | `SMOKE_INPROCESS_OK` (يشمل `GET /api/v1/revenue-launch/offer` و`GET /api/v1/revenue-launch/offer?lang=en`) | +| `launch_readiness_check.py` | `VERDICT: GO_PRIVATE_BETA`، exit `0` — يفحص محلياً: `/health`، customer-ops (checklist/sla/connectors)، `services/catalog` (حقول التسعير وProof لكل خدمة)، `launch/private-beta/offer`، `security-curator/demo`، ملفات `landing/companies|marketers|private-beta.html`، و`WHATSAPP_ALLOW_LIVE_SEND=false`. مع `--base-url` أو `STAGING_BASE_URL`: نفس المسارات عن بُعد → `PAID_BETA_READY` إذا نجحت كلها؛ وإلا `NO_GO`. اختياري: `--secrets` لفحص أنماط تسرّب شائعة. `--json` لمخرجات آلة | +| `smoke_staging.py` | يتطلب `STAGING_BASE_URL` — يشمل الآن `…/revenue-launch/offer?lang=en`؛ شغّله على الاستضافة الفعلية قبل أول عميل | +| Frontend + EN + `?lang=en` | وثّق في [`FRONTEND_AND_API_MAP.md`](FRONTEND_AND_API_MAP.md) — أعد `pytest` بعد أي تغيير على `revenue_launch` | ## CI -Confirm GitHub Actions workflow [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) is green on the merged commit. +Confirm GitHub Actions workflow [`.github/workflows/dealix-api-ci.yml`](../../.github/workflows/dealix-api-ci.yml) is green on the merged commit (jobs: `pytest`, `smoke_inprocess`, `launch_readiness`). لإعداد **branch protection** وأسماء الـ checks: [`BRANCH_PROTECTION_AND_CI.md`](BRANCH_PROTECTION_AND_CI.md). ## Manual follow-ups (not automated) diff --git a/dealix/docs/PRIVATE_BETA_LAUNCH_TODAY.md b/dealix/docs/PRIVATE_BETA_LAUNCH_TODAY.md index 6103f933..8a670720 100644 --- a/dealix/docs/PRIVATE_BETA_LAUNCH_TODAY.md +++ b/dealix/docs/PRIVATE_BETA_LAUNCH_TODAY.md @@ -1,110 +1,59 @@ -# Private Beta Launch — Today's Plan +# Private Beta — «اليوم» (تشغيل وبيع) -> **القرار:** ندشّن **Private Beta** اليوم، ليس Public Launch. -> **العرض الأساسي:** "10 فرص في 10 دقائق" + Pilot 7 أيام + Proof Pack. +قائمة تشغيل قبل أول عميل بيتا. تفاصيل تقنية إضافية: [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md). ديمو ١٢ دقيقة: [`DEMO_SCRIPT_12_MINUTES.md`](DEMO_SCRIPT_12_MINUTES.md). رسائل أول ٢٠: [`FIRST_20_OUTREACH_MESSAGES.md`](FIRST_20_OUTREACH_MESSAGES.md). ---- +## ما ندشّنه اليوم -## 1. العرض +- **Private Beta** (ليس إطلاقاً عاماً): عرض Pilot + قيمة واضحة + موافقات. +- **أربعة عروض أولى:** تشخيص مجاني، ذكاء قوائم، سباق ١٠ فرص، Growth OS Pilot — انظر [`landing/services.html`](../landing/services.html). +- **نسختان للصفحات الحرجة:** [`landing/private-beta-en.html`](../landing/private-beta-en.html)، [`landing/services-en.html`](../landing/services-en.html)، [`landing/command-center-en.html`](../landing/command-center-en.html) — روابط «English» من الصفحات العربية المقابلة. خريطة كاملة: [`FRONTEND_AND_API_MAP.md`](FRONTEND_AND_API_MAP.md). +- **Autonomous Revenue Company OS:** مرجع الفئة والطبقات — [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](AUTONOMOUS_REVENUE_COMPANY_OS.md)؛ واجهة تشغيل تجريبية: `GET /api/v1/operator/bundles`، `POST /api/v1/operator/chat/message`؛ طبقة أحداث وبطاقات: `GET /api/v1/revenue-os/company-os/command-feed/demo`. -``` -Pilot 7 أيام: 499 ريال أو مجاني مقابل case study -- 10 فرص B2B مع why-now -- 10 رسائل عربية جاهزة -- contactability + سياسة عدم الإرسال البارد -- متابعة لمدة 7 أيام -- Proof Pack بعد الأسبوع +## ما لا ندشّنه اليوم -Paid Pilot 30 يوم: 1,500–3,000 ريال -Growth OS اشتراك شهري: 2,999 ريال -``` +- إرسال واتساب جماعي بارد، Gmail إرسال تلقائي، إدراج تقويم حي بدون موافقة، شحن بطاقات داخل المنتج، scraping LinkedIn. -## 2. من نستهدف اليوم (أول 20) +## بيئة -1. وكالات تسويق B2B سعودية. -2. مستشارون نمو. -3. شركات تدريب B2B. -4. SaaS سعودية صغيرة-متوسطة. -5. شركات عقار/خدمات لديها واتساب نشط. -6. أصدقاء مؤسسين سعوديين. +- [ ] Staging يعمل (`GET /health`). +- [ ] `WHATSAPP_ALLOW_LIVE_SEND=false` (افتراضي) ما لم يُوثَّق خلاف ذلك. +- [ ] أسرار Moyasar / Google / Meta **غير** مكشوفة في الريبو أو اللوجات. -## 3. Demo Flow (12 دقيقة) +## API سريعة للتحقق -راجع [`DEMO_SCRIPT_12_MINUTES.md`](DEMO_SCRIPT_12_MINUTES.md). +- [ ] `GET /api/v1/growth-operator/missions` +- [ ] `GET /api/v1/platform/inbox/feed` +- [ ] `GET /api/v1/platform/proof/overview` +- [ ] `POST /api/v1/platform/events/ingest` مع `source: trusted_simulation` +- [ ] `GET /api/v1/security-curator/demo` +- [ ] `GET /api/v1/services/catalog` +- [ ] `GET /api/v1/launch/go-no-go` و `GET /api/v1/launch/scorecard` +- [ ] `GET /api/v1/revenue-launch/offer` و `GET /api/v1/revenue-launch/offer?lang=en` (تسميات إنجليزية إضافية بجانب العربية) -## 4. شروط القبول للعميل +## Go / No-Go (آلي demo) -العميل المثالي للـPrivate Beta: -- شركة سعودية أو خليجية B2B. -- لديها ≥3 موظفين مبيعات أو نمو. -- مرتاحة بالعربي + الإنجليزي. -- مستعدة لإعطاء feedback أسبوعي. -- تقبل أنه draft-first (لا live send افتراضياً). +شغّل `GET /api/v1/launch/go-no-go` بعد `pytest` و`print_routes`. تحذير staging متوقع حتى يُفعّل `STAGING_BASE_URL` في `smoke_staging.py`. -## 5. ما يعمل الآن (Phase 1 ready) +## تحقق آلي (مرجع الجلسة) -- `/api/v1/personal-operator/daily-brief` -- `/api/v1/growth-operator/missions` -- `/api/v1/growth-operator/proof-pack/demo` -- `/api/v1/intelligence/command-feed/demo` -- `/api/v1/intelligence/missions` + `/missions/recommend` -- `/api/v1/intelligence/simulate-opportunity` -- `/api/v1/intelligence/board-brief/demo` -- `/api/v1/platform/services/catalog` -- `/api/v1/platform/inbox/feed` -- `/api/v1/platform/proof-ledger/demo` -- `/api/v1/security-curator/demo` -- `/api/v1/growth-curator/report/demo` -- `/api/v1/meeting-intelligence/brief/demo` -- `/api/v1/connector-catalog/catalog` +من مجلد `dealix`: `python -m pytest -q --no-cov`، ثم `python scripts/smoke_inprocess.py`، ثم `python scripts/launch_readiness_check.py`. بعد نشر staging: `python scripts/smoke_staging.py --base-url https://`. سجّل النتائج في [`POST_MERGE_VERIFICATION.md`](POST_MERGE_VERIFICATION.md). -## 6. ما يبقى Draft فقط +## عملية بشرية -- WhatsApp send (live flag OFF). -- Gmail send (live flag OFF). -- Calendar insert (live flag OFF). -- Moyasar charge (live flag OFF — invoice/link manual). -- Social DMs. +- [ ] اتفاق pilot موقّع (نطاق، PDPL، قنوات مسموحة). +- [ ] مسؤول مراجعة لكل «إرسال» أو «دفع» خارجي. +- [ ] قناة دعم للعميل (واتساب أو إيميل داخلي). -## 7. المخاطر +## بعد الجلسة الأولى -- WhatsApp: PDPL — لا cold بدون opt-in. -- Gmail: SPF/DKIM/DMARC للـdomain. -- Moyasar: live keys ممنوعة في staging. -- Secrets: GitHub Push Protection + Patch Firewall + Trace Redactor. -- Hallucinations: Saudi Tone + Safety evals قبل publish. +- صدّر ملاحظات إلى `growth-curator` و`meeting-intelligence` كتحسين لمسودات الأسبوع التالي. -## 8. Go-Checklist قبل أول demo +## الصفحات (عرض) -- [ ] CI أخضر (`Dealix API CI`). -- [ ] `pytest -q` ≥663 passed. -- [ ] Staging URL يستجيب على `/health`. -- [ ] جميع env flags الـ`*_ALLOW_LIVE_*` = false. -- [ ] `secret_redactor` و`patch_firewall` يعملان. -- [ ] Demo URL مفتوح على `/api/v1/intelligence/command-feed/demo`. -- [ ] Demo URL مفتوح على `/api/v1/growth-operator/proof-pack/demo`. -- [ ] رسالة DM 1 جاهزة (راجع `FIRST_20_OUTREACH_MESSAGES.md`). +- [`landing/private-beta.html`](../landing/private-beta.html) +- [`landing/list-intelligence.html`](../landing/list-intelligence.html) +- [`landing/growth-os.html`](../landing/growth-os.html) -## 9. Post-Demo Checklist +## الإطلاق التجاري الأوسع -- [ ] قائمة 5 demos مجدولة. -- [ ] قائمة 3 pilots محتملين. -- [ ] feedback مكتوب لكل demo. -- [ ] Proof Pack template جاهز. -- [ ] أول Moyasar invoice draft (إن وُجد عميل). - -## 10. ما لا نفعله اليوم - -- لا public launch. -- لا live WhatsApp send. -- لا charges. -- لا "نضمن نتائج". -- لا scraping. -- لا إصدار صحفي. - -## 11. الخطوة التالية بعد أول 3 demos - -- استخراج الاعتراضات الموحدة → cards في `command_feed`. -- معايرة الأسعار (هل 499 رخيص جداً؟). -- اختيار vertical: تدريب أو وكالات أو SaaS. -- تجهيز case study واحد على الأقل. +انظر [`COMMERCIAL_LAUNCH_MASTER_PLAN.md`](COMMERCIAL_LAUNCH_MASTER_PLAN.md). diff --git a/dealix/docs/PRIVATE_BETA_OPERATING_BOARD.md b/dealix/docs/PRIVATE_BETA_OPERATING_BOARD.md index 73030a83..dd18a29c 100644 --- a/dealix/docs/PRIVATE_BETA_OPERATING_BOARD.md +++ b/dealix/docs/PRIVATE_BETA_OPERATING_BOARD.md @@ -1,183 +1,74 @@ -# Private Beta Operating Board +# Dealix Private Beta Operating Board -> **القاعدة:** كل prospect يدخل هذا الـ Board. كل خطوة تُسجّل. كل تأخير يولّد action item. هذا هو الـ source of truth للأسبوع. +**الغرض:** لوحة تشغيل يومية تربط **التواصل اليدوي** بالـ **funnel** حتى **الدفع** و**Proof Pack** — بدون أتمتة مخالفة للقنوات. + +**مرافق:** [`PAID_BETA_OPERATING_PLAYBOOK.md`](PAID_BETA_OPERATING_PLAYBOOK.md) — سكربت `scripts/paid_beta_daily_scorecard.py` لتلخيص الأرقام اليومية. --- -## 1. أين يعيش هذا الـ Board؟ +## 1. إنشاء الـ Sheet (Google Sheets أو Excel) -- **Primary:** Google Sheet خاص بك (لا تشاركه بصلاحيات edit مع أحد). -- **Backup:** نسخة في `pilots/operating_board.csv` (gitignored) في المستودع. -- **عدم التشارك:** هذا Sheet يحتوي PII لأشخاص لم يوافقوا — لا تشاركه. +**اسم المقترح:** `Dealix Private Beta Operating Board` + +### أعمدة سجل التواصل (صف واحد = شخص/شركة/لمسة) + +| العمود | الوصف | +|--------|--------| +| `date` | تاريخ اللمسة | +| `company` | اسم الشركة | +| `person` | اسم الشخص | +| `segment` | وكالة / SaaS / تدريب / استشارات / … | +| `source` | LinkedIn / إيميل / إحالة / … | +| `channel` | القناة المستخدمة | +| `message_sent` | نعم/لا + اختصار الموضوع | +| `reply_status` | لا رد / رد سلبي / رد إيجابي / محجوز ديمو | +| `demo_booked` | تاريخ/وقت أو فارغ | +| `diagnostic_sent` | نعم/لا | +| `pilot_offered` | نعم/لا | +| `price` | 0 / 499 / 1500 / … | +| `paid` | نعم/لا + المرجع (فاتورة/حوالة) | +| `proof_pack_sent` | نعم/لا + تاريخ | +| `next_step` | جملة واحدة | +| `notes` | حر | --- -## 2. الأعمدة (15 عمود) +## 2. ملخص نهاية اليوم (Executive snapshot) -| # | Column | النوع | شرح | مثال | -|---|--------|------|------|------| -| 1 | `company` | text | اسم الشركة الرسمي | شركة الأثاث المتقدم | -| 2 | `person` | text | اسم صاحب القرار | أحمد العتيبي | -| 3 | `segment` | enum | `agency` / `b2b_company` / `partnership` | b2b_company | -| 4 | `source` | enum | `linkedin_lead_form` / `referral` / `inbound` / `event` / `personal_network` | personal_network | -| 5 | `channel` | enum | `whatsapp` (opt-in) / `email` / `linkedin_dm_manual` / `call` | linkedin_dm_manual | -| 6 | `message_sent` | date | تاريخ إرسال أول رسالة | 2026-05-01 | -| 7 | `reply_status` | enum | `none` / `positive` / `objection` / `not_now` / `bounce` | positive | -| 8 | `demo_booked` | date \| null | تاريخ الديمو لو حُجز | 2026-05-03 | -| 9 | `diagnostic_sent` | date \| null | تاريخ تسليم Free Diagnostic | 2026-05-04 | -| 10 | `pilot_offered` | date \| null | تاريخ عرض Pilot 499 | 2026-05-05 | -| 11 | `price` | int | السعر المعروض (499 / 1500 / 2999) | 499 | -| 12 | `paid` | enum | `no` / `pending_invoice` / `paid` / `case_study` | pending_invoice | -| 13 | `proof_pack_sent` | date \| null | تاريخ تسليم Proof Pack | null | -| 14 | `next_step` | text | الإجراء التالي وتاريخه | 2026-05-06: follow-up #1 | -| 15 | `notes` | text | ملاحظات (بدون PII حساسة) | اهتم بـ partnerships في الرياض | +املأ يومياً (يمكن صفاً ثانياً «Summary» أو تبويب): + +| المقياس | قيمة اليوم | +|---------|------------| +| Messages sent | | +| Positive replies | | +| Demos booked | | +| Pilots offered | | +| Payments requested | | +| Payments received | | +| Proof packs delivered | | +| Top objection | | +| Next improvement | | --- -## 3. Status Flow +## 3. ربط مع السكربت -``` -prospect_added - → message_sent - → reply_status (none | positive | objection | not_now | bounce) - → demo_booked - → diagnostic_sent (T+24) - → pilot_offered (T+48) - → paid (or case_study) - → proof_pack_sent (T+7 من بدء Pilot) - → renewal_or_upsell -``` +1. صدّر ملخص اليوم إلى JSON (يدوياً أو من Sheet بسكربت صغير خارج الريبو إن رغبت). +2. شغّل: -كل عميل يجب أن يكون في حالة واحدة من هذه المراحل في كل لحظة. + ```bash + python scripts/paid_beta_daily_scorecard.py --file path/to/today.json + ``` + + أو مرّر الأرقام كوسائط (انظر `python scripts/paid_beta_daily_scorecard.py --help`). --- -## 4. أهداف الأسبوع (الصف الأول من الـ Board) +## 4. قواعد الجودة -| Metric | Target | Tracking | -|--------|-------:|----------| -| Prospects added | 50–70 | عداد عمود `company` | -| Messages sent | 50–70 | عدد التواريخ في `message_sent` | -| Positive replies | 5–10 | `reply_status = positive` | -| Demos booked | 3–5 | عدد التواريخ في `demo_booked` | -| Diagnostics sent | 2–4 | عدد التواريخ في `diagnostic_sent` | -| Pilots offered | 2–3 | عدد التواريخ في `pilot_offered` | -| Paid | 1+ | `paid = paid` | -| Proof packs sent | 1+ | عدد التواريخ في `proof_pack_sent` | +- لا تسجيل أسرار أو مفاتيح API في الـ Sheet. +- تقليل PII: أسماء الشركات كافية في المرحلة الأولى؛ تجنب تخزين أرقام هواتف غير ضرورية. --- -## 5. ICP Distribution (في 50–70 prospect) - -``` -Agencies (B2B marketing agencies) 20% -Construction & home services 20% -Clinics + dental + aesthetic 15% -Logistics + last-mile 15% -F&B (restaurants + cloud kitchens) 10% -Retail (offline + ecom) 10% -EdTech / SaaS B2B 10% -``` - -اضبط النسبة حسب القطاعات التي يخدمها العميل المثالي. - ---- - -## 6. Cadence لكل prospect - -| اليوم | الإجراء | -|------|--------| -| Day 0 | إرسال الرسالة الأولى + تسجيلها في الـ Board | -| Day 1 | تحقق من reply_status + Operating Board update | -| Day 2 | متابعة #1 (لو لا رد) — قالب Follow-up #1 | -| Day 4 | متابعة #2 (لو لا رد) — تحويل قناة لو منطقي | -| Day 7 | قرار keep / drop / nurture | -| Day 14 | nurture: رسالة قيمة (مثل Diagnostic مجاني للناس البطيئين) | - ---- - -## 7. Follow-up Templates (3 موجات) - -### Follow-up #1 (يوم 2) -> أنت اللي ذكرت . حضّرت لك مثال محدد لشركتك (3 فرص + رسالة جاهزة بالعربي + مخاطرة موجودة الآن). أرسله لك بعد ردك. ما يأخذ منك ≥3 دقائق. - -### Follow-up #2 (يوم 4 — تحويل قناة لو منطقي) -> سمعت أن . هذا أفضل وقت تجرب نموذج بسيط: 10 فرص + رسائل خلال 48 ساعة، 499 ريال، يبدأ غداً. لو ما عجبك في 7 أيام، تستردّ المبلغ. - -### Follow-up #3 (يوم 7 — قرار) -> سأوقف المحاولات بعد هذه الرسالة. لو هذا توقيت غير مناسب، حدد لي شهر تجارب أخرى — وأذكّرك. مكتب مفتوح دائماً. - -كل القوالب تمر `safety_eval` + `saudi_tone_eval` قبل الإرسال. - ---- - -## 8. Daily Routine لإدارة الـ Board - -### الصباح (15 دقيقة) -- افتح الـ Sheet. -- صفّ حسب `next_step` (date asc). -- نفّذ الـ next_step لكل prospect وصلت تاريخه. -- شغّل `paid_beta_daily_scorecard.py`. - -### الظهر (15 دقيقة) -- أضف prospects الجدد (5–10 يومياً). -- خصّص الرسالة لكل واحد (اسم + قطاع + city + why_now). -- اعتمد drafts. - -### آخر اليوم (10 دقائق) -- حدّث `reply_status` للذين ردّوا. -- حدّث `next_step` لكل prospect نشط. -- شغّل `paid_beta_daily_scorecard.py --json` واحفظه يومياً. - ---- - -## 9. Privacy & PDPL - -- **لا تشارك** هذا الـ Sheet بصلاحيات edit مع أحد. -- **لا تخزّن** أرقام واتساب لأشخاص لم يوافقوا opt-in. -- **لا تنسخ** الـ Sheet إلى أدوات خارجية بدون اتفاقية data processing. -- **احذف** البيانات بعد 90 يوم لمن لم يرد ولم يطلب nurture. -- **سجّل** كل export في Action Ledger. - ---- - -## 10. مثال صف كامل - -``` -| company | شركة الأثاث المتقدم | -| person | أحمد العتيبي | -| segment | b2b_company | -| source | personal_network | -| channel | linkedin_dm_manual | -| message_sent | 2026-05-01 | -| reply_status | positive | -| demo_booked | 2026-05-03 | -| diagnostic_sent| 2026-05-04 | -| pilot_offered | 2026-05-05 | -| price | 499 | -| paid | pending_invoice | -| proof_pack_sent| null | -| next_step | 2026-05-06: متابعة دفع invoice | -| notes | اهتم بـ partnerships في الرياض | -``` - ---- - -## 11. Sheet template (CSV header للنسخ) - -```csv -company,person,segment,source,channel,message_sent,reply_status,demo_booked,diagnostic_sent,pilot_offered,price,paid,proof_pack_sent,next_step,notes -``` - -ضع هذا الصف كـ header في Google Sheet جديد. ابدأ. - ---- - -## 12. القرار - -``` -الـ Board ليس "نظاماً". -الـ Board هو "الذاكرة العاملة" لأسبوعك. -بدون الـ Board: prospects ينسون، follow-ups تضيع، payments تتأخر. -مع الـ Board: 50 prospect → 5 ردود → 3 ديمو → 1 paid. -``` +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/PRIVATE_BETA_RUNBOOK.md b/dealix/docs/PRIVATE_BETA_RUNBOOK.md index c3ebe55b..db404bdc 100644 --- a/dealix/docs/PRIVATE_BETA_RUNBOOK.md +++ b/dealix/docs/PRIVATE_BETA_RUNBOOK.md @@ -1,7 +1,8 @@ # Private Beta — دليل تشغيل Dealix **يوم التدشين (فهرس ساعة بساعة):** [`LAUNCH_DAY_RUNBOOK_AR.md`](LAUNCH_DAY_RUNBOOK_AR.md). -**قائمة بوابات موحّدة:** [`BETA_PRIVATE_GATES_CHECKLIST.md`](BETA_PRIVATE_GATES_CHECKLIST.md). +**قائمة بوابات موحّدة:** [`BETA_PRIVATE_GATES_CHECKLIST.md`](BETA_PRIVATE_GATES_CHECKLIST.md). +**تشغيل مدفوع ولوحة يومية:** [`COMMERCIAL_GO_LIVE_CHECKLIST.md`](COMMERCIAL_GO_LIVE_CHECKLIST.md) (التسلسل الكامل)، [`PAID_BETA_OPERATING_PLAYBOOK.md`](PAID_BETA_OPERATING_PLAYBOOK.md)، [`FIRST_PILOT_DELIVERY_WORKFLOW.md`](FIRST_PILOT_DELIVERY_WORKFLOW.md)، [`PRIVATE_BETA_OPERATING_BOARD.md`](PRIVATE_BETA_OPERATING_BOARD.md). ## الجمهور diff --git a/dealix/docs/PROHIBITED_CLAIMS.md b/dealix/docs/PROHIBITED_CLAIMS.md index 705a234b..068a95ae 100644 --- a/dealix/docs/PROHIBITED_CLAIMS.md +++ b/dealix/docs/PROHIBITED_CLAIMS.md @@ -1,107 +1,19 @@ -# Prohibited Claims — ممنوع تماماً +# ادعاءات ممنوعة في التسويق والمبيعات -> أي landing page / رسالة بيع / ديمو / قائمة مزايا تحتوي إحدى هذه العبارات يجب رفضها فوراً. +لا تُستخدم هذه العبارات في صفحات الهبوط، الإعلانات، الرسائل، أو الديمو: ---- +- «نضمن لك عملاء» أو «نضاعف مبيعاتك مضموناً». +- «نرسل لكل السوق» أو «نوصل لكل الأرقام». +- «نسحب بيانات LinkedIn» أو «نستخرج جهات الاتصال تلقائياً من LinkedIn». +- «نرسل واتساب بارد لقائمة كبيرة بدون موافقة». +- «نعمل بدون موافقتك» أو «نُرسل نيابة عنك مباشرة». +- «نتجاوز سياسات المنصات» أو «نتفادى الحظر بطرق غير شرعية». +- أي وعد بنتائج مالية محددة بدون أساس قابل للتحقق والعقد. -## 1. ادعاءات نتائج مضمونة +## بدائل مسموحة -- ❌ "نضمن لك عملاء" -- ❌ "نضمن مبيعات" -- ❌ "نتائج مضمونة 100%" -- ❌ "ROI مضمون 10x" -- ❌ "Money-back guarantee" (إلا في حالة Pilot واضح بشروط محدودة) +- «نُجهّز فرصاً مؤهّلة ورسائل عربية، وأنت توافق قبل أي تواصل حساس». +- «نقيّم قابلية التواصل (safe / needs_review / blocked) لتقليل المخاطر». +- «Proof Pack يوثّق المسودات والموافقات والمخاطر التي تم منعها» (حسب ما يُقاس فعلياً). -**القاعدة:** نقول "Proof Pack بالأرقام" بدلاً من "نتيجة مضمونة". - ---- - -## 2. ادعاءات scraping أو بيانات غير مصرّح بها - -- ❌ "نسحب كل بيانات LinkedIn" -- ❌ "نستخرج جميع الأرقام من Google" -- ❌ "نجمع leads من أي مكان" -- ❌ "نحصل على إيميلات decision makers من Apollo" - -**القاعدة:** نقول "مصادر مصرّح بها: CRM، LinkedIn Lead Forms، website forms، manual research معتمد". - ---- - -## 3. ادعاءات automation كاملة - -- ❌ "نرسل تلقائياً للجميع" -- ❌ "Dealix يدير كل شيء بدونك" -- ❌ "Auto-DM على LinkedIn" -- ❌ "Cold WhatsApp campaigns جاهزة" - -**القاعدة:** نقول "Approval-first — لا إرسال بدون موافقتك. Drafts فقط افتراضياً". - ---- - -## 4. ادعاءات تجاوز الموافقات - -- ❌ "بدون مكالمة" -- ❌ "بدون فريق" -- ❌ "بدون مراجعة" -- ❌ "Ai-only — لا تدخل بشري" - -**القاعدة:** نقول "بشرية القرار، آلية التنفيذ — Approval Center في كل خطوة". - ---- - -## 5. ادعاءات منصات منافسة - -- ❌ "بديل HubSpot" -- ❌ "أرخص من Salesforce" -- ❌ "نقتل CRM التقليدي" -- ❌ "أقوى من Gong" - -**القاعدة:** نقول "Saudi Revenue Execution OS — يكمّل CRMs، لا يستبدلها". - ---- - -## 6. ادعاءات قانونية/مالية - -- ❌ "نتجاوز PDPL" -- ❌ "نخفي بياناتك من الجهات الرسمية" -- ❌ "نضمن عودة استثمارك" -- ❌ "Tax-deductible automatically" - -**القاعدة:** نقول "PDPL-aware. DPA draft جاهز. أي عقد يحتاج مراجعة قانونية". - ---- - -## 7. ادعاءات طبية أو جدية - -- ❌ "يعالج مشاكل العمل" -- ❌ "يشفي شركتك من الركود" -- ❌ "علاج مضمون لقلة العملاء" - -**القاعدة:** لا تستخدم لغة طبية أو علاجية. نقول "نحسّن الـ pipeline". - ---- - -## 8. ادعاءات سرعة مبالغ فيها - -- ❌ "10 عملاء خلال 24 ساعة" -- ❌ "مليون ريال خلال شهر" -- ❌ "نمو 1000% أسبوعياً" - -**القاعدة:** نقول "10 فرص خلال 7 أيام، ضمن workflow approval-first". - ---- - -## كيف نفرضها تقنياً - -1. **Safety Eval** — `agent_observability.safety_eval()` يكتشف "ضمان 100%" و"آخر فرصة" تلقائياً. -2. **Saudi Tone Eval** — يرفض "best-in-class" و"synergy". -3. **Quality Review Gate** — في `service_excellence.quality_review` أي خدمة بدون proof_metrics blocked. -4. **Tool Action Planner** — يحظر LinkedIn scraping و auto-DM في الكود مباشرة. -5. **Test `test_positioning_lock.py`** — يفحص landing pages وlanding/services.html والـ docs ضد هذه القائمة. - ---- - -## القاعدة الذهبية - -> **لو تحتاج إثبات قبل القول، لا تقله.** -> Dealix يبيع نتائج محسوبة بـ Proof Pack، لا وعود تسويقية. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/REVENUE_TODAY_PLAYBOOK.md b/dealix/docs/REVENUE_TODAY_PLAYBOOK.md index 646a7fd5..81bf2954 100644 --- a/dealix/docs/REVENUE_TODAY_PLAYBOOK.md +++ b/dealix/docs/REVENUE_TODAY_PLAYBOOK.md @@ -1,202 +1,66 @@ -# Revenue Today Playbook — تحويل Dealix إلى دخل اليوم +# Revenue Today — دليل تشغيل دخل اليوم (Private Beta) -> **القاعدة:** الهدف اليوم ليس إطلاق عام. الهدف **أول 499 ريال أو commitment** عبر Private Beta + Pilot 7 أيام. +> جمع بين المنتج والبيع دون كسر قواعد الأمان: **لا إرسال حي تلقائي، لا شحن من API داخل Dealix في هذه المرحلة، لا واتساب بارد، لا أتمتة LinkedIn المخالفة.** ---- +## 1. عرض ٤٩٩ ريال (Pilot ٧ أيام) -## 1. العروض المدفوعة المتاحة اليوم +- **الوعد:** ١٠ فرص B2B، لماذا الآن، رسائل عربية (مسودات)، فحص قابلية التواصل، خطة متابعة ٧ أيام، Proof Pack مختصر. +- **التحصيل:** فاتورة أو رابط دفع يدوي من لوحة Moyasar (sandbox أو live حسب سياسة شركتك) — راجع [وثائق Moyasar](https://docs.moyasar.com/). +- **API للمرجعية:** `GET /api/v1/revenue-launch/offer` (يحتوي `pilot_499`)؛ للتسميات الإنجليزية بجانب العربية: `GET /api/v1/revenue-launch/offer?lang=en`. -### Pilot 7 أيام — 499 ريال (الأساسي) -- 10 فرص B2B + رسائل عربية + خطة متابعة + Proof Pack. -- بدائل: مجاني مقابل case study. -- مدة التسليم: 7 أيام، تبدأ يوم الأحد بعد الدفع. +## 2. عرض Growth OS Pilot (١٥٠٠–٣٠٠٠ ريال / ٣٠ يوم) -### Growth OS Pilot — 1,500–3,000 ريال (30 يوم) -- التشغيل الكامل لشهر: command feed + drafts + اجتماعات + Proof Pack أسبوعي. -- الترقية المنطقية لـ Growth OS Monthly (2,999/شهر). +- تشغيل أوسع: موجز، فرص، ذكاء قوائم، مسودات قنوات، Proof أسبوعي — كلها **مسودات وموافقة** ما لم تُفعّل سياسات الإنتاج صراحةً لاحقاً. -### Free Growth Diagnostic — 0 ريال (24 ساعة) -- 3 فرص + رسالة + توصية بخدمة مدفوعة. -- يقود لـ Pilot 499 أو Growth OS Pilot. +## 3. من نستهدف اليوم ---- +- ٥ وكالات B2B، ٥ تدريب/استشارات، ٥ SaaS صغيرة، ٥ خدمات بواتساب نشط (يدوياً — لا scraping). -## 2. من نستهدف اليوم (4 فئات × 5 = 20 prospect) +## 4. أول ٢٠ تواصل -| Segment | عدد | عرض أساسي | عرض احتياطي | قناة | -|---------|----:|-----------|-------------|------| -| وكالات تسويق B2B | 5 | Growth OS Pilot | Free Case Study | Email | -| تدريب/استشارات | 5 | Pilot 499 | Free Case Study | Email | -| SaaS/تقنية صغيرة | 5 | Pilot 499 | Growth OS Pilot | LinkedIn Lead Form | -| خدمات بقاعدة واتساب | 5 | List Intelligence | WhatsApp Compliance | Email | +- قوالب: `GET /api/v1/launch/outreach/first-20` و`GET /api/v1/revenue-launch/outreach/first-20` (عينات + شرائح). +- **نسخ يدوي فقط** من الرسائل. -**القواعد:** -- لا scraping ولا قوائم مشتراة. -- استخدم علاقاتك المباشرة + جهات تعرفها. -- كل رسالة يدوية، لا automation. -- حد أقصى 3 follow-ups ثم أرشفة. +## 5. سيناريو الردود ---- +- `GET /api/v1/revenue-launch/demo-flow` → قسم `objections`. -## 3. أول 20 رسالة — جاهزة للنسخ +## 6. حجز الديمو -استخدم endpoint: -``` -GET /api/v1/launch/outreach/first-20 -``` +- ديمو ١٢ دقيقة: `docs/DEMO_SCRIPT_12_MINUTES.md` و`GET /api/v1/revenue-launch/demo-flow`. -أو يدوياً: +## 7. إغلاق أول Pilot -### رسالة عامة -هلا [الاسم]، أطلقنا Beta محدودة لـ Dealix. -Dealix يساعد الشركات تطلع فرص B2B مناسبة، يكتب الرسائل بالعربي، ويخلي صانع القرار يوافق قبل أي تواصل، وبعدها يعطي Proof Pack. -أفتح 5 مقاعد Pilot هذا الأسبوع. يناسبك أعطيك Free Diagnostic لشركتكم؟ +- `GET /api/v1/revenue-launch/pilot-delivery` — نموذج intake، خطة ٢٤ ساعة، مخرجات First 10 و List Intelligence. -### وكالة -هلا [الاسم]، عندي Beta خاص للوكالات. -Dealix يطلع فرص لعملاءكم، يجهز رسائل عربية، يدير موافقات، ويطلع Proof Pack بعلامة الوكالة. -أبحث عن وكالة واحدة نجرب معها Pilot مشترك على عميل حقيقي. يناسبك ديمو 15 دقيقة؟ +## 8. ماذا نطلب من العميل -### SaaS -هلا [الاسم]، رأيت إصدار النسخة الجديدة من منتجكم — مبروك. -نشتغل على مدير نمو عربي يطلع 10 فرص B2B عبر LinkedIn Lead Forms (لا scraping) ويكتب الرسائل بالعربي. -أبغى أجربه مع شركة SaaS سعودية واحدة. يناسبك ديمو 12 دقيقة؟ +- حقول intake في نفس الـ endpoint أعلاه (موقع، قطاع، مدينة، عرض، قائمة، قنوات، موافق واتساب). ---- +## 9. ماذا نسلّم خلال ٢٤ ساعة -## 4. الديمو — 12 دقيقة +- حسب الاتفاق: عينة فرص + مسودات + تقرير مخاطر — **بعد موافقة داخلية** على الصياغة. -استخدم: -``` -GET /api/v1/launch/demo/flow -``` +## 10. قالب Proof Pack -ملخص: 0–2 الفكرة → 2–4 Daily Brief → 4–6 10 فرص → 6–8 Trust + Approval → 8–10 الأمان → 10–12 العرض والـCTA. +- `GET /api/v1/revenue-launch/proof-pack/template`. -**الإغلاق:** -> "تمام، نبدأ Pilot 7 أيام بـ499 ريال. أرسل لك خلال ساعة intake form + Moyasar invoice + موعد كيك-أوف." +## 11. الدفع اليدوي (Moyasar) ---- +- `GET /api/v1/revenue-launch/payment/manual-flow` — تعليمات لوحة التحكم، قالب رسالة، قائمة تأكيد بعد الدفع. +- **تذكير:** المبالغ غالباً بالهللات (١ ريال = ١٠٠ هللة) — راجع وثائق Moyasar. -## 5. Pipeline Tracker +## 12. ما لا نفعله اليوم -8 stages: -``` -identified → contacted → replied → demo_booked → -diagnostic_sent → pilot_offered → paid → (or lost) -``` +- لا charge من كود Dealix، لا إرسال Gmail/واتساب/تقويم حي، لا scraping LinkedIn، لا وعود بنتائج مضمونة. -استخدم: -``` -GET /api/v1/revenue-launch/pipeline/schema -POST /api/v1/revenue-launch/pipeline/summarize -``` +## روابط تقنية -أو افتح Sheet باسم `Dealix First 20 Pipeline` بالعمدة المعرّفة في الـ schema. - ---- - -## 6. تسليم أول Pilot — خلال 24 ساعة - -بعد الدفع: -1. **T+0h** — كيك-أوف + استلام intake. -2. **T+1h** — Diagnosis (targeting + contactability). -3. **T+6h** — Drafting (10 رسائل عربية + safety/tone evals). -4. **T+18h** — Approval Pack (cards مع ≤3 أزرار). -5. **T+24h** — Proof Pack v1 + جدولة جلسة المراجعة. - -استخدم: -``` -GET /api/v1/revenue-launch/pilot-delivery/intake-form -POST /api/v1/revenue-launch/pilot-delivery/24h-plan -``` - ---- - -## 7. الدفع اليدوي عبر Moyasar - -**لا live charge من API.** فقط: -- Moyasar Dashboard → Invoices → Create Invoice. -- 499 ريال = 49,900 halalas. -- وصف: "Dealix Private Beta Pilot — 7 days". -- إرسال للعميل بالإيميل. - -استخدم: -``` -POST /api/v1/revenue-launch/payment/invoice-instructions -POST /api/v1/revenue-launch/payment/link-message -GET /api/v1/revenue-launch/payment/confirmation-checklist -``` - -**قبل بدء التسليم:** تأكد invoice في حالة `paid` على Moyasar dashboard. - ---- - -## 8. Proof Pack — في نهاية الأسبوع - -5 أسطر executive summary + 8 metrics + توصية بالخطوة التالية. - -استخدم: -``` -POST /api/v1/revenue-launch/proof-pack/template -POST /api/v1/revenue-launch/proof-pack/client-summary -POST /api/v1/revenue-launch/proof-pack/next-step -``` - ---- - -## 9. أهداف اليوم - -| Metric | Target | -|--------|-------:| -| Outreach sent | 20 | -| Replies | 5 | -| Demos booked | 3 | -| Pilots paid | 1 | - -أهداف 7 أيام: 100 outreach / 20 ردود / 10 ديمو / 2 pilots مدفوعة. - -استخدم: -``` -GET /api/v1/launch/scorecard/demo -POST /api/v1/launch/scorecard/event -POST /api/v1/launch/scorecard/daily -POST /api/v1/launch/scorecard/weekly -``` - ---- - -## 10. Go / No-Go اليوم - -10 بوابات (`POST /api/v1/launch/go-no-go` أو `python scripts/launch_readiness_check.py`): - -1. Tests passed. -2. Routes check OK. -3. No secrets in repo. -4. Staging /health → 200. -5. Supabase staging configured. -6. Service catalog ≥4 services. -7. landing/private-beta.html ready. -8. First-20 prospects identified. -9. WHATSAPP/GMAIL/CALENDAR/MOYASAR live=false. -10. Moyasar invoice/payment-link manual flow ready. - -**Critical gates** (must pass): `no_secrets`, `live_sends_disabled`, `staging_health`. Otherwise: NO-GO. - ---- - -## 11. ما لا تفعله اليوم - -- لا live WhatsApp/Gmail/Calendar/Moyasar من API. -- لا scraping LinkedIn ولا auto-DM. -- لا cold WhatsApp. -- لا Public Launch / إعلان صحفي. -- لا "نضمن نتائج". - ---- - -## 12. الخطوة بعد أول Pilot - -- Proof Pack → Case Study → ترقية لـ Growth OS Monthly. -- Case Study → استخدمه في الـ outreach التالي. -- متابعة شهرية مع Service Excellence backlog (ما يحسّن الخدمة). +| الغرض | المسار | +|--------|--------| +| حزمة العروض | `GET /api/v1/revenue-launch/offer` | +| ديمو + إغلاق | `GET /api/v1/revenue-launch/demo-flow` | +| مخطط pipeline | `GET /api/v1/revenue-launch/pipeline/schema` | +| تسليم Pilot | `GET /api/v1/revenue-launch/pilot-delivery` | +| دفع يدوي | `GET /api/v1/revenue-launch/payment/manual-flow` | +| Proof template | `GET /api/v1/revenue-launch/proof-pack/template` | diff --git a/dealix/docs/REVENUE_WORK_UNITS.md b/dealix/docs/REVENUE_WORK_UNITS.md new file mode 100644 index 00000000..213883a9 --- /dev/null +++ b/dealix/docs/REVENUE_WORK_UNITS.md @@ -0,0 +1,16 @@ +# Revenue Work Units (RWU) + +وحدة عمل إيراد = أي إنجاز قابل للقياس (فرصة، مسودة، موافقة، اجتماع، Proof، …). + +## التعريف البرمجي + +- [`revenue_work_units.py`](../auto_client_acquisition/revenue_company_os/revenue_work_units.py) +- `GET /api/v1/revenue-os/company-os/work-units/demo` — قائمة تجريبية. + +## الأنواع + +انظر `RWU_TYPES` في الملف: `opportunity_created`, `draft_created`, `approval_collected`, … + +## الربط لاحقاً + +تسجيل فعلي في دفتر أحداث أو جدول — انظر [`PROOF_LEDGER_WEEKLY_RUNBOOK.md`](ops/PROOF_LEDGER_WEEKLY_RUNBOOK.md). diff --git a/dealix/docs/SAFE_TOOL_GATEWAY_POLICY.md b/dealix/docs/SAFE_TOOL_GATEWAY_POLICY.md new file mode 100644 index 00000000..2d896f11 --- /dev/null +++ b/dealix/docs/SAFE_TOOL_GATEWAY_POLICY.md @@ -0,0 +1,29 @@ +# Safe Tool Gateway — سياسة Dealix + +كل إجراء خارجي يمر: **نية → سياسة → موافقة (عند الحاجة) → تنفيذ/تصدير → تدقيق**. + +## أوضاع الأداة (MVP) + +| وضع | معنى | +|-----|--------| +| suggest_only | اقتراح نصي فقط | +| draft_only | مسودة دون إرسال | +| approval_required | لا تنفيذ بدون موافقة بشرية | +| approved_execute | مسموح بعد موافقة صريحة (نادر في البيتا) | +| blocked | ممنوع | + +## المصفوفة البرمجية + +- [`tool_action_planner.py`](../auto_client_acquisition/autonomous_service_operator/tool_action_planner.py) +- `GET /api/v1/operator/tools/matrix` + +## قواعد صارمة (البيتا) + +- لا **Gmail send** تلقائي من المنصة. +- لا **LinkedIn scraping** ولا **auto-DM**. +- لا **واتساب بارد** غير موافق. +- لا **Moyasar charge** من API — روابط/فواتير يدوية حسب [`REVENUE_TODAY_PLAYBOOK.md`](REVENUE_TODAY_PLAYBOOK.md). + +## مراجع + +- [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md)، [`AGENT_SECURITY_CURATOR.md`](AGENT_SECURITY_CURATOR.md). diff --git a/dealix/docs/SELF_IMPROVING_REVENUE_LOOP.md b/dealix/docs/SELF_IMPROVING_REVENUE_LOOP.md new file mode 100644 index 00000000..d090b210 --- /dev/null +++ b/dealix/docs/SELF_IMPROVING_REVENUE_LOOP.md @@ -0,0 +1,17 @@ +# Self-Improving Revenue Loop + +حلقة أسبوعية: مراجعة رسائل، موافقات، ردود، مخاطر، خدمات — ثم توصيات وbacklog. + +## التنفيذ الحالي (demo) + +- [`self_improvement_loop.py`](../auto_client_acquisition/revenue_company_os/self_improvement_loop.py) +- `GET /api/v1/revenue-os/company-os/self-improvement/weekly-report` + +## التشغيل اليدوي الأسبوعي + +- سكربت: [`fetch_proof_ledger_weekly.py`](../scripts/fetch_proof_ledger_weekly.py) +- Runbook: [`ops/PROOF_LEDGER_WEEKLY_RUNBOOK.md`](ops/PROOF_LEDGER_WEEKLY_RUNBOOK.md) + +## لاحقاً + +ربط ببيانات عميل حقيقية و`growth_curator` — دون تغيير العقود العامة للـ API دون إصدار. diff --git a/dealix/docs/SERVICE_BUNDLES.md b/dealix/docs/SERVICE_BUNDLES.md new file mode 100644 index 00000000..0c07a7cd --- /dev/null +++ b/dealix/docs/SERVICE_BUNDLES.md @@ -0,0 +1,21 @@ +# Service Bundles + +باقات منتَجة تربط عدة `service_id` من [Service Tower](SERVICE_TOWER_STRATEGY.md) بسعر وزمن وProof. + +## التعريف البرمجي + +- [`auto_client_acquisition/autonomous_service_operator/service_bundles.py`](../auto_client_acquisition/autonomous_service_operator/service_bundles.py) +- `GET /api/v1/operator/bundles` — قائمة JSON للعرض. + +## الباقات + +| bundle_id | فكرة | نطاق سعر تقريبي (ريال) | +|-----------|--------|-------------------------| +| growth_starter | تشخيص + ١٠ فرص | ٤٩٩ | +| data_to_revenue | ذكاء قوائم + فرص | ١٥٠٠–٢٥٠٠ | +| executive_growth_os | موجز تنفيذي + Growth OS | من ٢٩٩٩ | +| partnership_growth | شراكات + اجتماعات | ٣٠٠٠–٧٥٠٠ | +| local_growth_os | نمو محلي | ٩٩٩–٢٩٩٩ | +| full_growth_control_tower | مخصص ٩٠ يوماً | ١٥٠٠٠+ | + +الأرقام توضيحية للعرض؛ العقد يُحدّد بعد الـ pilot. diff --git a/dealix/docs/SERVICE_EXCELLENCE_OS.md b/dealix/docs/SERVICE_EXCELLENCE_OS.md index 5032d51e..d981fd8d 100644 --- a/dealix/docs/SERVICE_EXCELLENCE_OS.md +++ b/dealix/docs/SERVICE_EXCELLENCE_OS.md @@ -1,185 +1,44 @@ -# Service Excellence OS — مصنع الخدمات الممتازة +# Service Excellence OS — مصنع جودة الخدمات -> **القاعدة:** لا خدمة تطلق إنتاجياً إلا إذا حصلت على score ≥80 وتجاوزت 4 quality gates. ولا تتوقف عند الإطلاق — تستمر في التحسين الأسبوعي. +## الرؤية ---- +طبقة فوق **Service Tower** تضمن أن كل خدمة لها: مصفوفة ميزات، درجة جاهزية، مسار موافقة، مقاييس Proof، حزمة إطلاق، وفجوات مقابل الفئات التنافسية — **كلها deterministic** في الـ MVP (بدون استدعاء ويب). -## 1. الوحدات +## المكوّنات (`auto_client_acquisition/service_excellence/`) -| الوحدة | الدور | -|--------|------| -| `feature_matrix` | 12 must-have feature لكل خدمة + advanced/premium/future. | -| `service_scoring` | 10 أبعاد × 10 نقاط = 100. status: launch_ready / beta_only / needs_work. | -| `quality_review` | 4 gates: proof / approval / pricing / channels. | -| `competitor_gap` | مقارنة structural بـ7 فئات منافسين. | -| `proof_metrics` | الـ metrics المطلوبة + ROI estimate. | -| `research_lab` | brief شهري + hypotheses + experiments. | -| `service_improvement_backlog` | feedback → backlog → prioritization. | -| `launch_package` | landing + sales + demo + onboarding. | +| الملف | الدور | +|-------|--------| +| `feature_matrix.py` | must_have / advanced / premium | +| `service_scoring.py` | درجة 0–100 وحالة `launch_ready` / `beta_only` / `needs_work` | +| `workflow_builder.py` | التحقق من وجود خطوة موافقة | +| `research_lab.py` | فرضيات وتجارب مقترحة (نص ثابت) | +| `competitor_gap.py` | نقاط قوة المنافس + مزايا Dealix + `do_not_copy` | +| `proof_metrics.py` | مقاييس إلزامية لكل خدمة | +| `quality_review.py` | فحص قبل الإطلاق + `review_all_services` | +| `service_improvement_backlog.py` | عناصر تحسين أسبوعية | +| `launch_package.py` | مخطط صفحة + سكربت مبيعات + onboarding | ---- +## مسارات API -## 2. الـ 12 Must-Have Features (لكل خدمة) +بادئة: `/api/v1/service-excellence` -1. Self-Serve Intake. -2. AI Recommendation. -3. Data Quality Check. -4. Contactability / Risk Gate. -5. Channel Strategy. -6. Arabic Contextual Drafting. -7. Approval Cards. -8. Execution Mode (draft/export/approved). -9. Proof Pack. -10. Learning Loop. -11. Upsell Path. -12. Service Score. +- `GET /review/all` — فحص جميع خدمات البرج. +- `GET /{service_id}/feature-matrix` — المصفوفة + التصنيف. +- `GET /{service_id}/score` — الدرجة والحالة. +- `GET /{service_id}/workflow` — مسار + تحقق + خطة أيام + موافقات. +- `GET /{service_id}/proof-metrics` — المقاييس والقالب. +- `GET /{service_id}/gap-analysis` — فجوات تنافسية. +- `GET /{service_id}/launch-package` — حزمة إطلاق كاملة. +- `GET /{service_id}/backlog` — تحسينات مقترحة. +- `GET /{service_id}/research-brief` — موجز بحث داخلي. +- `GET /{service_id}/review` — فحص خدمة واحدة. ---- +## منع الخدمات الضعيفة -## 3. الـ 10 أبعاد للـ Score +- درجة أقل من ٨٠ → `beta_only` (تشجيع تحسين قبل التسويق الواسع). +- خدمة عالية المخاطر + درجة غير كافية → `needs_work`. +- سياسات غير آمنة (`auto_send`) تُكتشف عبر `block_if_unsafe_channel` (لا تظهر في كتالوج البرج الحالي). -| البُعد | الوزن | -|------|----:| -| Clarity (وضوح الألم) | 10 | -| Speed-to-Value | 10 | -| Automation | 10 | -| Compliance | 10 | -| Proof | 10 | -| Upsell | 10 | -| Uniqueness (Saudi-first) | 10 | -| Scalability (multi-sector) | 10 | -| Ops Daily (autopilot) | 10 | -| Proof Data | 10 | +## التكامل -**Status:** -- ≥80: `launch_ready` -- ≥60: `beta_only` -- <60: `needs_work` - ---- - -## 4. الـ 4 Quality Gates - -قبل إطلاق أي خدمة: - -1. **Proof gate** — لا proof_metrics → blocked. -2. **Approval gate** — لا approval_policy → blocked. -3. **Pricing gate** — تسعير غير منطقي → blocked. -4. **Channels gate** — تكامل غير آمن (scraping/auto_dm/etc.) → blocked. - -`review_service_before_launch(service_id)` يُرجع verdict واحد من: -- `launch_ready` -- `beta_only` -- `needs_work` -- `blocked_at_gate` - ---- - -## 5. Competitor Gap (7 فئات) - -| Category | Strengths | Limits | -|----------|-----------|--------| -| CRM عام | تخزين بيانات | ينتظر إدخال يدوي | -| WhatsApp tools | Broadcast | لا approval-first | -| Email assistants | كتابة أسرع | لا تحول الإيميل لـ pipeline | -| LinkedIn tools | إيجاد leads | كثيرها يخالف ToS | -| وكالات | خبرة بشرية | لا تتوسع | -| Revenue intelligence | تحليل calls | تبدأ بعد المكالمة | -| Generic AI agent | مرن | بدون سياق شركة | - -**ميزات Dealix:** -- موجّه للسوق السعودي. -- Approval-first. -- Proof Pack شهري. -- Multi-channel orchestration. -- Self-improving Curator. -- PDPL-aware. - ---- - -## 6. Research Lab (شهرياً) - -لكل خدمة: -- 6 أسئلة بحث (من اشترى، TTV، اعتراضات، deliverables، metrics، pricing). -- 4-5 hypotheses للتحسين. -- 3 experiments الأولوية (impact/effort). -- Monthly review بـ score حالي + gap + experiments. - ---- - -## 7. Improvement Backlog - -- `convert_feedback_to_backlog` — Feedback → backlog item. -- `prioritize_backlog_items` — impact desc, effort asc. -- `recommend_weekly_improvements` — 3 weekly tasks. - ---- - -## 8. Launch Package (per service) - -1. **Landing outline** (RTL Arabic): hero, promise, 3-step how-it-works, deliverables, pricing, proof, safety, FAQ, CTA. -2. **Sales script**: 5 discovery questions + pitch + 4 objection handlers + close. -3. **Demo script**: 12-min minute-by-minute Arabic walkthrough. -4. **Onboarding checklist**: first-5-days plan. - ---- - -## 9. Endpoints (`/api/v1/service-excellence/...`) - -``` -GET /{id}/feature-matrix -GET /{id}/feature-classification -GET /{id}/missing-features -GET /{id}/score -GET /{id}/quality-review -GET /review/all -GET /{id}/proof-metrics -POST /{id}/roi-estimate -GET /{id}/gap-analysis -GET /{id}/research-brief -GET /{id}/feature-hypotheses -GET /{id}/experiments -GET /{id}/monthly-review -GET /{id}/backlog -POST /{id}/backlog/from-feedback -POST /{id}/backlog/prioritize -GET /{id}/weekly-improvements -GET /{id}/launch-package -GET /{id}/landing-outline -GET /{id}/sales-script -GET /{id}/demo-script -GET /{id}/onboarding-checklist -``` - ---- - -## 10. اختبارات - -`tests/unit/test_service_excellence.py` — 33 اختبار: -- Feature matrix ≥10 must-haves. -- Score returns valid status. -- Every catalogued service passes the 4 gates. -- ROI estimate returns x-multiples. -- Competitor gap lists advantages + do-not-copy. -- Research brief has ≥5 questions. -- Hypotheses ≥3 + experiments ≤3. -- Backlog conversion + prioritization. -- Launch package complete. -- Demo script = 12 minutes. - ---- - -## 11. Weekly Improvement Loop - -``` -كل اثنين: -1. شغّل /review/all على كل الـ 12 خدمة. -2. أي خدمة < 80 → افتح backlog item. -3. أي خدمة blocked → إصلاح فوري قبل إطلاق جديد. -4. اختر experiment واحد لكل خدمة. - -كل جمعة: -1. سجل النتائج في Service Scorecard. -2. حدّث الـ improvement backlog. -3. أرسل executive brief للمؤسس. -``` +يُستهلك مع **Service Tower** و**Targeting OS** و**Platform Services**؛ لا يكرر التنفيذ الخارجي — يقيّم ويُصدِر خططاً ووثائق عرض. diff --git a/dealix/docs/SERVICE_TOWER_STRATEGY.md b/dealix/docs/SERVICE_TOWER_STRATEGY.md index ad5003ac..b1a27f8b 100644 --- a/dealix/docs/SERVICE_TOWER_STRATEGY.md +++ b/dealix/docs/SERVICE_TOWER_STRATEGY.md @@ -1,136 +1,39 @@ -# Service Tower Strategy — برج الخدمات الذاتي +# Service Tower — استراتيجية الخدمات القابلة للبيع -> **الفكرة:** كل قدرة في Dealix تتحول إلى **Productized Service** بمواصفات: target customer + outcome + inputs + workflow + deliverables + pricing + risk + proof + upgrade path. +## الرؤية ---- +تحويل قدرات Dealix (Targeting، المنصة، الذكاء، Growth Operator) إلى **خدمات منتَجة** لها: مدخلات، مسار عمل، مخرجات، تسعير، سياسة موافقة، Proof Pack، ومسار ترقية — دون إرسال حي أو شحن تلقائي في مسارات الـ MVP. -## 1. القاعدة الذهبية +## العلاقة مع الكتالوجات الأخرى -**العميل لا يشتري ميزة. يشتري نتيجة منظمة.** +- **`platform_services.service_catalog`**: مكونات تقنية واشتراكات/خدمات طبقة المنصة. +- **`service_tower.service_catalog`**: تعريف **بيعي تشغيلي** (برج الخدمات) مع `pricing_range_sar` و`workflow_steps` و`upgrade_path`. +- **`targeting_os`**: استهداف آمن، قوائم، LinkedIn Lead Gen — يغذي توصيات الـ wizard. -كل خدمة تمشي في نفس الـ pipeline: -``` -Goal → Intake → Data Check → Risk Check → Strategy → -Drafts → Approval → Execution/Export → Tracking → Proof → Upsell -``` +## مسارات API ---- +| المسار | الوظيفة | +|--------|---------| +| `GET /api/v1/services/catalog` | برج الخدمات + لقطة من كتالوج المنصة | +| `POST /api/v1/services/recommend` | توصية خدمة من نوع الشركة والهدف | +| `POST /api/v1/services/start` | بدء تشغيل منطقي (تحقق مدخلات فقط) | +| `GET /api/v1/services/{id}/workflow` | خطوات المسار | +| `POST /api/v1/services/{id}/quote` | تقدير SAR (غير ملزم) | +| `GET /api/v1/services/demo/dashboard` | بطاقات عرض داخلية | +| `GET /api/v1/services/ceo/daily-brief` | موجز عربي + أزرار ≤٣ | +| `POST /api/v1/services/approval-card` | بطاقة موافقة لخدمة/إجراء | -## 2. الـ12 خدمة (Productized) +التفاصيل في [`docs/architecture/API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md). -| # | الخدمة | المدخلات | المخرجات | السعر | -|---|--------|----------|---------|-------| -| 1 | Free Growth Diagnostic | sector/city/offer/goal | 3 فرص + رسالة + مخاطر + خطة Pilot | 0 | -| 2 | List Intelligence | CSV + channels | تنظيف + أفضل 50 + رسائل | 499–1,500 | -| 3 | First 10 Opportunities Sprint | sector/city/offer/goal | 10 فرص + رسائل + Proof Pack | 499–1,500 | -| 4 | Self-Growth Operator | company profile + goals | Daily brief + drafts + reports | 999/شهر | -| 5 | Growth OS Monthly | channels + team_size | المنصة الكاملة شهرياً | 2,999/شهر | -| 6 | Email Revenue Rescue | gmail label + ICP | استخراج فرص ضائعة + drafts | 1,500–5,000 | -| 7 | Meeting Booking Sprint | prospects + calendar | invitations + briefs + follow-ups | 1,500–5,000 | -| 8 | Partner Sprint | sector + partner goal | 20 شريك + رسائل + 5 اجتماعات | 3,000–7,500 | -| 9 | Agency Partner Program | agency profile | بيع Dealix لعملاء الوكالة | 10,000–50,000 | -| 10 | WhatsApp Compliance Setup | contact list + practice | audit + opt-in templates + ledger | 1,500–4,000 | -| 11 | LinkedIn Lead Gen Setup | ICP + offer + ad budget | حملة Lead Form + ربط CRM | 2,000–7,500 | -| 12 | Executive Growth Brief | company profile | موجز يومي 3+3+3 | 499–999/شهر | +## القواعد ---- +- لا إرسال حي من هذه المسارات. +- واتساب بارد ممنوع افتراضياً (انظر `whatsapp_compliance_setup` و`targeting_os`). +- لا ضمان نتائج — التسعير والأثر **تقديرات عرض**. -## 3. الـ Wizard +## ترتيب الإطلاق التجاري المقترح -``` -العميل يجيب: -- نوع الشركة -- الهدف -- هل عندك قائمة؟ -- ما القنوات المتاحة؟ -- الميزانية - -النظام يوصي بخدمة واحدة + يبرر القرار. -``` - -ترتيب القرارات: -1. وكالة → Partner Sprint / Agency Program. -2. عنده قائمة → List Intelligence. -3. مؤسس → Self-Growth Operator. -4. CEO → Executive Growth Brief. -5. واتساب → Compliance Setup. -6. هدف rescue → Email Revenue Rescue. -7. هدف اجتماعات → Meeting Booking Sprint. -8. هدف شراكات → Partner Sprint. -9. ميزانية شهرية ≥ 2999 → Growth OS. -10. الافتراضي → First 10 Opportunities. - ---- - -## 4. WhatsApp CEO Control - -كل قرار يصل المؤسس عبر واتساب كـ كرت: -- Daily Service Brief (≤3 buttons). -- Service Approval Card (`اعتمد / عدّل / ارفض`). -- Risk Alert Card. -- End-of-Day Report. - ---- - -## 5. Pricing Engine - -ضرّابات السعر: -- `company_size`: micro 0.8x, small 1.0x, medium 1.3x, large 1.7x. -- `urgency`: normal 1.0x, rush 1.3x, asap 1.5x. -- `channels_count`: +15% لكل قناة إضافية. - -Setup fee = month-equivalent للـ monthly services. السنوي بخصم 15%. - ---- - -## 6. Upgrade Paths - -``` -Free Diagnostic → First 10 Opportunities → Growth OS Monthly → Agency Partner -List Intelligence → Growth OS Monthly -Self-Growth Operator → Growth OS Monthly -Email Revenue Rescue → Growth OS Monthly -Partner Sprint → Agency Partner Program -``` - -كل upgrade path له upsell message عربي جاهز. - ---- - -## 7. Endpoints (`/api/v1/services/...`) - -``` -GET /catalog -GET /summary -POST /recommend -GET /{id}/intake-questions -POST /{id}/start -GET /{id}/workflow -GET /{id}/deliverables -GET /{id}/proof-pack-template -GET /{id}/client-report-outline -GET /{id}/operator-checklist -POST /{id}/quote -GET /{id}/setup-fee -GET /{id}/monthly-offer -POST /{id}/scorecard -GET /{id}/upgrade-path -GET /{id}/post-service-plan -GET /ceo/daily-brief -POST /ceo/approval-card -GET /ceo/risk-alert/demo -GET /ceo/end-of-day/demo -``` - ---- - -## 8. اختبارات - -`tests/unit/test_service_tower.py` — 38 اختبار: -- Catalog ≥12 خدمة + critical services. -- Pricing + proof metrics + deliverables موجودة. -- Wizard recommendations (agency, list, founder, CEO, budget). -- Workflow includes approval. -- Quote scaling by size. -- CEO cards ≤3 buttons + لا live send. -- Upgrade paths. +1. `free_growth_diagnostic` → جذب. +2. `list_intelligence` و`first_10_opportunities` → إثبات سريع. +3. `email_revenue_rescue` و`meeting_booking_sprint` → قيمة عالية بمسودات. +4. `growth_os` → اشتراك بعد Pilot. diff --git a/dealix/docs/STAGING_DEPLOYMENT.md b/dealix/docs/STAGING_DEPLOYMENT.md index e4e41f7f..8a31d685 100644 --- a/dealix/docs/STAGING_DEPLOYMENT.md +++ b/dealix/docs/STAGING_DEPLOYMENT.md @@ -28,6 +28,39 @@ uvicorn api.main:app --host 0.0.0.0 --port ${PORT:-8000} - مفاتيح LLM إن لزم للتجارب - **لا** تضع `MOYASAR_SECRET` أو أسرار في المتغيرات العامة للواجهة +### Growth Control Tower / قنوات (موصى به على staging) + +- `WHATSAPP_ALLOW_LIVE_SEND=false` — يبقى معطّلاً حتى اكتمال webhook وopt-in والمراجعة القانونية (انظر [`core/config/settings.py`](../core/config/settings.py)). +- `MOYASAR_MODE=sandbox` (أو اسم متغير معادل عندك) — **تسمية تشغيلية** للفريق؛ مسارات API الحالية تبقى مسودات/تحقق فقط بدون charge حي ما لم تُضف تكامل إنتاجي صراحةً. +- `SENTRY_DSN` — للتقاط أخطاء staging (اختبار عبر `GET /api/v1/admin/sentry-check` حيث يُسمح). +- مفاتيح **Langfuse** إن استخدمت المراقبة: لا تُسجَّل في الريبو؛ ضعها كأسرار Railway فقط. + +### بوابة جاهزية الإطلاق (GO / NO-GO) + +**CI على الاستضافة:** من GitHub Actions شغّل workflow **Dealix staging smoke** (يدوي) بعد ضبط السر `STAGING_BASE_URL` — يشغّل `smoke_staging.py` ثم `launch_readiness_check.py --base-url` ويتوقع **`PAID_BETA_READY`** عند نجاح كل الفحوص. تفاصيل الفروع: [`BRANCH_PROTECTION_AND_CI.md`](BRANCH_PROTECTION_AND_CI.md). + +بعد ضبط `STAGING_BASE_URL` (أو تمرير `--base-url`): + +```bash +python scripts/launch_readiness_check.py --base-url "https://YOUR-STAGING-URL" +``` + +توقّع **`VERDICT: PAID_BETA_READY`** وexit code `0` عندما تمر كل فحوصات الشبكة نفسها التي يمرّرها السكربت محلياً (`customer-ops`، `services/catalog`، `launch/private-beta/offer`، `security-curator/demo`، إلخ). للتحقق المحلي فقط بدون URL: `python scripts/launch_readiness_check.py` → **`GO_PRIVATE_BETA`**. + +### Smoke — مسارات إضافية للتحقق يدوياً + +بعد `GET /health` و`smoke_staging.py`، يُنصح بالتحقق من وجود: + +- `GET /api/v1/platform/service-catalog` +- `GET /api/v1/platform/inbox/feed` +- `GET /api/v1/platform/proof/overview` +- `GET /api/v1/intelligence/command-feed` (أو `/command-feed/demo`) +- `POST /api/v1/innovation/opportunities/ten-in-ten` أو alias `POST /api/v1/intelligence/missions/first-10-opportunities` +- `GET /api/v1/services/catalog`، `GET /api/v1/services/verticals`، `GET /api/v1/launch/go-no-go`، `GET /api/v1/launch/scorecard` +- `GET /api/v1/revenue-launch/offer`، `GET /api/v1/revenue-launch/payment/manual-flow` (عروض Pilot ودفع يدوي فقط — انظر [`REVENUE_TODAY_PLAYBOOK.md`](REVENUE_TODAY_PLAYBOOK.md)) + +خريطة كاملة للمرادفات: [`docs/architecture/API_CANONICAL_ALIASES.md`](architecture/API_CANONICAL_ALIASES.md). + ## Smoke بعد النشر من جهازك (مسارات GET الحرجة، بدون أسرار في الريبو): diff --git a/dealix/docs/SUPPORT_SLA.md b/dealix/docs/SUPPORT_SLA.md index 3c9db508..c04d664b 100644 --- a/dealix/docs/SUPPORT_SLA.md +++ b/dealix/docs/SUPPORT_SLA.md @@ -1,95 +1,14 @@ -# Dealix Support SLA +# دعم Pilot — SLA مبدئي -> **القاعدة:** كل tickets تُصنّف P0/P1/P2/P3 آلياً، لها أهداف first-response و resolution محددة، ويتم تتبع كل تجاوز. +| الأولوية | الوصف | أول رد مستهدف | حل مستهدف | +|---------|--------|---------------|------------| +| P0 | أمان / إرسال خاطئ / توقف كامل | ٢ ساعات | ٨ ساعات | +| P1 | تعطل خدمة أساسية | ٤ ساعات | ٢٤ ساعة | +| P2 | تكامل أو Proof متأخر | ٢٤ ساعة | ٧٢ ساعة | +| P3 | سؤال أو تحسين | ٤٨ ساعة | ١٢٠ ساعة | ---- +**ملاحظة:** الأرقام أهداف تشغيلية للبيتا؛ تُرفع في العقد عند التوسع. -## Priority Tiers +**مرجع API:** `GET /api/v1/customer-ops/support/sla` -| Priority | الوصف | First Response | Resolution Target | Owner | -|----------|------|---------------:|------------------:|-------| -| **P0** | حرج جداً — أمان / إرسال خاطئ / تعطل كامل | 30 دقيقة | 4 ساعات | Founder | -| **P1** | خدمة مهمة معطلة | 2 ساعة | 24 ساعة | Operator on-call | -| **P2** | Connector أو Proof Pack متأخر | 8 ساعات | 72 ساعة | Operator on-call | -| **P3** | سؤال عام / تحسين | 24 ساعة | 7 أيام | Operator team | - ---- - -## Endpoints - -``` -POST /api/v1/customer-ops/support/classify # تصنيف ticket → priority -POST /api/v1/customer-ops/support/route # routing مع SLA + first response template -POST /api/v1/customer-ops/sla/event # تسجيل opened/first_response/resolved/escalated -POST /api/v1/customer-ops/sla/classify-breach # تحديد إن كان في breach -POST /api/v1/customer-ops/sla/health-report # تقرير صحة SLA من tickets list -GET /api/v1/customer-ops/sla/health-report/demo # demo -``` - ---- - -## Auto-classification Keywords - -### P0 (حرج جداً) -- أمان -- تسريب -- إرسال خاطئ -- إرسال بدون موافقة / بدون موافقتي -- secret / leak / data breach -- outage / completely down -- live charge / charge بدون موافقة -- unauthorized - -### P1 (خدمة معطلة) -- service down / خدمة معطلة -- service failed -- Pilot stopped -- Proof Pack مفقود - -### P2 (connector أو proof) -- connector / Gmail / Calendar / Sheets -- WhatsApp setup -- Moyasar invoice - -### P3 (افتراضي) -أي ticket لم يتطابق مع P0/P1/P2. - ---- - -## First-Response Templates - -كل priority لها قالب رد أولي عربي معد مسبقاً عبر `build_first_response_template(priority)`. - -### مثال P0 -> وصلني بلاغك الآن. نتعامل معه كأولوية حرجة. سأرد عليك خلال 30 دقيقة بتفاصيل ما حدث + الإجراءات المتخذة. إذا اكتشفت أي إرسال غير معتمد أو تسريب بيانات، سأتواصل معك مباشرة. - ---- - -## Health Report Verdict - -عبر `build_sla_health_report`: -- **healthy**: breach_rate < 10% -- **watch**: 10% ≤ breach_rate < 25% -- **critical**: breach_rate ≥ 25% - -عند `critical` → escalate تلقائي للمؤسس + إيقاف الـ live actions حتى المراجعة. - ---- - -## Weekly SLA Review - -كل اثنين: -1. تجميع كل tickets الأسبوع المنقضي. -2. تشغيل `build_sla_health_report`. -3. مراجعة الـ breaches. -4. تحديث `customer_success_cadence` للعملاء المتأثرين. -5. إذا critical → post-mortem + `incident_router`. - ---- - -## ما لا يحدث في الـ support - -- لا response تلقائي للعميل بدون مراجعة بشرية. -- لا تسريب لـ ticket id في القنوات العامة. -- لا فتح ticket بـ priority < classified-priority (الـ system يحدد، البشر يرفع فقط). -- لا إغلاق ticket بدون تأكيد من العميل. +**آخر تحديث:** 2026-05-01 diff --git a/dealix/docs/TARGETING_ACQUISITION_OS.md b/dealix/docs/TARGETING_ACQUISITION_OS.md index 81b1fbe7..8e829788 100644 --- a/dealix/docs/TARGETING_ACQUISITION_OS.md +++ b/dealix/docs/TARGETING_ACQUISITION_OS.md @@ -1,184 +1,49 @@ -# Targeting & Acquisition OS — نظام الاستهداف الذكي +# Targeting & Acquisition OS — Dealix -> **القاعدة:** Dealix لا يجمع كل شيء من كل مكان. يستهدف بذكاء، عبر مصادر مصرّح بها، مع موافقات بشرية، ومراقبة سمعة، وتعلّم يومي. +## الرؤية ---- +طبقة تستهدف **الحسابات (Accounts)** أولاً ثم **لجنة الشراء** ثم **قابلية التواصل**، وتخرج خطط outreach ومسودات فقط — بدون scraping وبدون إرسال حي بدون موافقة. تكمل [`PLATFORM_SERVICES_STRATEGY.md`](PLATFORM_SERVICES_STRATEGY.md) و[`INTELLIGENCE_LAYER_STRATEGY.md`](INTELLIGENCE_LAYER_STRATEGY.md). -## 1. لماذا Targeting OS؟ +## الكود -أي أداة تستطيع جمع أرقام بالـ scraping. القوة الحقيقية: -- **Account-first**: ابحث عن الشركات قبل الأشخاص. -- **Buying Committee**: من غالباً يقرر داخل كل شركة؟ -- **Contactability Gate**: هل التواصل مسموح؟ -- **Channel Strategy**: ما القناة الأفضل لكل مصدر؟ -- **Reputation Guard**: إذا تدهورت السمعة → أوقف القناة تلقائياً. -- **Daily Autopilot**: brief يومي + actions + Proof. -- **Self-Growth Mode**: Dealix يستهدف عملاءه بنفس النظام. +| مسار | ملف | +|------|-----| +| `auto_client_acquisition/targeting_os/` | سياسات مصدر، contactability، حسابات تجريبية، LinkedIn المتوافق، جدولة، سمعة، تقارير | ---- +## المسارات (`/api/v1/targeting/*`) -## 2. الوحدات (16 module) +- `POST /accounts/recommend` — شركات تجريبية مرتبة بالقطاع/المدينة +- `POST /buying-committee/map` — أدوار قرار مقترحة +- `POST /contacts/evaluate` — `safe` / `needs_review` / `blocked` +- `POST /uploaded-list/analyze` — يمر عبر [`contact_import_preview`](../auto_client_acquisition/platform_services/contact_import_preview.py) +- `POST /outreach/plan` — خطوات بحدود يومية (MVP) +- `GET /daily-autopilot/demo` — بطاقات يومية (≤3 أزرار في العرض) +- `GET /self-growth/demo` — أهداف تشغيل ذاتي لـ Dealix (مسودات فقط) +- `GET /reputation/status` — مثال مقاييس + `should_pause` +- `POST /linkedin/strategy` — Lead Gen أولاً + `do_not_do` +- `GET /services` — عروض خدمات قابلة للبيع +- `POST /free-diagnostic` — 3 فرص + عرض pilot +- `GET /contracts/templates` — مخططات عقود **ليست استشارة قانونية** +- `POST /trust-score` — جسر إلى `compute_trust_score` +- `POST /account-strategy` — استراتيجية مصدر لكل حساب -| الوحدة | الدور | -|--------|------| -| `account_finder` | يحدد 10-25 شركة مناسبة لكل (sector, city). | -| `buyer_role_mapper` | 14 دور + خرائط buying committee حسب القطاع. | -| `contact_source_policy` | 12 مصدر، كل واحد له risk_score + channels مسموحة + retention. | -| `contactability_matrix` | 5 action modes: suggest_only / draft_only / approval_required / approved_execute / blocked. | -| `linkedin_strategy` | Lead Forms + Ads + Manual فقط. **لا scraping/auto-DM/auto-connect**. | -| `email_strategy` | Drafts + unsubscribe + pacing حسب domain reputation. | -| `whatsapp_strategy` | Opt-in only؛ rejects cold + risky phrases. | -| `social_strategy` | Listening + drafts فقط؛ لا auto-publish. | -| `outreach_scheduler` | Day-by-day plan + daily limits + opt-out enforcement. | -| `reputation_guard` | Bounce/complaint/opt-out thresholds → healthy/watch/pause. | -| `daily_autopilot` | Daily brief + 7 today actions + EOD report. | -| `acquisition_scorecard` | Pipeline / meetings / risks / productivity score. | -| `self_growth_mode` | Dealix ICP focus + daily brief + weekly learning. | -| `free_diagnostic` | Free 5-section Arabic diagnostic → paid pilot offer. | -| `contract_drafts` | Pilot/DPA/Referral/Agency/SOW outlines (legal review required). | -| `service_offers` | 7 targeting-tier offers + pricing + recommend. | +## LinkedIn — المسموح والممنوع ---- +- **ممنوع:** scraping، auto-DM، auto-connect، أتمتة غير أصيلة (انظر سياسات LinkedIn العامة). +- **مسموح:** Lead Gen Forms، إعلانات، مهام بحث يدوية معتمدة، بيانات العميل وCRM. -## 3. القنوات والقواعد +## WhatsApp -### LinkedIn -**الممنوع** (encoded in `linkedin_do_not_do()`): -- `scrape_profiles, auto_connect, auto_dm, browser_automation, fake_engagement, download_contacts_from_linkedin, buy_scraped_leads, use_unauthorized_extensions`. +- لا cold outbound افتراضياً؛ opt-in واضح؛ راجع [`PRIVATE_BETA_RUNBOOK.md`](PRIVATE_BETA_RUNBOOK.md). -**المسموح**: -- LinkedIn Lead Gen Forms (أساسي). -- LinkedIn Ads. -- البحث اليدوي المعتمد (manual research task). -- Connection requests يدوية بمسودات Dealix. +## PDPL والمصادر -### WhatsApp -- لا cold بدون opt-in واضح. -- opt-in template يحتاج: اسم النشاط + الغرض + خيار الانسحاب. -- double opt-in موصى به. +كل جهة تحتاج `source` وغرض معالجة؛ القوائم المشتراة أو المكشوفة **محظورة** في المصفوفة. التفاصيل التشغيلية في [`DATA_MAP.md`](DATA_MAP.md) و[`PRIVACY_PDPL_READINESS.md`](PRIVACY_PDPL_READINESS.md). -### Email -- سياق واضح + unsubscribe. -- Pacing حسب `domain_reputation`: fresh/warmed/trusted/damaged. -- إيقاف على bounce ≥ 5%. +## الخدمات القابلة للبيع -### Social -- API رسمية فقط. -- Listening مسموح. -- Replies = drafts بموافقة. +انظر `GET /targeting/services` و[`service_catalog`](../auto_client_acquisition/platform_services/service_catalog.py) للمحاذاة مع كتالوج المنصة. ---- +## الاختبارات -## 4. مصادر الـ Contacts (12) - -| Source | Risk | Status الافتراضي | -|--------|------|-----------------| -| `crm_customer` | 5 | safe | -| `inbound_lead` | 5 | safe | -| `website_form` | 10 | safe | -| `linkedin_lead_form` | 10 | safe | -| `event_lead` | 20 | needs_review | -| `referral` | 25 | needs_review | -| `partner_intro` | 25 | needs_review | -| `manual_research` | 50 | needs_review | -| `uploaded_list` | 60 | needs_review | -| `unknown_source` | 80 | needs_review | -| `cold_list` | 95 | blocked (waتساب)/needs_review (إيميل) | -| `opt_out` | 100 | blocked (كل القنوات) | - ---- - -## 5. Daily Operating Loop - -``` -صباحاً: -- 10 شركات جديدة مناسبة -- 5 رسائل drafts للموافقة -- 3 leads متأخرة (>72h) -- 1 فرصة شريك -- 1 خطر سمعة - -ظهراً: -- اعتماد + إرسال 5 emails -- مراجعة 12 رقم بدون مصدر -- ديمو شريك - -مساءً: -- 32 حساب تم تحليله -- 6 مسودات معتمدة -- 2 ردود إيجابية -- 1 اجتماع مجدول -- 8 مخاطر منعت -``` - ---- - -## 6. Self-Growth Mode - -5 ICP focuses لـ Dealix نفسه: -1. وكالات تسويق B2B في الرياض. -2. شركات تدريب B2B في الرياض. -3. شركات استشارات نمو. -4. SaaS سعودية صغيرة-متوسطة. -5. وسطاء عقار B2B في جدة. - -كل صباح: 10 شركات + 5 رسائل + اعتماد المؤسس. - -أهداف شهرية: 30 Free Diagnostic، 6 Paid Pilots، 3 Growth OS، 1 وكالة شريكة. - ---- - -## 7. Endpoints (`/api/v1/targeting/...`) - -``` -POST /accounts/recommend -POST /buying-committee/map -POST /contacts/evaluate -POST /uploaded-list/analyze -POST /outreach/plan -GET /daily-autopilot/demo -GET /self-growth/demo -POST /self-growth/targets -POST /self-growth/weekly-report -GET /reputation/status -POST /reputation/recovery -POST /linkedin/strategy -POST /drafts/email -POST /drafts/whatsapp -POST /drafts/email-followup -POST /drafts/role-angle -POST /free-diagnostic -GET /services -POST /services/recommend -GET /contracts/templates -``` - ---- - -## 8. اختبارات - -`tests/unit/test_targeting_os.py` — 47 اختبار: -- Account finder + Arabic + safe sources. -- Buying committee + role-based angles. -- Source classification + 12 sources. -- Contactability (opt-out, cold WA, inbound safe, unknown review). -- LinkedIn (لا scraping/auto-DM). -- Email risk + unsubscribe + 3-step follow-up. -- WhatsApp risk + opt-in templates. -- Outreach plan + daily limits. -- Reputation guard + recovery. -- Self-growth + free diagnostic + uploaded list preview. -- Contracts (legal review + PDPL). -- Acquisition scorecard. - ---- - -## 9. ما لا تفعله - -- لا scraping LinkedIn/social. -- لا auto-DM في أي منصة. -- لا cold WhatsApp. -- لا charge بدون تأكيد. -- لا scraping ToS-مخالف. -- لا وعود بنتائج مضمونة. -- لا تخزين بطاقات. +[`tests/test_targeting_os.py`](../tests/test_targeting_os.py) diff --git a/dealix/docs/architecture/API_CANONICAL_ALIASES.md b/dealix/docs/architecture/API_CANONICAL_ALIASES.md new file mode 100644 index 00000000..eb7f30dc --- /dev/null +++ b/dealix/docs/architecture/API_CANONICAL_ALIASES.md @@ -0,0 +1,73 @@ +# Growth Control Tower — مسارات canonical و aliases + +> مرجع مواءمة بين وثائق المنتج ومسارات الريبو الفعلية. **لا تُولَّد تلقائياً** — حدّثها عند إضافة alias جديد. + +## مبدأ + +- **Canonical**: المسار الذي يُنصح به في الكود الجديد والاختبارات. +- **Alias**: نفس السلوك أو لفّ رفيع حول منطق موجود (مثل `ten-in-ten` في `innovation`). + +| المفهوم في الوثائق | Canonical في الريبو | ملاحظة | +|--------------------|---------------------|---------| +| Growth Brain build | `POST /api/v1/intelligence/growth-profile` | نفس الـ handler؛ اسم «growth-brain/build» في الوثائق القديمة يُعامل كمرادف لفظي. | +| Command Feed (intel) | `GET /api/v1/intelligence/command-feed` | `GET .../command-feed/demo` **alias** — نفس الاستجابة. | +| Command Feed (innovation demo) | `GET /api/v1/innovation/command-feed/demo` | مصدر بطاقات الابتكار؛ شكل الحقول قد يختلف قليلاً عن طبقة الذكاء. | +| 10 فرص في 10 دقائق | `POST /api/v1/innovation/opportunities/ten-in-ten` | المنطق: `build_ten_opportunities`. | +| First 10 opportunities (alias منتجي) | `POST /api/v1/intelligence/missions/first-10-opportunities` | يستدعي نفس `build_ten_opportunities` دون تكرار المنطق. | +| Unified inbox من حدث | `POST /api/v1/platform/inbox/from-event` | | +| Inbox feed (عرض تجريبي) | `GET /api/v1/platform/inbox/feed` | قائمة بطاقات deterministic للعرض — لا إرسال. | +| Proof ledger demo (innovation) | `GET /api/v1/innovation/proof-ledger/demo` | أحداث تجريبية. | +| Proof pack (أعمال) | `GET /api/v1/business/proof-pack/demo` | أقسام ROI تجريبية. | +| Proof — نظرة موحّدة | `GET /api/v1/platform/proof/overview` | يلخص demo ledger + proof pack + إشارة للتقارير. | +| Gmail draft (payload) | `POST /api/v1/platform/integrations/gmail/draft` | لا OAuth في المسار. | +| Calendar draft | `POST /api/v1/platform/integrations/calendar/draft` | | +| Moyasar payment draft | `POST /api/v1/platform/integrations/moyasar/payment-draft` | هللات + مسودة رابط دفع شكلية. | +| Lead form ingest | `POST /api/v1/platform/ingest/lead-form` | مصدر `trusted_simulation` في MVP. | +| استيراد جهات (معاينة) | `POST /api/v1/platform/contacts/import-preview` | CSV/صفوف — لا إرسال. | +| Growth missions (اسم منتجي) | `GET /api/v1/growth-operator/missions` | Alias رفيع: نفس `list_growth_missions` + `canonical_route` → `GET /api/v1/innovation/growth-missions`. | +| Proof pack demo (اسم منتجي) | `GET /api/v1/growth-operator/proof-pack/demo` | Alias → `GET /api/v1/business/proof-pack/demo`. | +| Security curator | `GET /api/v1/security-curator/demo`، `POST /api/v1/security-curator/redact`، `POST /api/v1/security-curator/inspect-diff` | لا تطبيق patch تلقائياً — فحص وإرجاع قرار. | +| Growth curator | `GET /api/v1/growth-curator/report/demo`، `POST /api/v1/growth-curator/messages/grade` | تقدير رسائل وتقرير أسبوعي تجريبي. | +| Meeting intelligence | `POST /api/v1/meeting-intelligence/transcript/summarize`، `.../followup/draft`، `.../brief/pre-meeting` | نص فقط — بدون إدراج تقويم حي. | +| Model router | `GET /api/v1/model-router/tasks`، `POST /api/v1/model-router/route`، `GET /api/v1/model-router/providers` | تلميحات تكوين deterministic. | +| Connector catalog | `GET /api/v1/connectors/catalog` | بيانات ثابتة + `risk_level` — لا OAuth في الاستجابة. | +| Agent observability | `GET /api/v1/agent-observability/demo`، `POST .../eval/safety`، `POST .../eval/saudi-tone`، `POST .../trace/build` | أشكال تقييم/تتبع للربط بـ Langfuse لاحقاً. | +| Platform — ingest حدث | `POST /api/v1/platform/events/ingest` | حدث موحّد (مثلاً `trusted_simulation`). | +| Platform — موافقة (stub) | `POST /api/v1/platform/actions/approve` | MVP: استجابة شكلية — ربط دفتر قرارات لاحقاً. | +| Platform — خدمات (alias كتالوج) | `GET /api/v1/platform/services/catalog` | يعادل `GET /api/v1/platform/service-catalog`. | +| Platform — proof ledger demo | `GET /api/v1/platform/proof-ledger/demo` | يلف `build_demo_proof_ledger` للعرض. | +| Platform — هوية (تجريبي) | `GET /api/v1/platform/identity/resolve-demo` | عرض توضيحي فقط. | +| Targeting — توصية حسابات | `POST /api/v1/targeting/accounts/recommend` | بيانات demo deterministic. | +| Targeting — لجنة شراء | `POST /api/v1/targeting/buying-committee/map` | | +| Targeting — تقييم جهة | `POST /api/v1/targeting/contacts/evaluate` | سياسات مصدر + قنوات مقترحة. | +| Targeting — تحليل قائمة مرفوعة | `POST /api/v1/targeting/uploaded-list/analyze` | نفس منطق `platform/contacts/import-preview`. | +| Targeting — خطة تواصل | `POST /api/v1/targeting/outreach/plan` | مسودات وموافقة فقط. | +| Targeting — LinkedIn آمن | `POST /api/v1/targeting/linkedin/strategy` | Lead Gen أولاً؛ `do_not_do` صريحة. | +| Targeting — خدمات | `GET /api/v1/targeting/services` | | +| Targeting — تشخيص مجاني | `POST /api/v1/targeting/free-diagnostic` | | +| Targeting — قوالب عقود | `GET /api/v1/targeting/contracts/templates` | ليست استشارة قانونية — مراجعة بشرية. | +| Targeting — trust score | `POST /api/v1/targeting/trust-score` | جسر إلى منطق intelligence. | +| Intelligence — كتالوج المهمات | `GET /api/v1/intelligence/missions/catalog` | يضم metadata + رابط لـ innovation. | +| Intelligence — تفصيل مهمة | `GET /api/v1/intelligence/missions/{mission_id}` | | +| Intelligence — Action graph | `POST /api/v1/intelligence/action-graph/demo` | مخطط signal→proof (عرض). | +| Intelligence — ذاكرة قرارات | `GET /api/v1/intelligence/decision-memory/demo`، `POST .../decision-memory/record` | in-memory MVP. | +| Security — تنقية trace | `POST /api/v1/security-curator/trace/sanitize` | بيانات متداخلة آمنة للمراقبة. | +| Growth curator — جرد مهارات | `GET /api/v1/growth-curator/skills/demo` | | +| Growth curator — تنسيق مهمات | `GET /api/v1/growth-curator/missions/curate/demo` | | +| Platform — تقييم إجراء (alias) | `POST /api/v1/platform/actions/evaluate` | يعادل `POST /api/v1/platform/policy/evaluate`. | +| Platform — موافقة بشرية | `POST /api/v1/platform/actions/approve` | يسجّل في `action_ledger` — لا تنفيذ live. | +| أحداث منصة موسّعة | `email.received`, `payment.paid`, `review.created`, … | انظر `EventType` في `event_bus.py`. | +| Service Tower — كتالوج + توصية | `GET /api/v1/services/catalog`، `POST /api/v1/services/recommend`، `POST /api/v1/services/start` | [`SERVICE_TOWER_STRATEGY.md`](../SERVICE_TOWER_STRATEGY.md) | +| Service Tower — تشغيل عرض | `GET /api/v1/services/demo/dashboard`، `GET /api/v1/services/ceo/daily-brief`، `GET /api/v1/services/ceo/end-of-day`، `POST /api/v1/services/approval-card` | أزرار ≤٣ — لا live send | +| Service Tower — تفاصيل خدمة | `GET /api/v1/services/{service_id}/workflow`، `POST .../quote`، `GET .../intake-questions`، `POST .../validate`، `GET .../deliverables`، `GET .../upgrade` | | +| Service Tower — خرائط ثابتة | `GET /api/v1/services/verticals`، `GET /api/v1/services/upgrade-paths`، `GET /api/v1/services/contracts/templates` | ثلاثة أبواب + ترقيات + مسودات عقود (ليست استشارة قانونية) | +| Launch Ops — بيتا وديمو | `GET /api/v1/launch/private-beta/offer`، `GET /api/v1/launch/demo-script`، `GET /api/v1/launch/outreach/first-20`، `GET /api/v1/launch/go-no-go`، `POST /api/v1/launch/go-no-go`، `GET /api/v1/launch/scorecard` | [`PRIVATE_BETA_LAUNCH_TODAY.md`](../PRIVATE_BETA_LAUNCH_TODAY.md) | +| Revenue Today (عروض + تسليم + دفع يدوي) | `GET /api/v1/revenue-launch/offer` (معامل اختياري `lang=en` يضيف `title_en` بجانب `title_ar`)، `.../outreach/first-20`، `.../demo-flow`، `.../pipeline/schema`، `.../pilot-delivery`، `.../payment/manual-flow`، `.../proof-pack/template` | [`REVENUE_TODAY_PLAYBOOK.md`](../REVENUE_TODAY_PLAYBOOK.md) — لا charge من API داخل Dealix | +| Service Excellence OS | `GET /api/v1/service-excellence/review/all`، `GET /api/v1/service-excellence/{id}/feature-matrix`، `.../score`، `.../workflow`، `.../proof-metrics`، `.../gap-analysis`، `.../launch-package`، `.../backlog`، `.../research-brief`، `.../review` | [`SERVICE_EXCELLENCE_OS.md`](../SERVICE_EXCELLENCE_OS.md) | +| Autonomous Service Operator | `POST /api/v1/operator/chat/message`، `POST /api/v1/operator/chat/decision`، `GET /api/v1/operator/session/{id}`، `GET /api/v1/operator/cards/pending`، `POST /api/v1/operator/service/start`، `POST /api/v1/operator/service/continue`، `GET /api/v1/operator/proof-pack/demo`، `GET /api/v1/operator/whatsapp/daily-brief`، `GET /api/v1/operator/bundles`، `GET /api/v1/operator/tools/matrix`، `GET /api/v1/operator/upsell` | [`AUTONOMOUS_SERVICE_OPERATOR.md`](../AUTONOMOUS_SERVICE_OPERATOR.md) | +| Revenue Company OS | `GET /api/v1/revenue-os/company-os/command-feed/demo`، `POST /api/v1/revenue-os/company-os/events/ingest`، `GET .../work-units/demo`، `GET .../channel-health/demo`، `GET .../opportunity-factory/demo`، `GET .../action-graph/demo`، `GET .../self-improvement/weekly-report`، `GET .../proof-ledger/demo`، `GET .../services/snapshot` | [`AUTONOMOUS_REVENUE_COMPANY_OS.md`](../AUTONOMOUS_REVENUE_COMPANY_OS.md) | +| Customer Ops — تشغيل Pilot ودعم | `GET /api/v1/customer-ops/onboarding/checklist`، `GET /api/v1/customer-ops/support/sla`، `GET /api/v1/customer-ops/connectors/status`، `GET /api/v1/customer-ops/success/cadence`، `GET /api/v1/customer-ops/incidents/playbook`، `POST /api/v1/customer-ops/support/route` (JSON: `issue_ar`)، `GET /api/v1/customer-ops/incidents/classify?severity=P0` | [`ONBOARDING_RUNBOOK.md`](../ONBOARDING_RUNBOOK.md)، [`SUPPORT_SLA.md`](../SUPPORT_SLA.md) — لا إرسال live | + +## Staging smoke + +مسارات يُنصح بفحصها بعد النشر: انظر [`STAGING_DEPLOYMENT.md`](../STAGING_DEPLOYMENT.md) و`scripts/smoke_staging.py`. diff --git a/dealix/landing/agency-partner.html b/dealix/landing/agency-partner.html index a3ba5fa3..a88d30fd 100644 --- a/dealix/landing/agency-partner.html +++ b/dealix/landing/agency-partner.html @@ -1,90 +1,17 @@ - + - - -Dealix — برنامج وكالة شريكة - + + + Dealix — شركاء وكالات + + -
-

برنامج وكالة شريكة

-

إذا كنت وكالة تسويق/مبيعات/CRM في السعودية، Dealix يشتغل خلفك: - أنت تختار العميل، Dealix يشغل النظام، وأنت تأخذ revenue share + co-branded Proof Pack.

+
+

برنامج شركاء الوكالات

+

Agency Partner Mode ضمن Autonomous Revenue Company OS: onboarding وكالة، عميل تجريبي، Proof مشترك، وإطار revenue share — كله Approval-first ومسودات حتى المراجعة القانونية.

+ راسلنا
- -
-
-

ماذا تحصل الوكالة؟

-
-
- Setup Fee -

10,000–50,000 ريال حسب الحزمة.

-
-
- Revenue Share -

على كل عميل تجلبه + اشتراكاته الشهرية.

-
-
- Co-Branded Proof -

تقارير شهرية بعلامة الوكالة لعملائها.

-
-
- Client Dashboard -

لوحة لإدارة كل عملاء الوكالة في مكان واحد.

-
-
-
- -
-

كيف تبدأ؟

-
    -
  • Onboarding للوكالة (2-3 أيام).
  • -
  • أول عميل عبر Free Diagnostic.
  • -
  • تشغيل Pilot 7 أيام.
  • -
  • تسليم Proof Pack بعلامة الوكالة.
  • -
  • تحويل العميل لـ Growth OS الشهري + revenue share.
  • -
-
- -
-

لماذا Dealix بدلاً من البناء داخلياً؟

-
    -
  • Saudi-first — رسائل عربية طبيعية.
  • -
  • Approval-first — تحمي سمعة عملائك.
  • -
  • PDPL-aware — لا cold WhatsApp.
  • -
  • Multi-channel — واتساب + إيميل + تقويم + Sheets.
  • -
  • Proof Pack شهري قابل للتسليم.
  • -
  • Self-improving — Curator يحسّن الرسائل أسبوعياً.
  • -
-
- - -
diff --git a/dealix/landing/command-center-en.html b/dealix/landing/command-center-en.html new file mode 100644 index 00000000..c222df41 --- /dev/null +++ b/dealix/landing/command-center-en.html @@ -0,0 +1,64 @@ + + + + + + Dealix Revenue Command Center — API hub (EN) + + + + + + + +
+ Dealix — Revenue Command Center (EN) + + Full Arabic dashboard (static demo) + · API map + +
+
+

API hub

+

This page is a short English companion to the rich RTL demo in command-center.html. Wire a real dashboard to these endpoints (same origin or CORS-enabled staging).

+ +
+

Command center (revenue graph)

+
    +
  • GET /api/v1/command-center/snapshot?customer_id=demo — aggregated snapshot for in-product dashboards.
  • +
  • Additional routes: see api/routers/command_center.py (agents, proof pack, leaks, maturity, …).
  • +
+
+ +
+

v3 stack (autonomous revenue OS)

+
    +
  • GET /api/v1/v3/stack — layer names and recommended tools.
  • +
  • GET /api/v1/v3/command-center/snapshot — included in staging smoke scripts.
  • +
  • GET /api/v1/v3/agents — agent catalog.
  • +
+
+ +
+

Growth tower (platform + services)

+
    +
  • GET /api/v1/platform/inbox/feed
  • +
  • GET /api/v1/services/catalog
  • +
  • GET /api/v1/launch/go-no-go · GET /api/v1/revenue-launch/offer?lang=en
  • +
+
+ +

No live WhatsApp/Gmail/calendar sends from these demos unless explicitly enabled in production policy.

+
+ + diff --git a/dealix/landing/command-center.html b/dealix/landing/command-center.html index 1a6ececf..1f468bfd 100644 --- a/dealix/landing/command-center.html +++ b/dealix/landing/command-center.html @@ -188,6 +188,7 @@
14اجتماع محجوز
7.4×عائد على Dealix
← العودة للموقع + English diff --git a/dealix/landing/companies.html b/dealix/landing/companies.html index 174df91b..a352248e 100644 --- a/dealix/landing/companies.html +++ b/dealix/landing/companies.html @@ -1,125 +1,49 @@ - + - - -Dealix للشركات — Saudi Revenue Execution OS - + + + Dealix للشركات — نمو بموافقة وProof Pack + + + + -
-

Dealix للشركات

-

حوّل بياناتك وقنواتك إلى فرص ورسائل واجتماعات وProof Pack — - Approval-first في كل خطوة، بدون scraping ولا cold WhatsApp.

-
- -
-
-

المشكلة

-
    -
  • عندك إيميل وقائمة عملاء قدامى وقنوات نشطة، لكن لا تعرف وش أهم شيء اليوم.
  • -
  • الفريق يقضي وقت كبير على الـ outreach بدون نتائج محسوبة.
  • -
  • تخاف من حظر القناة لو أرسلت بدون عناية.
  • -
  • لا يوجد Proof واضح للإدارة عن العائد.
  • -
-
- -
-

كيف يعمل Dealix

-
    -
  • اختر هدفك: عملاء جدد / استخدام قائمة / شراكات / تشغيل يومي.
  • -
  • Dealix يوصي بالخدمة + يجمع intake.
  • -
  • يطلع لك 10 فرص B2B + رسائل عربية + خطة متابعة.
  • -
  • كل رسالة تنتظر اعتمادك قبل الإرسال.
  • -
  • Proof Pack أسبوعي + Founder Shadow Board شهري.
  • -
-
- -
-

الباقات

-
-
-
Growth Starter
-
499 ريال
-
Free Diagnostic + First 10 Opportunities + Proof Pack مختصر
-
-
-
Data to Revenue
-
1,500 ريال
-
List Intelligence + Top 50 Targets + رسائل + Risk Report
-
-
-
Executive Growth OS
-
2,999 ريال شهرياً
-
Daily Brief + Approvals + Proof Pack أسبوعي
-
-
-
Full Growth Control Tower
-
Custom
-
30 يوم — كل الخدمات على مراحل
-
-
-
- -
- ضمانات Dealix: - Approval-first في كل قناة. لا scraping. لا cold WhatsApp. - لا charge بدون موافقة. لا وعود مضمونة. Proof Pack بالأرقام. -
- -
-

Proof Pack

-
    -
  • Opportunities created.
  • -
  • Drafts created + approved.
  • -
  • Replies received + meetings drafted.
  • -
  • Pipeline influenced (SAR).
  • -
  • Risks blocked (مخاطر منعت تلقائياً).
  • -
  • Time saved (ساعات).
  • -
-
- -
-

الأمان والامتثال

-
    -
  • Approval-first في كل قناة — لا live send بدون اعتمادك.
  • -
  • PDPL-aware: لا cold WhatsApp، DPA draft جاهز.
  • -
  • Secret redactor + Patch firewall + Trace redactor.
  • -
  • Saudi Tone + Safety eval قبل كل رسالة.
  • -
  • Action Ledger يسجّل كل فعل + من اعتمده.
  • -
-
- -
- ابدأ بالتشخيص المجاني - احجز ديمو 12 دقيقة + +
+

Dealix — الشركات

+

+ حوّل بياناتك وقنواتك إلى فرص ورسائل واجتماعات وProof Pack +

+

+ Dealix يكتشف الفرص، يكتب الرسائل بالعربي، ويطلب موافقتك قبل أي تواصل. Saudi Revenue Execution OS — مسودات أولاً، تقييم مخاطر، وإثبات أثر؛ لا وعود بنتائج مضمونة؛ لا واتساب بارد افتراضياً؛ لا scraping لـ LinkedIn. +

+ +
+
+
+

ماذا تستلم؟

+
    +
  • فرص مؤهّلة مع «لماذا الآن» وقناة مقترحة.
  • +
  • مسودات عربية للمراجعة — لا إرسال تلقائي افتراضي.
  • +
  • تقييم مخاطر التواصل (safe / يحتاج مراجعة / ممنوع).
  • +
  • Proof Pack يوثّق المسودات والموافقات والمخاطر الممنوعة حيث ينطبق.
  • +
+
+

انظر أيضاً: POSITIONING_LOCK.md

diff --git a/dealix/landing/first-10-opportunities.html b/dealix/landing/first-10-opportunities.html index a9a331f5..d9d1b2ae 100644 --- a/dealix/landing/first-10-opportunities.html +++ b/dealix/landing/first-10-opportunities.html @@ -1,98 +1,17 @@ - + - - -Dealix — 10 فرص في 10 دقائق - + + + Dealix — ١٠ فرص + + -
- Kill Feature — متاح في Private Beta -

10 فرص في 10 دقائق

-

أعطنا قطاعك ومدينتك وعرضك، نطلع لك 10 فرص B2B مع why-now + رسائل عربية - + خطة متابعة 7 أيام + Proof Pack — وأنت توافق قبل أي تواصل.

+
+

سباق ١٠ فرص

+

لماذا الآن، قناة آمنة، مسودات عربية، موافقة قبل أي تواصل.

+

API: POST /api/v1/intelligence/missions/first-10-opportunities أو مسار الابتكار الموثّق في API_CANONICAL_ALIASES.

- -
-
-

ما الذي ستحصل عليه

-
    -
  • 10 فرص B2B مرتبة حسب fit_score
  • -
  • سبب "لماذا الآن" لكل فرصة (إشارات شراء حقيقية)
  • -
  • صانع القرار المحتمل (founder/head of sales/etc.)
  • -
  • أفضل قناة (email/LinkedIn Lead Form/شريك)
  • -
  • 10 رسائل عربية بنبرة طبيعية سعودية
  • -
  • contactability gate (safe / needs_review / blocked)
  • -
  • خطة متابعة 7 أيام
  • -
  • Proof Pack تفصيلي بعد الأسبوع
  • -
-
- -
-

الأسعار

-
-
-
Pilot 7 أيام
-
499 ريال
-
أو مجاني مقابل case study
-
-
-
Paid Pilot 30 يوم
-
1,500 ريال
-
إعداد موسّع + 3 جولات
-
-
-
Growth OS شهري
-
2,999 ريال
-
المنصة الكاملة شهرياً
-
-
-
- -
-

الأمان

-
    -
  • Approval-first — لا إرسال بدون موافقتك.
  • -
  • لا cold WhatsApp (PDPL-aware).
  • -
  • لا scraping ولا auto-DM على LinkedIn.
  • -
  • لا charge بدون تأكيد.
  • -
  • لا وعود بنتائج مضمونة.
  • -
-
- - -
diff --git a/dealix/landing/free-diagnostic.html b/dealix/landing/free-diagnostic.html index 781f538f..c99b27d0 100644 --- a/dealix/landing/free-diagnostic.html +++ b/dealix/landing/free-diagnostic.html @@ -1,71 +1,17 @@ - + - - -Dealix — تشخيص نمو مجاني - + + + Dealix — تشخيص نمو مجاني + + -
-

تشخيص نمو مجاني

-

أرسل لنا قطاعك ومدينتك وعرضك، نرسل لك خلال 24 ساعة عمل: - 3 فرص B2B + رسالة عربية + تقرير مخاطر + خطة Pilot — كل شيء بدون التزام.

+
+

تشخيص نمو مجاني

+

٣ فرص + رسالة + مخاطر + اقتراح Pilot — بدون التزام.

+ ابدأ
- -
-
-

ماذا تستلم؟

-
    -
  • 3 فرص B2B مناسبة لقطاعك ومدينتك
  • -
  • سبب "لماذا الآن" لكل فرصة
  • -
  • رسالة عربية مخصصة (≤80 كلمة، نبرة سعودية طبيعية)
  • -
  • تقرير مخاطر (PDPL + سمعة القناة)
  • -
  • خطة Pilot 7 أيام مقترحة
  • -
-
- -
-

كيف نعمل؟

-
    -
  • تملأ نموذج 3 دقائق (قطاع/مدينة/عرض/هدف)
  • -
  • Dealix يولّد التشخيص خلال 24 ساعة عمل
  • -
  • تستلمه على إيميلك (PDF + JSON)
  • -
  • إذا أعجبك، نكمل Pilot 7 أيام بـ 499 ريال
  • -
  • أو مجاني مقابل case study بعد انتهاء الـPilot
  • -
-
- -
- ضمانات Dealix: - Approval-first — لا نرسل أي شيء قبل موافقتك. - لا cold WhatsApp. لا scraping. لا وعود بنتائج مضمونة. - PDPL-aware من اليوم الأول. -
- - -
diff --git a/dealix/landing/growth-os.html b/dealix/landing/growth-os.html index f5d05d5d..cccb9290 100644 --- a/dealix/landing/growth-os.html +++ b/dealix/landing/growth-os.html @@ -1,120 +1,18 @@ - + - - -Dealix Growth OS — اشتراك شهري - + + + Dealix — Growth OS + + -
-

Dealix Growth OS

-

منصة نمو شهرية تدير قنواتك الخارجية، تجمع كل الإشارات في Command Feed، - تكتب الرسائل، تطلب موافقات، ترتب اجتماعات، وتطلع Proof Pack شهري.

+
+

Growth OS

+

Growth OS — طبقة تشغيل شهرية ضمن Dealix Company OS: موجز يومي، كروت قرار، موافقات، Proof Pack؛ قنوات متعددة مع Safe Tool Gateway (مسودات افتراضياً).

+

اشتراك تقريبي من ٢٩٩٩ ريال/شهر حسب النطاق؛ pilot أضيق يُحدَّد بعد الديمو.

+ عرض الخدمات
- -
-
-

ماذا تستلم شهرياً؟

-
    -
  • Daily Command Feed عربي — 5 cards/day.
  • -
  • First 10 Opportunities Sprint كل أسبوع.
  • -
  • List Intelligence على قاعدة عملائك.
  • -
  • Email + WhatsApp drafts (بدون live send بدون اعتماد).
  • -
  • Calendar drafts + meeting briefs.
  • -
  • Approval Center: CEO يوافق من واتساب.
  • -
  • Proof Pack شهري + Founder Shadow Board أسبوعي.
  • -
  • Reputation Guard على كل قناة.
  • -
  • Service Excellence Score على كل campaign.
  • -
  • Decision Memory يتعلم من Accept/Skip/Edit.
  • -
-
- -
-

التسعير

-
-
-
Pilot 30 يوم
-
1,500–3,000 ريال
-
إعداد + شهر تجربة
-
-
-
Growth OS Monthly
-
2,999 ريال
-
شهري — بعد الإثبات
-
-
-
Annual (–15%)
-
30,589 ريال
-
دفع سنوي — توفير شهرين
-
-
-
- -
-

التكاملات (Phase 1)

-
    -
  • Gmail (drafts فقط افتراضياً)
  • -
  • Google Calendar (drafts فقط)
  • -
  • WhatsApp Cloud (مع opt-in + approval)
  • -
  • Moyasar (invoice/payment link manual)
  • -
  • Google Sheets (read/append بموافقة)
  • -
  • Website Forms (ingest)
  • -
-

المرحلة 2: LinkedIn Lead Forms، Google Business Profile، Google Meet transcripts.

-

المرحلة 3: Instagram, X (ingest only), social drafts.

-
- -
-

الأمان والامتثال

-
    -
  • Approval-first في كل قناة — لا live send بدون اعتماد بشري.
  • -
  • PDPL-aware: لا cold WhatsApp، DPA draft جاهز.
  • -
  • Secret redactor + patch firewall + trace redactor.
  • -
  • Saudi Tone + Safety eval قبل كل رسالة.
  • -
  • Reputation Guard يوقف القناة عند تدهور السمعة.
  • -
  • Action Ledger يسجّل كل فعل + من اعتمده.
  • -
-
- -
-

كيف تبدأ؟

-
    -
  1. Free Growth Diagnostic — 24 ساعة بدون التزام.
  2. -
  3. Pilot 7 أيام بـ499 ريال — تثبت طريقة التشغيل.
  4. -
  5. Growth OS Pilot 30 يوم — تشغيل شهر كامل.
  6. -
  7. Growth OS Monthly — التزام مستمر مع Proof Pack شهري.
  8. -
-
- - -
diff --git a/dealix/landing/index.html b/dealix/landing/index.html index 4a9279fe..7c86467b 100644 --- a/dealix/landing/index.html +++ b/dealix/landing/index.html @@ -87,6 +87,11 @@