Merge pull request #28 from VoXc2/sync/dealix-full-complete

feat(dealix): sync full local Dealix package to ai-company
This commit is contained in:
VoXc2 2026-05-01 21:07:50 +03:00 committed by GitHub
commit 265f1c6185
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
242 changed files with 8931 additions and 20686 deletions

View File

@ -1,5 +1,6 @@
# Canonical CI for the Dealix API package (monorepo). # Canonical CI for the Dealix API package (monorepo).
# GitHub only loads workflows from the repository root .github/workflows/. # 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 name: Dealix API CI
on: on:
@ -18,8 +19,17 @@ defaults:
run: run:
working-directory: dealix 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: jobs:
test: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -41,37 +51,53 @@ jobs:
run: python -m compileall api auto_client_acquisition run: python -m compileall api auto_client_acquisition
- name: Tests - name: Tests
env: # tests/unit expects alternate package facades; run canonical integration tests only.
APP_ENV: test run: pytest -q --no-cov --ignore=tests/unit
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
- name: Embeddings pipeline placeholder - name: Embeddings pipeline placeholder
run: python scripts/embeddings_pipeline_placeholder.py run: python scripts/embeddings_pipeline_placeholder.py
- name: Deterministic eval smoke - 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 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

View File

@ -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 name: Dealix staging smoke
on: on:
@ -17,17 +17,46 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: pip
cache-dependency-path: dealix/requirements.txt
- name: Install httpx - name: Install dependencies
run: pip install httpx run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx aiosqlite
- name: Run staging smoke - name: Run staging smoke
env: env:
STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
STAGING_API_KEY: ${{ secrets.STAGING_API_KEY }} 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: | run: |
if [ -z "$STAGING_BASE_URL" ]; then if [ -z "$STAGING_BASE_URL" ]; then
echo "STAGING_BASE_URL secret not set — skipping." echo "STAGING_BASE_URL secret not set — skipping."
exit 0 exit 0
fi fi
python scripts/smoke_staging.py --base-url "$STAGING_BASE_URL" 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"

View File

@ -22,7 +22,7 @@ from api.routers import (
autonomous, autonomous,
business, business,
command_center, command_center,
connector_catalog, connector_router,
customer_ops, customer_ops,
customer_success, customer_success,
data, data,
@ -46,9 +46,8 @@ from api.routers import (
pricing, pricing,
prospect, prospect,
public, public,
revenue,
revenue_company_os,
revenue_launch, revenue_launch,
revenue,
revenue_os, revenue_os,
sales, sales,
sectors, sectors,
@ -147,6 +146,7 @@ def create_app() -> FastAPI:
app.include_router(pricing.router) app.include_router(pricing.router)
app.include_router(prospect.router) app.include_router(prospect.router)
app.include_router(autonomous.router) app.include_router(autonomous.router)
app.include_router(autonomous_service_operator.router)
app.include_router(data.router) app.include_router(data.router)
app.include_router(outreach.router) app.include_router(outreach.router)
app.include_router(revenue.router) app.include_router(revenue.router)
@ -156,30 +156,28 @@ def create_app() -> FastAPI:
app.include_router(dominance.router) app.include_router(dominance.router)
app.include_router(full_os.router) app.include_router(full_os.router)
app.include_router(customer_success.router) app.include_router(customer_success.router)
app.include_router(customer_ops.router)
app.include_router(ecosystem.router) app.include_router(ecosystem.router)
app.include_router(command_center.router) app.include_router(command_center.router)
app.include_router(revenue_os.router) app.include_router(revenue_os.router)
app.include_router(v3.router) app.include_router(v3.router)
app.include_router(innovation.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(platform_services.router)
app.include_router(intelligence_layer.router) app.include_router(intelligence_layer.router)
app.include_router(growth_operator.router)
app.include_router(security_curator.router) app.include_router(security_curator.router)
app.include_router(growth_curator.router) app.include_router(growth_curator.router)
app.include_router(meeting_intelligence.router) app.include_router(meeting_intelligence.router)
app.include_router(model_router.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(agent_observability.router)
app.include_router(targeting_os.router) app.include_router(targeting_os.router)
app.include_router(service_tower.router) app.include_router(service_tower.router)
app.include_router(service_excellence.router) app.include_router(service_excellence.router)
app.include_router(launch_ops.router) app.include_router(launch_ops.router)
app.include_router(revenue_launch.router) app.include_router(revenue_launch.router)
app.include_router(autonomous_service_operator.router) app.include_router(business.router)
app.include_router(revenue_company_os.router) app.include_router(personal_operator.router)
app.include_router(customer_ops.router)
app.include_router(public.router) app.include_router(public.router)
app.include_router(admin.router) app.include_router(admin.router)

View File

@ -1,4 +1,4 @@
"""Agent Observability router — trace events + safety/tone evals.""" """Agent observability demo endpoints — evals and trace shapes."""
from __future__ import annotations from __future__ import annotations
@ -6,45 +6,36 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.agent_observability import ( from auto_client_acquisition.agent_observability.safety_eval import evaluate_safety
build_trace_event, from auto_client_acquisition.agent_observability.saudi_tone_eval import evaluate_saudi_tone
run_eval_pack, from auto_client_acquisition.agent_observability.trace_events import build_trace_event
safety_eval,
saudi_tone_eval,
)
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") @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( return build_trace_event(
workflow_name=payload.get("workflow_name", "unknown"), workflow_name=str(payload.get("workflow_name") or "demo"),
agent_name=payload.get("agent_name", "unknown"), agent_name=str(payload.get("agent_name") or "dealix"),
status=payload.get("status", "started"), action_type=str(payload.get("action_type") or "draft"),
user_id=payload.get("user_id"), policy_result=str(payload.get("policy_result") or "approval_required"),
company_id=payload.get("company_id"), tool_called=payload.get("tool_called"),
tool=payload.get("tool"), outcome=payload.get("outcome"),
policy_result=payload.get("policy_result"), metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
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"),
) )
@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()

View File

@ -1,4 +1,4 @@
"""Autonomous Service Operator router — chat + decisions + sessions + bundles.""" """Autonomous Service Operator — /api/v1/operator (deterministic MVP)."""
from __future__ import annotations from __future__ import annotations
@ -7,298 +7,131 @@ from typing import Any
from fastapi import APIRouter, Body, HTTPException from fastapi import APIRouter, Body, HTTPException
from auto_client_acquisition.autonomous_service_operator import ( from auto_client_acquisition.autonomous_service_operator import (
OperatorMemory, approval_manager as am,
add_agency_client, agency_mode,
build_agency_dashboard, client_mode,
build_approval_card, conversation_router,
build_ceo_command_center, executive_mode,
build_client_dashboard, intake_collector,
build_co_branded_proof_pack, proof_pack_dispatcher,
build_executive_daily_brief, self_growth_mode,
build_intake_questions_for_intent, service_bundles,
build_new_session, service_delivery_mode,
build_revenue_risks_summary, session_state as ss,
build_service_pipeline, tool_action_planner,
build_session_context, upsell_engine,
build_upsell_card, whatsapp_renderer,
classify_intent, workflow_runner as wr,
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,
) )
from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score
router = APIRouter(prefix="/api/v1/operator", tags=["autonomous-service-operator"]) router = APIRouter(prefix="/api/v1/operator", tags=["autonomous_service_operator"])
# Process-level memory (demo). Production = Redis/Supabase.
_MEMORY = OperatorMemory() 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") @router.post("/chat/message")
async def chat_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def operator_chat_message(body: dict[str, Any] = Body(...)) -> dict[str, Any]:
"""Send a message to the operator. Classifies intent + recommends action.""" msg = str(body.get("message") or "").strip()
return handle_message( if not msg:
message=payload.get("message", ""), raise HTTPException(status_code=400, detail="message_required")
customer_id=payload.get("customer_id"), sid = str(body.get("session_id") or ss.new_session_id())
has_contact_list=bool(payload.get("has_contact_list", False)), ss.touch_session(sid)
is_agency=bool(payload.get("is_agency", False)), mode = str(body.get("mode") or "client")
is_local_business=bool(payload.get("is_local_business", False)), result = conversation_router.handle_message(sid, msg, mode=mode)
budget_sar=int(payload.get("budget_sar", 1000)), result["mode_profile"] = _mode_profile(mode)
) return result
@router.post("/chat/decision") @router.post("/chat/decision")
async def chat_decision(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def operator_chat_decision(body: dict[str, Any] = Body(...)) -> dict[str, Any]:
"""Process an approval/edit/skip decision on an action card.""" sid = str(body.get("session_id") or "").strip()
card = payload.get("card") or build_approval_card( dec = str(body.get("decision") or "").strip()
action_type="example", if not sid or not dec:
title_ar="فعل مثال", raise HTTPException(status_code=400, detail="session_id_and_decision_required")
summary_ar="مثال", updated = am.apply_decision(sid, dec)
) return {"session": updated, "demo": True}
return process_approval_decision(
card,
decision=payload.get("decision", "skip"),
decided_by=payload.get("decided_by", "user"),
note=payload.get("note", ""),
)
@router.post("/chat/classify") @router.get("/session/{session_id}")
async def chat_classify(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def operator_get_session(session_id: str) -> dict[str, Any]:
return classify_intent(payload.get("message", "")) s = ss.get_session(session_id)
if not s:
raise HTTPException(status_code=404, detail="session_not_found")
return {**s, "demo": True}
# ── Sessions ───────────────────────────────────────────────── @router.get("/cards/pending")
@router.post("/sessions/new") async def operator_cards_pending() -> dict[str, Any]:
async def sessions_new(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return {"pending": ss.list_sessions_with_pending(), "demo": True}
session = build_new_session(customer_id=payload.get("customer_id"))
_MEMORY.upsert_session(session)
return session.to_dict()
@router.get("/sessions/{session_id}")
async def sessions_get(session_id: str) -> dict[str, Any]:
session = _MEMORY.get_session(session_id)
if session is None:
raise HTTPException(status_code=404, detail="session not found")
return session.to_dict()
@router.post("/sessions/{session_id}/transition")
async def sessions_transition(
session_id: str,
payload: dict[str, Any] = Body(...),
) -> dict[str, Any]:
session = _MEMORY.get_session(session_id)
if session is None:
raise HTTPException(status_code=404, detail="session not found")
transition_session(
session,
new_state=payload.get("new_state", "new"),
note=payload.get("note", ""),
)
return session.to_dict()
@router.get("/sessions/{session_id}/context")
async def sessions_context(session_id: str) -> dict[str, Any]:
return build_session_context(memory=_MEMORY, session_id=session_id)
# ── Cards / Approvals ────────────────────────────────────────
@router.post("/cards/approval")
async def cards_approval(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_approval_card(
action_type=payload.get("action_type", "unknown"),
title_ar=payload.get("title_ar", ""),
summary_ar=payload.get("summary_ar", ""),
risk_level=payload.get("risk_level", "low"),
why_now_ar=payload.get("why_now_ar", ""),
recommended_action_ar=payload.get("recommended_action_ar", ""),
expected_impact_sar=float(payload.get("expected_impact_sar", 0)),
service_id=payload.get("service_id"),
customer_id=payload.get("customer_id"),
action_id=payload.get("action_id"),
)
@router.post("/cards/whatsapp/render")
async def cards_whatsapp_render(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
kind = payload.get("kind", "card")
if kind == "approval":
return render_approval_card_for_whatsapp(payload.get("card") or {})
if kind == "daily_brief":
return render_daily_brief_for_whatsapp(payload.get("brief") or {})
return render_card_for_whatsapp(payload.get("card") or {})
# ── Intake ───────────────────────────────────────────────────
@router.get("/intake/questions/{intent}")
async def intake_questions(intent: str) -> dict[str, Any]:
return build_intake_questions_for_intent(intent)
@router.post("/intake/validate")
async def intake_validate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return validate_intake_completeness(
payload.get("intent", "ask_services"),
payload.get("payload") or {},
)
# ── Service workflow ─────────────────────────────────────────
@router.post("/service/start") @router.post("/service/start")
async def service_start(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def operator_service_start(body: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_service_pipeline( sid = str(body.get("session_id") or ss.new_session_id())
service_id=payload.get("service_id", ""), svc_id = str(body.get("service_id") or "").strip()
customer_id=payload.get("customer_id", ""), 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") @router.post("/service/continue")
async def tools_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def operator_service_continue(body: dict[str, Any] = Body(...)) -> dict[str, Any]:
return plan_tool_action( sid = str(body.get("session_id") or "").strip()
tool=payload.get("tool", ""), event = str(body.get("event") or "draft_ready").strip()
payload=payload.get("payload"), if not sid:
customer_id=payload.get("customer_id"), raise HTTPException(status_code=400, detail="session_id_required")
context=payload.get("context"), ss.touch_session(sid)
) return {"session": wr.advance(sid, event), "demo": True}
# ── Proof + Upsell ───────────────────────────────────────────
@router.post("/proof-pack/dispatch")
async def proof_pack_dispatch(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return dispatch_proof_pack(
service_id=payload.get("service_id", ""),
customer_id=payload.get("customer_id"),
channel=payload.get("channel", "email"),
metrics=payload.get("metrics"),
)
@router.post("/upsell/recommend")
async def upsell_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return recommend_upsell_after_service(
completed_service_id=payload.get("completed_service_id", ""),
pilot_metrics=payload.get("pilot_metrics"),
)
@router.post("/upsell/card")
async def upsell_card(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_upsell_card(
completed_service_id=payload.get("completed_service_id", ""),
pilot_metrics=payload.get("pilot_metrics"),
)
# ── Bundles ──────────────────────────────────────────────────
@router.get("/bundles")
async def bundles() -> dict[str, Any]:
return list_bundles()
@router.post("/bundles/recommend")
async def bundles_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return recommend_bundle(
intent=payload.get("intent"),
has_contact_list=bool(payload.get("has_contact_list", False)),
is_agency=bool(payload.get("is_agency", False)),
is_local_business=bool(payload.get("is_local_business", False)),
budget_sar=int(payload.get("budget_sar", 1000)),
)
# ── Modes ────────────────────────────────────────────────────
@router.post("/mode/ceo")
async def mode_ceo(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_ceo_command_center(
company_name=payload.get("company_name", ""),
sector=payload.get("sector", "saas"),
)
@router.post("/mode/ceo/daily-brief")
async def mode_ceo_daily(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_executive_daily_brief(
company_name=payload.get("company_name", ""),
sector=payload.get("sector", "saas"),
)
@router.post("/mode/ceo/risks")
async def mode_ceo_risks() -> dict[str, Any]:
return build_revenue_risks_summary()
@router.post("/mode/client")
async def mode_client(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_client_dashboard(
customer_id=payload.get("customer_id", ""),
company_name=payload.get("company_name", ""),
active_services=payload.get("active_services") or [],
open_actions=int(payload.get("open_actions", 0)),
proof_pack_due=bool(payload.get("proof_pack_due", False)),
)
@router.post("/mode/agency")
async def mode_agency(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_agency_dashboard(
agency_id=payload.get("agency_id", "agency_demo"),
agency_name=payload.get("agency_name", ""),
clients=payload.get("clients") or [],
)
@router.post("/mode/agency/add-client")
async def mode_agency_add_client(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return add_agency_client(
agency_id=payload.get("agency_id", "agency_demo"),
client_company_name=payload.get("client_company_name", ""),
sector=payload.get("sector", ""),
monthly_subscription_sar=int(payload.get("monthly_subscription_sar", 0)),
revenue_share_pct=int(payload.get("revenue_share_pct", 20)),
)
@router.post("/mode/agency/revenue-share")
async def mode_agency_revenue_share(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return list_agency_revenue_share(clients=payload.get("clients") or [])
@router.post("/mode/agency/co-branded-proof")
async def mode_agency_co_branded_proof(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_co_branded_proof_pack(
agency_name=payload.get("agency_name", ""),
client_company_name=payload.get("client_company_name", ""),
metrics=payload.get("metrics"),
)
# ── Demos ────────────────────────────────────────────────────
@router.get("/whatsapp/daily-brief/demo")
async def whatsapp_daily_brief_demo() -> dict[str, Any]:
brief = build_executive_daily_brief(company_name="Acme")
return render_daily_brief_for_whatsapp(brief)
@router.get("/proof-pack/demo") @router.get("/proof-pack/demo")
async def proof_pack_demo() -> dict[str, Any]: async def operator_proof_pack_demo(service_id: str = "first_10_opportunities") -> dict[str, Any]:
return dispatch_proof_pack( return proof_pack_dispatcher.build_proof_pack(service_id)
service_id="first_10_opportunities_sprint",
customer_id="demo",
metrics={"opportunities_generated": 10, "drafts_approved": 6, @router.get("/whatsapp/daily-brief")
"meetings_drafted": 2, "pipeline_influenced_sar": 30000, async def operator_whatsapp_daily_brief() -> dict[str, Any]:
"risks_blocked": 3}, 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)

View File

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

View File

@ -1,208 +1,50 @@
"""Customer Ops router — onboarding + connectors + support + SLA + incidents.""" """Customer ops API — onboarding, SLA, connectors (deterministic)."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.customer_ops import ( from auto_client_acquisition.customer_ops.connector_setup_status import build_connector_status
SUPPORT_PRIORITIES, from auto_client_acquisition.customer_ops.customer_success_cadence import build_weekly_cadence
SUPPORTED_CONNECTORS, from auto_client_acquisition.customer_ops.incident_router import build_incident_playbook, classify_incident
build_at_risk_alert, from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist
build_connector_setup_summary, from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary
build_customer_success_plan, from auto_client_acquisition.customer_ops.support_ticket_router import route_ticket
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,
)
router = APIRouter(prefix="/api/v1/customer-ops", tags=["customer-ops"]) router = APIRouter(prefix="/api/v1/customer-ops", tags=["customer-ops"])
# ── Onboarding ─────────────────────────────────────────────── @router.get("/onboarding/checklist")
@router.post("/onboarding/checklist") async def onboarding_checklist(service_id: str | None = None) -> dict[str, object]:
async def onboarding_checklist(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return build_onboarding_checklist(service_id)
return build_onboarding_checklist(
customer_id=payload.get("customer_id", ""),
company_name=payload.get("company_name", ""),
bundle_id=payload.get("bundle_id"),
)
@router.post("/onboarding/update-step") @router.get("/support/sla")
async def onboarding_update_step(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def support_sla() -> dict[str, object]:
return update_onboarding_step( return build_sla_summary()
payload.get("checklist") or {},
step_id=payload.get("step_id", ""),
completed=bool(payload.get("completed", True)),
notes=payload.get("notes", ""),
)
@router.get("/onboarding/checklist/demo") @router.get("/connectors/status")
async def onboarding_checklist_demo() -> dict[str, Any]: async def connectors_status() -> dict[str, object]:
return build_onboarding_checklist( return build_connector_status()
customer_id="demo", company_name="شركة نمو للتدريب",
bundle_id="growth_starter",
)
# ── Connectors ─────────────────────────────────────────────── @router.get("/success/cadence")
@router.get("/connectors/catalog") async def success_cadence() -> dict[str, object]:
async def connectors_catalog() -> dict[str, Any]: return build_weekly_cadence()
return {
"total": len(SUPPORTED_CONNECTORS),
"connectors": [dict(c) for c in SUPPORTED_CONNECTORS],
}
@router.post("/connectors/summary") @router.get("/incidents/playbook")
async def connectors_summary(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: async def incidents_playbook() -> dict[str, object]:
return build_connector_setup_summary( return build_incident_playbook()
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.post("/support/route") @router.post("/support/route")
async def support_route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def support_route(payload: dict[str, object] = Body(default_factory=dict)) -> dict[str, object]:
return route_ticket( issue = str(payload.get("issue_ar") or "")
text=payload.get("text", ""), return route_ticket(issue)
customer_id=payload.get("customer_id", ""),
contact_email=payload.get("contact_email", ""),
)
@router.get("/support/first-response/{priority}") @router.get("/incidents/classify")
async def support_first_response(priority: str) -> dict[str, Any]: async def incidents_classify(severity: str = "P3") -> dict[str, object]:
return build_first_response_template(priority) return classify_incident(severity)
# ── 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"),
)

View File

@ -1,4 +1,4 @@
"""Growth Curator router — message grading + weekly curator report.""" """Growth curator API — grading and weekly report."""
from __future__ import annotations from __future__ import annotations
@ -6,95 +6,33 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.growth_curator import ( from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report
build_weekly_curator_report, from auto_client_acquisition.growth_curator.message_curator import grade_message
detect_duplicates, from auto_client_acquisition.growth_curator.mission_curator import curate_missions_weekly
grade_message, from auto_client_acquisition.growth_curator.skill_inventory import list_skill_inventory
inventory_skills,
recommend_next_mission,
suggest_improvement,
)
router = APIRouter(prefix="/api/v1/growth-curator", tags=["growth-curator"]) 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.get("/report/demo") @router.get("/report/demo")
async def report_demo() -> dict[str, Any]: async def report_demo() -> dict[str, Any]:
"""Demo curator report with a small synthetic dataset.""" return build_weekly_curator_report()
return build_weekly_curator_report(
messages=[
{"id": "m1", "text": "هلا أحمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"}, @router.post("/messages/grade")
{"id": "m2", "text": "هلا محمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"}, async def messages_grade(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
{"id": "m3", "text": "آخر فرصة! ضمان 100% نتائج مضمونة!"}, return grade_message(
{"id": "m4", "text": "Hi"}, str(payload.get("message_ar") or ""),
], sector=str(payload.get("sector") or ""),
playbooks=[ channel=str(payload.get("channel") or "whatsapp"),
{"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",
) )
@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()

View File

@ -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 / لا يكرر منطق ten-in-ten؛ يعرّف مسارات متوقعة في وثائق الـ beta والـ smoke.
scheduled live from this router; that happens in dedicated send / billing
/ calendar services after explicit user approval.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any from typing import Any
from fastapi import APIRouter, Body, Query from fastapi import APIRouter
from auto_client_acquisition.growth_operator import ( from auto_client_acquisition.business.proof_pack import build_demo_proof_pack
build_calendar_draft, from auto_client_acquisition.innovation.growth_missions import list_growth_missions
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,
)
router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth-operator"]) router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth_operator"])
log = logging.getLogger(__name__)
# ── 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") @router.get("/missions")
async def missions_list() -> dict[str, Any]: async def missions() -> dict[str, Any]:
return list_missions() """نفس محتوى ``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") @router.get("/proof-pack/demo")
async def proof_pack_demo( async def proof_pack_demo() -> dict[str, Any]:
customer_id: str = Query(default="demo"), """نفس ``GET /api/v1/business/proof-pack/demo`` — مسار موحّد للعرض في الـ beta."""
customer_name: str = Query(default="Demo Saudi B2B Co."), pack = build_demo_proof_pack()
) -> dict[str, Any]: if isinstance(pack, dict):
return build_weekly_proof_pack( out = dict(pack)
customer_id=customer_id, out["canonical_route"] = "/api/v1/business/proof-pack/demo"
customer_name=customer_name, return out
week_label="W18-2026", return {"pack": pack, "canonical_route": "/api/v1/business/proof-pack/demo"}
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"]
),
}

View File

@ -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 from __future__ import annotations
@ -6,135 +6,117 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.intelligence_layer import ( from auto_client_acquisition.innovation.ten_in_ten import build_ten_opportunities
DecisionMemory, from auto_client_acquisition.intelligence_layer.action_graph import build_action_graph_trace
analyze_competitive_move, from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief
build_board_brief, from auto_client_acquisition.intelligence_layer.competitive_moves import build_competitive_moves
build_command_feed_demo, from auto_client_acquisition.intelligence_layer.decision_memory import list_decisions, record_decision
build_growth_brain, from auto_client_acquisition.intelligence_layer.growth_brain import build_growth_profile
build_revenue_dna_demo, from auto_client_acquisition.intelligence_layer.intel_command_feed import build_intel_command_feed
compute_trust_score, from auto_client_acquisition.intelligence_layer.mission_engine import get_mission, list_mission_catalog
extract_revenue_dna, from auto_client_acquisition.intelligence_layer.opportunity_simulator import simulate_opportunities
learn_from_decision, from auto_client_acquisition.intelligence_layer.revenue_dna import build_revenue_dna
list_intel_missions, from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
recommend_missions,
simulate_opportunity,
)
router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence-layer"]) router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence_layer"])
# Per-customer in-memory decision memory (demo; production = Supabase)
_MEMORY: dict[str, DecisionMemory] = {}
def _memory_for(customer_id: str) -> DecisionMemory: @router.post("/growth-profile")
if customer_id not in _MEMORY: async def growth_profile(company: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
_MEMORY[customer_id] = DecisionMemory(customer_id=customer_id) return build_growth_profile(company or {})
return _MEMORY[customer_id]
# ── Growth Brain ────────────────────────────────────────────── @router.get("/command-feed")
@router.post("/growth-brain/build") async def intel_command_feed() -> dict[str, Any]:
async def growth_brain_build(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: return build_intel_command_feed()
brain = build_growth_brain(payload)
return {**brain.to_dict(), "ready_for_autopilot": brain.is_ready_for_autopilot()}
# ── Command Feed ──────────────────────────────────────────────
@router.get("/command-feed/demo") @router.get("/command-feed/demo")
async def command_feed_demo() -> dict[str, Any]: async def intel_command_feed_demo() -> dict[str, Any]:
return build_command_feed_demo() """Alias of ``GET /command-feed`` for product/docs compatibility."""
return build_intel_command_feed()
# ── Missions ────────────────────────────────────────────────── @router.post("/missions/first-10-opportunities")
@router.get("/missions") async def missions_first_10_opportunities(
async def missions_list() -> dict[str, Any]: payload: dict[str, Any] = Body(default_factory=dict),
return list_intel_missions() ) -> dict[str, Any]:
"""Thin wrapper around innovation ``build_ten_opportunities`` — no duplicate logic."""
return build_ten_opportunities(payload or None)
@router.post("/missions/recommend") @router.get("/missions/catalog")
async def missions_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: async def missions_catalog() -> dict[str, Any]:
brain_payload = payload.get("growth_brain") or payload """Mission engine metadata + pointer to innovation missions."""
brain = build_growth_brain(brain_payload) if brain_payload else None return list_mission_catalog()
return recommend_missions(brain, limit=int(payload.get("limit", 3)))
@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") @router.post("/trust-score")
async def trust_score(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def trust_score(signals: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return compute_trust_score( return compute_trust_score(signals or {})
source_quality=payload.get("source_quality", "unknown"),
opt_in=bool(payload.get("opt_in", False)),
channel=payload.get("channel", "whatsapp"),
message_text=payload.get("message_text", ""),
frequency_count_this_week=int(payload.get("frequency_count_this_week", 0)),
weekly_cap=int(payload.get("weekly_cap", 2)),
approval_status=payload.get("approval_status", "pending"),
)
# ── Revenue DNA ───────────────────────────────────────────────
@router.get("/revenue-dna/demo")
async def revenue_dna_demo() -> dict[str, Any]:
return build_revenue_dna_demo()
@router.post("/revenue-dna") @router.post("/revenue-dna")
async def revenue_dna_post(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def revenue_dna(context: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return extract_revenue_dna( return build_revenue_dna(context or {})
customer_id=payload.get("customer_id", "unknown"),
won_deals=payload.get("won_deals", []),
replies=payload.get("replies", []),
objections=payload.get("objections", []),
)
# ── Opportunity Simulator ───────────────────────────────────── @router.post("/opportunity-simulator")
@router.post("/simulate-opportunity") async def opportunity_simulator(inputs: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
async def simulate_opportunity_endpoint(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: return simulate_opportunities(inputs or {})
return simulate_opportunity(
target_count=int(payload.get("target_count", 100)),
sector=payload.get("sector", "saas"),
avg_deal_value_sar=float(payload.get("avg_deal_value_sar", 25_000)),
channel=payload.get("channel", "whatsapp"),
cold_pct=float(payload.get("cold_pct", 0)),
quality_lift=float(payload.get("quality_lift", 1.0)),
)
# ── Competitive Moves ───────────────────────────────────────── @router.post("/board-brief")
@router.post("/competitive-move/analyze") async def board_brief(snapshot: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
async def competitive_move_analyze(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: return build_board_brief(snapshot or {})
return analyze_competitive_move(
competitor_name=payload.get("competitor_name", "?"),
move_type=payload.get("move_type", "new_offer"),
payload=payload.get("payload", {}),
)
# ── Board Brief ─────────────────────────────────────────────── @router.get("/competitive-moves")
@router.get("/board-brief/demo") async def competitive_moves(sector: str | None = None) -> dict[str, Any]:
async def board_brief_demo() -> dict[str, Any]: return build_competitive_moves(sector)
return build_board_brief()
# ── Decision Memory ─────────────────────────────────────────── @router.post("/bundle")
@router.post("/decisions/record") async def intelligence_bundle(
async def decisions_record(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: payload: dict[str, Any] = Body(default_factory=dict),
customer_id = payload.get("customer_id", "demo") ) -> dict[str, Any]:
mem = _memory_for(customer_id) """
return learn_from_decision( Single round-trip for demos. Optional ``include_ten_in_ten`` merges
memory=mem, ``build_ten_opportunities`` without exposing a duplicate HTTP path.
decision=payload.get("decision", "skip"), """
action_type=payload.get("action_type", "send_whatsapp"), company = payload.get("company") if isinstance(payload.get("company"), dict) else {}
channel=payload.get("channel", "whatsapp"), out: dict[str, Any] = {
sector=payload.get("sector"), "growth_profile": build_growth_profile(company),
tone=payload.get("tone"), "intel_command_feed": build_intel_command_feed({"append_custom": payload.get("extra_card")}),
objection_id=payload.get("objection_id"), "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 {}
@router.get("/decisions/preferences") ),
async def decisions_preferences(customer_id: str) -> dict[str, Any]: "board_brief": build_board_brief(payload.get("board") if isinstance(payload.get("board"), dict) else {}),
mem = _memory_for(customer_id) "competitive_moves": build_competitive_moves(str(payload.get("sector") or "") or None),
return {"customer_id": customer_id, "preferences": mem.preferences()} }
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

View File

@ -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 from __future__ import annotations
@ -6,130 +6,40 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.launch_ops import ( from auto_client_acquisition.launch_ops.demo_flow import build_demo_script
build_12_min_demo_flow, from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go
build_close_script, from auto_client_acquisition.launch_ops.launch_scorecard import build_launch_scorecard
build_daily_launch_scorecard, from auto_client_acquisition.launch_ops.outreach_messages import build_first_twenty_outreach
build_discovery_questions, from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer
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,
)
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") @router.get("/private-beta/offer")
async def private_beta_offer() -> dict[str, Any]: async def launch_private_beta_offer() -> dict[str, Any]:
return { return build_private_beta_offer()
"offer": build_private_beta_offer(),
"safety": build_private_beta_safety_notes(),
"faq": private_beta_faq(),
}
# ── Demo flow ──────────────────────────────────────────────── @router.get("/demo-script")
@router.get("/demo/flow") async def launch_demo_script() -> dict[str, Any]:
async def demo_flow() -> dict[str, Any]: return build_demo_script()
return {
"flow": build_12_min_demo_flow(),
"discovery_questions": build_discovery_questions(),
"objections": build_objection_responses(),
"close": build_close_script(),
}
# ── Outreach ─────────────────────────────────────────────────
@router.get("/outreach/first-20") @router.get("/outreach/first-20")
async def outreach_first_20() -> dict[str, Any]: async def launch_outreach_first_20() -> dict[str, Any]:
segments = build_first_20_segments() return build_first_twenty_outreach()
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(),
}
@router.post("/outreach/message") @router.get("/go-no-go")
async def outreach_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def launch_go_no_go_get() -> dict[str, Any]:
return build_outreach_message( return evaluate_go_no_go(None)
segment_id=payload.get("segment_id", ""),
name=payload.get("name", "[الاسم]"),
)
@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") @router.post("/go-no-go")
async def go_no_go(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: async def launch_go_no_go_post(flags: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return decide_go_no_go(statuses=payload.get("statuses")) return evaluate_go_no_go(flags or {})
@router.get("/readiness") @router.get("/scorecard")
async def readiness() -> dict[str, Any]: async def launch_scorecard() -> dict[str, Any]:
"""Readiness with all gates assumed False (use POST /go-no-go for real status).""" return build_launch_scorecard()
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)

View File

@ -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 from __future__ import annotations
@ -6,65 +6,32 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.meeting_intelligence import ( from auto_client_acquisition.meeting_intelligence.followup_builder import build_post_meeting_followup
build_post_meeting_followup, from auto_client_acquisition.meeting_intelligence.meeting_brief import build_pre_meeting_brief
build_pre_meeting_brief, from auto_client_acquisition.meeting_intelligence.objection_extractor import extract_objections
compute_deal_risk, from auto_client_acquisition.meeting_intelligence.transcript_parser import summarize_transcript_text
extract_objections,
parse_transcript_entries,
summarize_meeting,
)
router = APIRouter(prefix="/api/v1/meeting-intelligence", tags=["meeting-intelligence"]) 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.post("/transcript/summarize") @router.post("/transcript/summarize")
async def transcript_summarize(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def transcript_summarize(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
parsed = parse_transcript_entries(payload.get("entries") or payload.get("text", "")) text = str(payload.get("text") or "")
summary = summarize_meeting(parsed) base = summarize_transcript_text(text)
objections = extract_objections( base["objections"] = extract_objections(text)
" ".join(t["text"] for t in parsed.get("speaker_turns", [])) return base
)
return {"parsed": parsed, "summary": summary, "objections": objections}
@router.post("/followup/draft") @router.post("/followup/draft")
async def followup_draft(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def followup_draft(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_post_meeting_followup( summary = str(payload.get("summary_ar") or "")
summary=payload.get("summary"), steps = payload.get("next_steps") if isinstance(payload.get("next_steps"), list) else None
next_steps=payload.get("next_steps", []), return build_post_meeting_followup(summary, steps)
contact_name=payload.get("contact_name", ""),
company_name=payload.get("company_name", ""),
objections=payload.get("objections", []),
)
@router.post("/deal-risk") @router.post("/brief/pre-meeting")
async def deal_risk(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def pre_meeting_brief(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return compute_deal_risk( company = payload.get("company") if isinstance(payload.get("company"), dict) else {}
objections=payload.get("objections", []), contact = payload.get("contact") if isinstance(payload.get("contact"), dict) else {}
next_step_set=bool(payload.get("next_step_set", False)), opportunity = payload.get("opportunity") if isinstance(payload.get("opportunity"), dict) else {}
decision_maker_present=bool(payload.get("decision_maker_present", False)), return build_pre_meeting_brief(company, contact, opportunity)
days_since_last_touch=int(payload.get("days_since_last_touch", 0)),
expected_value_sar=float(payload.get("expected_value_sar", 0)),
)

View File

@ -1,4 +1,4 @@
"""Model Router router — task routing + provider registry + cost class.""" """Model routing API — configuration hints only."""
from __future__ import annotations from __future__ import annotations
@ -6,57 +6,22 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.model_router import ( from auto_client_acquisition.model_router.provider_registry import list_providers
ALL_PROVIDERS, from auto_client_acquisition.model_router.task_router import list_tasks, route_task
ALL_TASK_TYPES,
build_usage_demo,
classify_cost,
route_task,
)
router = APIRouter(prefix="/api/v1/model-router", tags=["model-router"]) 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.get("/tasks") @router.get("/tasks")
async def tasks() -> dict[str, Any]: async def tasks() -> dict[str, Any]:
return {"total": len(ALL_TASK_TYPES), "tasks": list(ALL_TASK_TYPES)} return list_tasks()
@router.post("/route") @router.post("/route")
async def route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
decision = route_task( return route_task(str(payload.get("task_type") or ""))
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()
@router.post("/cost-class") @router.get("/providers")
async def cost_class(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def providers() -> dict[str, Any]:
return { return list_providers()
"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()

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
from fastapi import APIRouter, Body, Query from fastapi import APIRouter, Body
from auto_client_acquisition.platform_services import ( from auto_client_acquisition.platform_services import (
ALL_CHANNELS, build_proof_summary,
POLICY_RULES,
SELLABLE_SERVICES,
build_card_from_event,
build_demo_feed,
build_demo_platform_proof,
evaluate_action, evaluate_action,
get_channel, event_to_inbox_card,
invoke_tool, execute_tool,
list_services, get_action_ledger,
make_event, get_service_catalog,
resolve_identity, list_channels,
validate_event,
) )
from auto_client_acquisition.platform_services.action_ledger import ActionLedger from auto_client_acquisition.innovation.proof_ledger import build_demo_proof_ledger
from auto_client_acquisition.platform_services.channel_registry import channels_summary 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"]) router = APIRouter(prefix="/api/v1/platform", tags=["platform_services"])
_LEDGER = ActionLedger()
@router.get("/service-catalog")
async def service_catalog() -> dict[str, Any]:
return get_service_catalog()
# ── Catalog ────────────────────────────────────────────────────
@router.get("/services/catalog") @router.get("/services/catalog")
async def services_catalog() -> dict[str, Any]: async def services_catalog_alias() -> dict[str, Any]:
return list_services() """Alias path for product docs compatibility."""
return get_service_catalog()
@router.get("/channels") @router.get("/channels")
async def channels() -> dict[str, Any]: async def channels() -> dict[str, Any]:
return { return list_channels()
"summary": channels_summary(),
"channels": [
{
"key": c.key, "label_ar": c.label_ar, "label_en": c.label_en,
"capabilities": list(c.capabilities), "beta_status": c.beta_status,
"required_permissions": list(c.required_permissions),
"allowed_actions": list(c.allowed_actions),
"blocked_actions": list(c.blocked_actions),
"risk_level": c.risk_level, "notes_ar": c.notes_ar,
}
for c in ALL_CHANNELS
],
}
@router.get("/channels/{channel_key}") @router.post("/events/validate")
async def channel_detail(channel_key: str) -> dict[str, Any]: async def events_validate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
c = get_channel(channel_key) return validate_event(payload or {})
if c is None:
return {"error": f"unknown channel: {channel_key}"}
return {
"key": c.key, "label_ar": c.label_ar, "label_en": c.label_en,
"capabilities": list(c.capabilities), "beta_status": c.beta_status,
"required_permissions": list(c.required_permissions),
"allowed_actions": list(c.allowed_actions),
"blocked_actions": list(c.blocked_actions),
"risk_level": c.risk_level, "notes_ar": c.notes_ar,
}
# ── Policy ───────────────────────────────────────────────────── @router.post("/events/ingest")
@router.get("/policy/rules") async def events_ingest(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
async def policy_rules() -> dict[str, Any]: """Validate normalized event and return inbox card — no persistence."""
return {"count": len(POLICY_RULES), "rules": POLICY_RULES} v = validate_event(payload or {})
if not v["valid"]:
return {"ok": False, "errors": v["errors"], "approval_required": True}
@router.post("/actions/evaluate") ev = v.get("normalized") or {}
async def actions_evaluate( return {"ok": True, "event": ev, "card": event_to_inbox_card(ev), "approval_required": True}
action: str = Body(..., embed=True),
context: dict[str, Any] = Body(default_factory=dict, embed=True),
) -> dict[str, Any]:
d = evaluate_action(action=action, context=context)
return {
"decision": d.decision,
"matched_rule_id": d.matched_rule_id,
"reasons_ar": d.reasons_ar,
"suggested_next_action_ar": d.suggested_next_action_ar,
}
@router.post("/actions/approve") @router.post("/actions/approve")
async def actions_approve( async def actions_approve(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
customer_id: str = Body(..., embed=True), """Record human approval/rejection in the in-memory action ledger — no live side effects."""
action_type: str = Body(..., embed=True), ledger = get_action_ledger()
channel: str = Body(..., embed=True), action_id = str(payload.get("action_id") or payload.get("request_id") or "unspecified")
actor: str = Body(default="user", embed=True), actor = str(payload.get("actor") or "operator")
payload: dict[str, Any] = Body(default_factory=dict, embed=True), approved = payload.get("approved")
correlation_id: str | None = Body(default=None, embed=True), is_approved = True if approved is None else bool(approved)
) -> dict[str, Any]: entry = ledger.append_decision(
entry = _LEDGER.append( tool="human_approval",
customer_id=customer_id, outcome="approved" if is_approved else "rejected",
action_type=action_type, detail={
channel=channel, "action_id": action_id,
stage="approved", "actor": actor,
actor=actor, "notes": payload.get("notes"),
payload=payload, },
correlation_id=correlation_id,
) )
return {"approved": True, "entry": entry.to_dict()}
@router.get("/ledger/summary")
async def ledger_summary(customer_id: str = Query(...)) -> dict[str, Any]:
return _LEDGER.summary(customer_id=customer_id)
# ── Events + Inbox ─────────────────────────────────────────────
@router.post("/events/ingest")
async def events_ingest(
event_type: str = Body(..., embed=True),
channel: str = Body(..., embed=True),
customer_id: str = Body(..., embed=True),
payload: dict[str, Any] = Body(default_factory=dict, embed=True),
) -> dict[str, Any]:
try:
evt = make_event(
event_type=event_type, channel=channel,
customer_id=customer_id, payload=payload,
)
except ValueError as exc:
return {"error": str(exc)}
card = build_card_from_event(evt)
return { return {
"event": evt.to_dict(), "ok": True,
"card": card.to_dict() if card else None, "ledger_entry": entry,
"actionable": card is not None, "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") @router.get("/inbox/feed")
async def inbox_feed() -> dict[str, Any]: async def inbox_feed() -> dict[str, Any]:
"""Demo unified-inbox feed; production version reads from event store.""" return build_inbox_feed()
return build_demo_feed()
# ── Identity + Tool gateway ─────────────────────────────────── @router.post("/contacts/import-preview")
@router.post("/identity/resolve") async def contacts_import_preview(body: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
async def identity_resolve( return build_import_preview(body or {})
signals: list[dict[str, Any]] = Body(..., embed=True),
) -> dict[str, Any]:
out = resolve_identity(signals=signals)
return {
"identity_id": out.identity_id,
"primary_phone": out.primary_phone,
"primary_email": out.primary_email,
"company": out.company,
"crm_id": out.crm_id,
"social_handles": out.social_handles,
"confidence": out.confidence,
"sources": out.sources,
}
@router.get("/identity/resolve-demo") @router.get("/action-ledger/recent")
async def identity_resolve_demo() -> dict[str, Any]: async def action_ledger_recent(limit: int = 50) -> dict[str, Any]:
"""Sample multi-source identity resolution.""" lim = max(1, min(limit, 200))
out = resolve_identity(signals=[ return {"entries": get_action_ledger().recent(lim)}
{"phone": "+966500000001", "company": "شركة العقار الذهبي", "source": "whatsapp"},
{"email": "ali@example.sa", "company": "شركة العقار الذهبي", "source": "gmail"},
{"crm_id": "crm_5421", "company": "شركة العقار الذهبي", "source": "crm"},
{"social_handles": {"linkedin": "ali-realestate"}, "source": "linkedin_lead_forms"},
])
return {
"identity_id": out.identity_id,
"primary_phone": out.primary_phone,
"primary_email": out.primary_email,
"company": out.company,
"crm_id": out.crm_id,
"social_handles": out.social_handles,
"confidence": out.confidence,
"sources": out.sources,
}
@router.post("/tools/invoke") @router.post("/ingest/lead-form")
async def tools_invoke( async def ingest_lead_form_route(body: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
tool: str = Body(..., embed=True), return ingest_lead_form(body or {})
payload: dict[str, Any] = Body(default_factory=dict, embed=True),
context: dict[str, Any] = Body(default_factory=dict, embed=True),
) -> dict[str, Any]:
r = invoke_tool(tool=tool, payload=payload, context=context)
return {
"status": r.status,
"tool": r.tool,
"matched_policy_rule": r.matched_policy_rule,
"reasons_ar": r.reasons_ar,
"next_action_ar": r.next_action_ar,
}
# ── Proof ────────────────────────────────────────────────────── # --- Wave 4: draft payloads only (re-export from aca.integrations) ---
@router.get("/proof-ledger/demo")
async def proof_ledger_demo() -> dict[str, Any]:
return build_demo_platform_proof().to_dict() @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 {})

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Query
from auto_client_acquisition.revenue_launch import ( from auto_client_acquisition.revenue_launch.demo_closer import (
build_24h_delivery_plan, build_12_min_demo_flow,
build_499_pilot_offer, build_close_script,
build_case_study_free_offer, build_discovery_questions,
build_client_intake_form, build_objection_responses,
build_client_summary, )
build_first_10_opportunities_delivery, from auto_client_acquisition.revenue_launch.offer_i18n import build_revenue_offers_payload
build_first_20_segments_v2, from auto_client_acquisition.revenue_launch.outreach_sequence import (
build_followup_1, build_first_20_segments,
build_followup_2, build_outreach_message,
build_growth_diagnostic_delivery, )
build_growth_os_pilot_offer, from auto_client_acquisition.revenue_launch.payment_manual_flow import (
build_list_intelligence_delivery,
build_moyasar_invoice_instructions, build_moyasar_invoice_instructions,
build_next_step_recommendation,
build_outreach_message_v2,
build_payment_confirmation_checklist, build_payment_confirmation_checklist,
build_payment_link_message, 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_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("/offer")
@router.get("/offers") 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]:
async def offers() -> dict[str, Any]: return build_revenue_offers_payload(lang)
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.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") @router.get("/outreach/first-20")
async def outreach_first_20() -> dict[str, Any]: async def revenue_launch_outreach_first_20() -> dict[str, Any]:
seg = build_first_20_segments_v2() segs = build_first_20_segments()
return { samples = [
**seg, build_outreach_message("agency_b2b"),
"messages": { build_outreach_message("training"),
s["id"]: build_outreach_message_v2(s["id"]) ]
for s in seg["segments"] return {**segs, "sample_messages": samples, "demo": True}
},
"reply_handlers": build_reply_handlers_v2(),
}
@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") @router.get("/demo-flow")
async def demo_flow() -> dict[str, Any]: async def revenue_launch_demo_flow() -> dict[str, Any]:
return { return {
"flow": demo_12_min(), "flow": build_12_min_demo_flow(),
"discovery_questions": demo_discovery(), "discovery": build_discovery_questions(),
"objections": demo_objections(), "close": build_close_script(),
"close": demo_close_script(), "objections": build_objection_responses(),
"demo": True,
} }
# ── Pipeline ─────────────────────────────────────────────────
@router.get("/pipeline/schema") @router.get("/pipeline/schema")
async def pipeline_schema() -> dict[str, Any]: async def revenue_launch_pipeline_schema() -> dict[str, Any]:
return build_pipeline_schema() return build_pipeline_schema()
@router.post("/pipeline/summarize") @router.get("/pilot-delivery")
async def pipeline_summarize( async def revenue_launch_pilot_delivery() -> dict[str, Any]:
pipeline: list[dict[str, Any]] = Body(default_factory=list, embed=True), return {
) -> dict[str, Any]: "intake": build_client_intake_form(),
return summarize_pipeline(pipeline) "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("/payment/manual-flow")
@router.get("/pilot-delivery/intake-form") async def revenue_launch_payment_manual() -> dict[str, Any]:
async def pilot_intake_form() -> dict[str, Any]: return {
return build_client_intake_form() "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") @router.get("/proof-pack/template")
async def pilot_24h_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def revenue_launch_proof_pack_template() -> dict[str, Any]:
return build_24h_delivery_plan(payload.get("service_id", "")) return build_private_beta_proof_pack()
@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)

View File

@ -112,6 +112,17 @@ from auto_client_acquisition.revenue_graph.why_now import (
explain_why_now, 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"]) router = APIRouter(prefix="/api/v1/revenue-os", tags=["revenue-os"])
log = logging.getLogger(__name__) 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 # Helpers
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────

View File

@ -1,4 +1,4 @@
"""Security Curator router — secret redaction + diff inspection.""" """Security curator API — redact and inspect diffs."""
from __future__ import annotations from __future__ import annotations
@ -6,50 +6,32 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.security_curator import ( from auto_client_acquisition.security_curator.patch_firewall import inspect_diff
inspect_diff, from auto_client_acquisition.security_curator.secret_redactor import redact_secrets, scan_payload
redact_trace, from auto_client_acquisition.security_curator.trace_redactor import redact_trace_payload
sanitize_tool_output,
scan_payload,
)
router = APIRouter(prefix="/api/v1/security-curator", tags=["security-curator"]) router = APIRouter(prefix="/api/v1/security-curator", tags=["security_curator"])
@router.get("/demo") @router.get("/demo")
async def demo() -> dict[str, Any]: async def demo() -> dict[str, Any]:
"""Run the redactor against a synthetic payload (deterministic, no network).""" return {"ok": True, "message_ar": "طبقة أمان للوكلاء — redaction وفحص فرق قبل التطبيق.", "demo": True}
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,
}
@router.post("/redact") @router.post("/redact")
async def redact(payload: Any = Body(...)) -> dict[str, Any]: async def redact(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
"""Redact secrets + PII from arbitrary JSON payload.""" text = str(payload.get("text") or "")
return redact_trace(payload) return {"redacted": redact_secrets(text), "findings": scan_payload(payload)}
@router.post("/inspect-diff") @router.post("/inspect-diff")
async def inspect_diff_endpoint( async def inspect_diff_route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
diff: str = Body(..., embed=True), diff = str(payload.get("diff_text") or "")
) -> dict[str, Any]: return inspect_diff(diff)
"""Inspect a unified diff for blocked files + secret patterns."""
return inspect_diff(diff).to_dict()
@router.post("/sanitize-output") @router.post("/trace/sanitize")
async def sanitize_output(payload: Any = Body(...)) -> dict[str, Any]: async def trace_sanitize(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
"""Sanitize a tool output before logging or showing it to a human.""" """Redact nested trace/span metadata before export to observability backends."""
return sanitize_tool_output(payload) body = payload.get("payload") if isinstance(payload.get("payload"), dict) else payload
return {"sanitized": redact_trace_payload(body or {}), "demo": True}

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter
from auto_client_acquisition.service_excellence import ( from auto_client_acquisition.service_excellence.competitor_gap import compare_against_categories
build_backlog, 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_demo_script,
build_feature_matrix,
build_landing_page_outline, build_landing_page_outline,
build_monthly_service_review,
build_onboarding_checklist, build_onboarding_checklist,
build_proof_pack_template_excellence,
build_sales_script, build_sales_script,
build_service_launch_package, build_service_launch_package,
build_service_research_brief, )
calculate_service_excellence_score, from auto_client_acquisition.service_excellence.proof_metrics import (
calculate_service_roi_estimate, build_proof_pack_template,
classify_features,
compare_against_categories,
convert_feedback_to_backlog,
generate_feature_hypotheses,
prioritize_backlog_items,
recommend_missing_features,
recommend_next_experiments,
recommend_weekly_improvements,
required_proof_metrics, required_proof_metrics,
review_service_before_launch,
summarize_proof_ar, 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"]) 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.get("/review/all") @router.get("/review/all")
async def review_all() -> dict[str, Any]: async def review_all() -> dict[str, Any]:
"""Review every catalogued service.""" return review_all_services()
out = [review_service_before_launch(s.id) for s in ALL_SERVICES]
counts: dict[str, int] = {}
for r in out: @router.get("/{service_id}/feature-matrix")
v = str(r.get("verdict", "?")) async def feature_matrix(service_id: str) -> dict[str, Any]:
counts[v] = counts.get(v, 0) + 1 fm = build_feature_matrix(service_id)
return {"total": len(out), "by_verdict": counts, "results": out} 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") @router.get("/{service_id}/proof-metrics")
async def proof_metrics(service_id: str) -> dict[str, Any]: async def proof_metrics(service_id: str) -> dict[str, Any]:
return { return {
"service_id": service_id, "required": required_proof_metrics(service_id),
"metrics": required_proof_metrics(service_id), "template": build_proof_pack_template(service_id),
"template": build_proof_pack_template_excellence(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") @router.get("/{service_id}/gap-analysis")
async def gap_analysis(service_id: str) -> dict[str, Any]: async def gap_analysis(service_id: str) -> dict[str, Any]:
return compare_against_categories(service_id) 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") @router.get("/{service_id}/research-brief")
async def research_brief(service_id: str) -> dict[str, Any]: async def research_brief(service_id: str) -> dict[str, Any]:
return build_service_research_brief(service_id) return build_service_research_brief(service_id)
@router.get("/{service_id}/feature-hypotheses") @router.get("/{service_id}/review")
async def feature_hypotheses(service_id: str) -> dict[str, Any]: async def review_one(service_id: str) -> dict[str, Any]:
return {"hypotheses": generate_feature_hypotheses(service_id)} return review_service_before_launch(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)

View File

@ -1,4 +1,4 @@
"""Service Tower router — كتالوج الخدمات + wizard + workflow + pricing + cards.""" """Service Tower API — sellable services wizard (no live send)."""
from __future__ import annotations from __future__ import annotations
@ -6,85 +6,122 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.service_tower import ( from auto_client_acquisition.platform_services.service_catalog import get_service_catalog
build_ceo_daily_service_brief, from auto_client_acquisition.service_tower.deliverables import (
build_client_report_outline, build_client_report_outline,
build_deliverables, build_deliverables,
build_end_of_day_service_report,
build_intake_questions,
build_internal_operator_checklist, build_internal_operator_checklist,
build_proof_pack_template, build_proof_pack_template,
build_risk_alert_card, )
build_service_approval_card, from auto_client_acquisition.service_tower.mission_templates import build_service_workflow
build_service_scorecard, from auto_client_acquisition.service_tower.pricing_engine import (
build_service_workflow,
build_upsell_message_ar,
calculate_monthly_offer, calculate_monthly_offer,
calculate_setup_fee, calculate_setup_fee,
catalog_summary,
get_service,
list_all_services,
map_service_to_growth_mission,
map_service_to_subscription,
quote_service, quote_service,
recommend_next_step,
recommend_plan_after_service, 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_service,
recommend_upgrade, start_service,
summarize_recommendation_ar, summarize_recommendation_ar,
summarize_scorecard_ar,
validate_service_inputs, 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") @router.get("/catalog")
async def catalog() -> dict[str, Any]: async def services_catalog() -> dict[str, Any]:
return list_all_services() tower = list_tower_services()
platform = get_service_catalog()
return {
@router.get("/summary") "tower": tower,
async def summary() -> dict[str, Any]: "platform_service_catalog": platform,
return catalog_summary() "note_ar": "برج الخدمات (تفصيل بيع) + كتالوج المنصة (طبقة تقنية) — يُدمجان للعرض.",
"demo": True,
}
@router.post("/recommend") @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( rec = recommend_service(
company_type=payload.get("company_type", ""), company_type=str(p.get("company_type") or ""),
goal=payload.get("goal", "fill_pipeline"), goal=str(p.get("goal") or ""),
has_contact_list=bool(payload.get("has_contact_list", False)), has_contact_list=bool(p.get("has_contact_list")),
channels=payload.get("channels", []), channels=list(p.get("channels") or []),
budget_sar=int(payload.get("budget_sar", 1000)), budget_sar=p.get("budget_sar"),
) )
rec["summary_ar"] = summarize_recommendation_ar(rec) rec["summary_ar"] = summarize_recommendation_ar(rec)
return rec return rec
# ── Per-service ────────────────────────────────────────────── @router.post("/start")
@router.get("/{service_id}/intake-questions") async def services_start(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
async def service_intake_questions(service_id: str) -> dict[str, Any]: p = payload or {}
return build_intake_questions(service_id) return start_service(str(p.get("service_id") or ""), dict(p.get("payload") or p))
@router.post("/{service_id}/start") @router.get("/demo/dashboard")
async def service_start( async def services_demo_dashboard() -> dict[str, Any]:
service_id: str, ids = [s["service_id"] for s in list_tower_services().get("services") or []][:5]
payload: dict[str, Any] = Body(...), cards = []
) -> dict[str, Any]: for sid in ids:
validation = validate_service_inputs(service_id, payload) svc = get_service_by_id(sid)
if not validation["valid"]: cards.append(
return {"started": False, "validation": validation} {
workflow = build_service_workflow(service_id) "service_id": sid,
return { "name_ar": (svc or {}).get("name_ar"),
"started": True, "deliverables": build_deliverables(sid),
"validation": validation, "scorecard": build_service_scorecard(
"workflow": workflow, sid,
"linked_growth_mission": map_service_to_growth_mission(service_id), {"drafts_created": 2, "approvals": 1, "meetings_booked": 0, "risks_blocked": 3},
"approval_required": True, ),
} }
)
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") @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) 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") @router.post("/{service_id}/quote")
async def service_quote( async def service_quote(
service_id: str, service_id: str,
payload: dict[str, Any] = Body(default_factory=dict), payload: dict[str, Any] = Body(default_factory=dict),
) -> dict[str, Any]: ) -> dict[str, Any]:
return quote_service( p = payload or {}
q = quote_service(
service_id, service_id,
company_size=payload.get("company_size", "small"), company_size=str(p.get("company_size") or "smb"),
urgency=payload.get("urgency", "normal"), urgency=str(p.get("urgency") or "normal"),
channels_count=int(payload.get("channels_count", 1)), 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") @router.get("/{service_id}/intake-questions")
async def service_setup_fee(service_id: str) -> dict[str, Any]: async def intake_questions(service_id: str) -> dict[str, Any]:
return calculate_setup_fee(service_id) return build_intake_questions(service_id)
@router.get("/{service_id}/monthly-offer") @router.post("/{service_id}/validate")
async def service_monthly_offer(service_id: str) -> dict[str, Any]: async def validate_inputs(service_id: str, payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return calculate_monthly_offer(service_id) return validate_service_inputs(service_id, payload or {})
@router.post("/{service_id}/scorecard") @router.get("/{service_id}/deliverables")
async def service_scorecard( async def service_deliverables(service_id: str) -> dict[str, Any]:
service_id: str, return {
metrics: dict[str, Any] = Body(default_factory=dict), "deliverables": build_deliverables(service_id),
) -> dict[str, Any]: "proof_pack": build_proof_pack_template(service_id),
return build_service_scorecard(service_id, metrics) "client_report": build_client_report_outline(service_id),
"operator_checklist": build_internal_operator_checklist(service_id),
"demo": True,
}
@router.get("/{service_id}/upgrade-path") @router.get("/{service_id}/upgrade")
async def service_upgrade_path(service_id: str) -> dict[str, Any]: async def service_upgrade(service_id: str) -> dict[str, Any]:
return recommend_upgrade(service_id) 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()

View File

@ -1,4 +1,4 @@
"""Targeting & Acquisition OS router.""" """Targeting & Acquisition OS API — planning and evaluation only, no live send."""
from __future__ import annotations from __future__ import annotations
@ -6,235 +6,128 @@ from typing import Any
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from auto_client_acquisition.targeting_os import ( from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
analyze_uploaded_list_preview, from auto_client_acquisition.platform_services.contact_import_preview import build_import_preview
build_dealix_self_growth_plan, from auto_client_acquisition.targeting_os.account_finder import recommend_accounts, recommend_account_source_strategy
build_daily_targeting_brief, from auto_client_acquisition.targeting_os.acquisition_scorecard import build_acquisition_scorecard
build_end_of_day_report, from auto_client_acquisition.targeting_os.buyer_role_mapper import map_buying_committee
build_followup_sequence, 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, build_free_growth_diagnostic,
recommend_paid_pilot_offer,
)
from auto_client_acquisition.targeting_os.linkedin_strategy import (
build_lead_gen_form_plan, 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_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") @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( return recommend_accounts(
sector=payload.get("sector", "saas"), str(payload.get("sector") or ""),
city=payload.get("city", "Riyadh"), str(payload.get("city") or ""),
offer=payload.get("offer", ""), str(payload.get("offer") or ""),
goal=payload.get("goal", "fill_pipeline"), str(payload.get("goal") or ""),
limit=int(payload.get("limit", 10)), limit=int(payload.get("limit") or 10),
) )
# ── Buying committee ─────────────────────────────────────────
@router.post("/buying-committee/map") @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( return map_buying_committee(
sector=payload.get("sector", "saas"), str(payload.get("sector") or ""),
company_size=payload.get("company_size", "small"), payload.get("company_size"),
goal=payload.get("goal", "fill_pipeline"), payload.get("goal"),
) )
# ── Contacts ─────────────────────────────────────────────────
@router.post("/contacts/evaluate") @router.post("/contacts/evaluate")
async def contacts_evaluate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def contacts_evaluate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
contact = payload.get("contact") or payload contact = payload.get("contact") if isinstance(payload.get("contact"), dict) else payload
desired = payload.get("desired_channel") desired = payload.get("desired_channel")
result = evaluate_contactability(contact, desired_channel=desired) return evaluate_contactability(contact, str(desired) if desired else None)
result["explanation_ar"] = explain_contactability_ar(result)
return result
@router.post("/uploaded-list/analyze") @router.post("/uploaded-list/analyze")
async def uploaded_list_analyze( async def uploaded_list_analyze(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
contacts: list[dict[str, Any]] = Body(..., embed=True), """Delegates to platform import preview for full bucket logic."""
) -> dict[str, Any]: return build_import_preview(payload or {})
return analyze_uploaded_list_preview(contacts)
# ── Outreach ─────────────────────────────────────────────────
@router.post("/outreach/plan") @router.post("/outreach/plan")
async def outreach_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def outreach_plan(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
plan = build_outreach_plan( targets = payload.get("targets") if isinstance(payload.get("targets"), list) else []
targets=payload.get("targets", []), channels = payload.get("channels") if isinstance(payload.get("channels"), list) else ["email"]
channels=payload.get("channels"), goal = str(payload.get("goal") or "growth")
goal=payload.get("goal", "fill_pipeline"), return build_outreach_plan([dict(t) for t in targets if isinstance(t, dict)], [str(c) for c in channels], goal)
)
plan = enforce_daily_limits(plan)
plan["summary_ar"] = summarize_plan_ar(plan)
return plan
# ── Daily autopilot ──────────────────────────────────────────
@router.get("/daily-autopilot/demo") @router.get("/daily-autopilot/demo")
async def daily_autopilot_demo() -> dict[str, Any]: async def daily_autopilot_demo() -> dict[str, Any]:
return { return build_daily_targeting_brief({"sector": "training", "city": "الرياض", "offer": "Growth OS", "goal": "meetings"})
"brief": build_daily_targeting_brief(),
"today_actions": recommend_today_actions(),
"end_of_day_template": build_end_of_day_report(),
}
# ── Self-Growth Mode ─────────────────────────────────────────
@router.get("/self-growth/demo") @router.get("/self-growth/demo")
async def self_growth_demo() -> dict[str, Any]: async def self_growth_demo() -> dict[str, Any]:
return { return build_self_growth_daily_brief()
"plan": build_dealix_self_growth_plan(),
"today": 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") @router.get("/reputation/status")
async def reputation_status() -> dict[str, Any]: async def reputation_status() -> dict[str, Any]:
"""Demo reputation snapshot.""" metrics = {"bounce_rate": 0.12, "opt_out_rate": 0.01, "complaint_rate": 0.0, "reply_rate": 0.08}
healthy_email = {"bounce_rate": 0.005, "complaint_rate": 0.0001, rep = calculate_channel_reputation(metrics)
"opt_out_rate": 0.01, "reply_rate": 0.04} return {**rep, "should_pause": should_pause_channel(metrics)}
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"),
}
@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") @router.post("/linkedin/strategy")
async def linkedin_strategy(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def linkedin_strategy(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
strategy = recommend_linkedin_strategy( seg = str(payload.get("segment") or "b2b")
segment=payload.get("segment", "B2B Saudi"), goal = str(payload.get("goal") or "leads")
goal=payload.get("goal", "fill_pipeline"), base = recommend_linkedin_strategy(seg, goal)
) if payload.get("include_lead_gen_plan"):
if payload.get("with_lead_gen_form"): base["lead_gen_plan"] = build_lead_gen_form_plan(
strategy["lead_gen_form_plan"] = build_lead_gen_form_plan( seg,
segment=payload.get("segment", "B2B Saudi"), str(payload.get("offer") or "Pilot"),
offer=payload.get("offer", "Pilot 7 days"), str(payload.get("campaign_name") or "dealix"),
campaign_name=payload.get("campaign_name", ""),
) )
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") @router.get("/services")
async def services_list() -> dict[str, Any]: async def targeting_services() -> dict[str, Any]:
return list_targeting_services() return list_targeting_services()
@router.post("/services/recommend") @router.post("/free-diagnostic")
async def services_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def free_diagnostic(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return recommend_service_offer( company = payload.get("company") if isinstance(payload.get("company"), dict) else payload
customer_type=payload.get("customer_type", ""), if not isinstance(company, dict):
goal=payload.get("goal", "fill_pipeline"), 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") @router.get("/contracts/templates")
async def contracts_templates() -> dict[str, Any]: async def contracts_templates() -> dict[str, Any]:
return { return list_contract_templates()
"pilot": draft_pilot_agreement_outline(),
"dpa": draft_dpa_outline(),
"referral": draft_referral_agreement_outline(), @router.post("/trust-score")
"agency_partner": draft_agency_partner_outline(), 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)

View File

@ -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 __all__ = ["build_trace_event", "evaluate_safety", "evaluate_saudi_tone"]
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",
]

View File

@ -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 __future__ import annotations
from typing import Any EVAL_CASES: tuple[str, ...] = ("no_guarantee_language", "no_payment_collect", "has_single_cta")
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,
}

View File

@ -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 from __future__ import annotations
import re import re
from typing import Any
# Each tuple: (category, regex, Arabic reason, severity_points 0..50) _BAD = ("ضمان كامل", "مضمون 100%", "ارسل لي رقم البطاقة", "كلمة المرور", "حسابك معلق")
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),
)
def safety_eval(text: str) -> dict[str, object]: def evaluate_safety(text_ar: str) -> dict[str, Any]:
""" t = text_ar or ""
Evaluate a message for safety violations. trips: list[str] = []
for phrase in _BAD:
Returns: if phrase in t:
{ trips.append(phrase)
"score": int 0..100 (100 = perfectly safe), if re.search(r"\b\d{16}\b", t):
"verdict": "safe" | "needs_review" | "blocked", trips.append("possible_pan")
"violations": [{"category", "reason_ar"}], return {"passed": len(trips) == 0, "tripwires": trips, "demo": True}
}
"""
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}

View File

@ -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 from __future__ import annotations
import re import re
from typing import Any
# 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",
"نفخر بأن نقدم لكم",
)
def _arabic_ratio(text: str) -> float: def evaluate_saudi_tone(text_ar: str) -> dict[str, Any]:
if not text: t = (text_ar or "").strip()
return 0.0 score = 65
arabic = sum(1 for ch in text if "؀" <= ch <= "ۿ") if re.search(r"(هل|ممكن|نقدّم|نرحب|شاكرين)", t):
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:
score += 10 score += 10
if len(t) > 600:
score -= 10
if "!!!" in t or "؟؟؟" in t:
score -= 8
score = max(0, min(100, score)) score = max(0, min(100, score))
return {"tone_score": score, "demo": True}
# 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),
}

View File

@ -1,56 +1,35 @@
"""Build sanitized trace events for Langfuse/Sentry.""" """Structured trace event for dashboards (PII-redacted strings)."""
from __future__ import annotations from __future__ import annotations
import hashlib
import time import time
import uuid
from typing import Any from typing import Any
from auto_client_acquisition.security_curator import sanitize_trace_event from auto_client_acquisition.security_curator.trace_redactor import redact_trace_payload
def _hash_id(value: str | None) -> str | None:
if not value:
return None
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
def build_trace_event( def build_trace_event(
*, *,
workflow_name: str, workflow_name: str,
agent_name: str, agent_name: str,
status: str = "started", action_type: str,
user_id: str | None = None, policy_result: str,
company_id: str | None = None, tool_called: str | None = None,
tool: str | None = None, outcome: str | None = None,
policy_result: str | None = None, metadata: dict[str, Any] | 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,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" meta = metadata or {}
Build a sanitized trace event ready for Langfuse/Sentry. safe_meta = redact_trace_payload(meta)
return {
All payload/output fields go through the security_curator sanitizer. "trace_id": str(uuid.uuid4()),
User/company IDs are hashed before logging. "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"""
raw = {
"ts": time.time(),
"workflow_name": workflow_name, "workflow_name": workflow_name,
"agent_name": agent_name, "agent_name": agent_name,
"status": status, "action_type": action_type,
"user_id_hash": _hash_id(user_id),
"company_id_hash": _hash_id(company_id),
"tool": tool,
"policy_result": policy_result, "policy_result": policy_result,
"risk_level": risk_level, "tool_called": tool_called,
"approval_status": approval_status, "outcome": outcome,
"latency_ms": latency_ms, "metadata": safe_meta,
"cost_estimate": cost_estimate, "demo": True,
"payload": payload,
"output": output,
} }
return sanitize_trace_event(raw)

View File

@ -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, from auto_client_acquisition.autonomous_service_operator.conversation_router import handle_message
recommends a service, collects intake, runs workflow, requests approval, from auto_client_acquisition.autonomous_service_operator.service_bundles import get_bundle, list_bundles
delivers Proof Pack, suggests upgrade.
"""
from __future__ import annotations __all__ = ["handle_message", "list_bundles", "get_bundle"]
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",
]

View File

@ -1,133 +1,14 @@
"""Agency Mode — manage multiple clients + co-branded Proof Pack + revenue share.""" """Agency partner prioritization."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
def add_agency_client( def mode_profile() -> dict[str, Any]:
*,
agency_id: str,
client_company_name: str,
sector: str = "",
monthly_subscription_sar: int = 0,
revenue_share_pct: int = 20,
clients: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Add a new client to an agency's roster + return the entry."""
entry: dict[str, Any] = {
"agency_id": agency_id,
"client_company_name": client_company_name,
"sector": sector,
"monthly_subscription_sar": int(monthly_subscription_sar),
"revenue_share_pct": int(revenue_share_pct),
"status": "onboarding",
"co_branded_proof_pack": True,
"approval_required": True,
}
if clients is not None:
clients.append(entry)
return entry
def build_agency_dashboard(
*,
agency_id: str,
agency_name: str = "",
clients: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build the agency's dashboard summary."""
clients = clients or []
total_clients = len(clients)
active = sum(1 for c in clients if c.get("status") in ("active", "onboarding"))
monthly_revenue_total = sum(
float(c.get("monthly_subscription_sar", 0) or 0) for c in clients
)
avg_share_pct = (
round(
sum(int(c.get("revenue_share_pct", 0) or 0) for c in clients)
/ max(1, total_clients),
1,
)
if total_clients else 0.0
)
return { return {
"mode": "agency", "mode": "agency_partner",
"agency_id": agency_id, "priority_intents": ["want_partnerships", "ask_services", "ask_proof"],
"agency_name": agency_name, "card_types_first": ["opportunity", "proof_update", "compliance_risk"],
"metrics": { "demo": True,
"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,
} }

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
APPROVAL_STATES: tuple[str, ...] = ( from auto_client_acquisition.autonomous_service_operator import session_state as ss
"pending",
"approved",
"edited",
"rejected",
"expired",
)
def build_approval_card( def set_pending_approval(session_id: str, payload: dict[str, Any]) -> dict[str, Any]:
*, return ss.upsert_session(
action_type: str, session_id,
title_ar: str, {"workflow_state": "pending_approval", "pending_card": payload},
summary_ar: str, )
risk_level: str = "low",
why_now_ar: str = "",
recommended_action_ar: str = "",
expected_impact_sar: float = 0.0,
service_id: str | None = None,
customer_id: str | None = None,
action_id: str | None = None,
) -> dict[str, Any]:
"""Build a structured Arabic approval card."""
return {
"type": "approval",
"action_id": action_id,
"action_type": action_type,
"service_id": service_id,
"customer_id": customer_id,
"title_ar": title_ar[:140],
"summary_ar": summary_ar[:280],
"why_now_ar": why_now_ar[:200],
"recommended_action_ar": recommended_action_ar[:200],
"risk_level": risk_level if risk_level in (
"low", "medium", "high",
) else "medium",
"expected_impact_sar": float(expected_impact_sar),
"buttons_ar": ["اعتمد", "عدّل", "تخطي"],
"state": "pending",
"approval_required": True,
"live_send_allowed": False,
}
def process_approval_decision( def apply_decision(session_id: str, decision: str) -> dict[str, Any]:
card: dict[str, Any], d = (decision or "").strip().lower()
*, if d in ("approve", "اعتمد"):
decision: str, return ss.upsert_session(
decided_by: str = "user", session_id,
note: str = "", {"workflow_state": "approved", "pending_card": None, "last_decision": "approve"},
) -> dict[str, Any]: )
""" if d in ("edit", "تعديل"):
Process an approval decision (`approve` / `edit` / `skip` / `reject`). 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) def pending_card(session_id: str) -> dict[str, Any] | None:
out["state"] = new_state s = ss.get_session(session_id)
out["decided_by"] = decided_by if not s:
out["decision_note"] = note[:200] return None
out["next_action"] = next_action return s.get("pending_card") if isinstance(s.get("pending_card"), dict) else None
return out

View File

@ -1,55 +1,14 @@
"""Client Mode — dashboard for the customer (Growth Manager) view.""" """End-customer (growth manager) prioritization."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
def build_client_dashboard( def mode_profile() -> dict[str, Any]:
*,
customer_id: str = "",
company_name: str = "",
active_services: list[str] | None = None,
open_actions: int = 0,
proof_pack_due: bool = False,
) -> dict[str, Any]:
"""Build the client-facing dashboard."""
active_services = active_services or []
return { return {
"mode": "client", "mode": "client",
"customer_id": customer_id, "priority_intents": ["want_more_customers", "has_contact_list", "want_meetings"],
"company_name": company_name, "card_types_first": ["approval_needed", "opportunity", "proof_update"],
"active_services": list(active_services), "demo": True,
"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,
} }

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
from .approval_manager import ( from auto_client_acquisition.autonomous_service_operator import (
build_approval_card, approval_manager as am,
process_approval_decision, 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 def handle_message(session_id: str, message: str, mode: str = "client") -> dict[str, Any]:
INTENT_TO_HANDLER: dict[str, str] = { intent = ic.classify_intent(message)
"want_more_customers": "start_first_10_opportunities", om.append_turn(session_id, "user", message, {"intent": intent})
"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",
}
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]: rec = so.recommend_for_intent(intent)
"""Classify a message + return the routed handler + recommended service.""" ss.upsert_session(
classification = classify_intent(message) session_id,
intent = classification["intent"] {
handler = INTENT_TO_HANDLER.get(intent, "show_bundles") "last_intent": intent,
service_id = intent_to_service(intent) "recommended_service_id": rec["recommended_service_id"],
"mode": mode,
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,
) )
wr.advance(session_id, "start_service")
# If a service is recommended, build its initial pipeline + intake form. reply_ar = _build_reply_ar(intent, rec)
response: dict[str, Any] = { om.append_turn(session_id, "assistant", reply_ar, {"recommendation": rec})
return {
"session_id": session_id,
"intent": intent, "intent": intent,
"handler": handler, "recommendation": rec,
"bundle_recommendation": bundle_rec, "reply_ar": reply_ar,
"service_id": routed["recommended_service_id"], "bundles_hint": sb.list_bundles() if intent == ic.INTENT_ASK_SERVICES else None,
"approval_required": True, "demo": True,
"live_send_allowed": False,
} }
if intent in ("approve_action", "edit_action", "skip_action"):
# Approvals are handled separately — surface a placeholder card.
decision = (
"approve" if intent == "approve_action"
else "edit" if intent == "edit_action"
else "skip"
)
sample_card = build_approval_card(
action_type="example_action",
title_ar="فعل مثال",
summary_ar="هذا مثال على approval card",
)
response["decision_processed"] = process_approval_decision(
sample_card, decision=decision, decided_by=customer_id or "user",
)
return response
if routed["recommended_service_id"]: def _build_reply_ar(intent: str, rec: dict[str, Any]) -> str:
response["intake_questions"] = build_intake_questions_for_intent(intent) sid = rec.get("recommended_service_id")
response["initial_pipeline"] = build_service_pipeline( name = rec.get("service_name_ar") or sid
routed["recommended_service_id"], customer_id=customer_id or "", if intent == ic.INTENT_ASK_SERVICES:
return (
"أنسب مسار: ابدأ بتشخيص مجاني ثم اختر باقة Growth Starter أو Data to Revenue. "
"راجع قائمة الباقات من /api/v1/operator/bundles."
) )
if intent == ic.INTENT_ASK_PROOF:
return response return f"Proof Pack مرتبط بخدمة {name} — جاهز كعرض تجريبي بعد أول مسودات موافَق عليها."
return f"نوصي بخدمة: {name} ({sid}). الخطوة التالية: أكمل المدخلات ثم راجع المسودات قبل أي إرسال."

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
def build_executive_daily_brief( def mode_profile() -> dict[str, Any]:
*,
company_name: str = "",
sector: str = "saas",
) -> dict[str, Any]:
"""Build the CEO's daily brief (Arabic)."""
return { return {
"title_ar": f"موجز اليوم التنفيذي — {company_name or '(الشركة)'}", "mode": "executive",
"summary_ar": [ "priority_intents": ["want_more_customers", "ask_proof", "approve_action"],
f"3 قرارات تنتظر اعتمادك في قطاع {sector}.", "card_types_first": ["leak", "approval_needed", "opportunity"],
"5 رسائل drafts معدّة بـ Saudi tone.", "demo": True,
"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,
} }

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
# Intake questions per intent (Arabic). from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
_INTAKE_QUESTIONS_BY_INTENT: dict[str, list[dict[str, Any]]] = {
"want_more_customers": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "city", "label_ar": "المدينة", "required": True},
{"key": "offer", "label_ar": "العرض الرئيسي", "required": True},
{"key": "ideal_customer", "label_ar": "العميل المثالي",
"required": True},
],
"has_contact_list": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "list_size", "label_ar": "حجم القائمة (تقريباً)",
"required": True},
{"key": "list_source", "label_ar": "مصدر القائمة (CRM/event/upload)",
"required": True},
{"key": "channels_available", "label_ar": "القنوات المتاحة",
"required": True},
],
"want_partnerships": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "partner_goal",
"label_ar": "هدف الشراكة (وكالات/موزعين/co-marketing)",
"required": True},
{"key": "current_partners", "label_ar": "شركاء حاليين (إن وجد)",
"required": False},
],
"want_daily_growth": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "team_size", "label_ar": "حجم فريق المبيعات/النمو",
"required": True},
{"key": "channels", "label_ar": "القنوات الحالية", "required": True},
{"key": "approval_owner", "label_ar": "من يوافق على الرسائل؟",
"required": True},
],
"want_meetings": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "prospect_count", "label_ar": "عدد الـ prospects",
"required": True},
{"key": "calendar_link", "label_ar": "رابط Calendar (لو وُجد)",
"required": False},
],
"want_email_rescue": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "gmail_label",
"label_ar": "اسم الـ label/الـ folder المستهدف",
"required": True},
{"key": "ICP", "label_ar": "العميل المثالي", "required": True},
],
"want_whatsapp_setup": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "list_size",
"label_ar": "حجم قاعدة الواتساب الحالية", "required": True},
{"key": "current_practice",
"label_ar": "الطريقة الحالية في إرسال الرسائل", "required": True},
],
"ask_revenue_today": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "city", "label_ar": "المدينة", "required": True},
{"key": "offer", "label_ar": "العرض الرئيسي", "required": True},
],
# Default minimal intake for any "ask_*" intent.
"ask_services": [
{"key": "goal", "label_ar": "ما هدفك الأساسي؟", "required": True},
],
}
def build_intake_questions_for_intent(intent: str) -> dict[str, Any]: def intake_questions(service_id: str) -> dict[str, Any]:
"""Return intake questions for an intent. Falls back to ask_services.""" svc = get_service_by_id(service_id) or {}
questions = _INTAKE_QUESTIONS_BY_INTENT.get(intent) fields = svc.get("inputs_required") or []
if questions is None:
questions = _INTAKE_QUESTIONS_BY_INTENT["ask_services"]
return { return {
"intent": intent, "service_id": service_id,
"questions": [dict(q) for q in questions], "fields": [{"name": f, "prompt_ar": f"يرجى تزويدنا بـ: {f}"} for f in fields],
"estimated_minutes": max(2, len(questions) * 1), "demo": True,
"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),
} }

View File

@ -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 from __future__ import annotations
import re import re
from typing import Any from typing import Final
# 16 supported intents that drive the operator. # Intent ids consumed by service_orchestrator and conversation_router.
SUPPORTED_INTENTS: tuple[str, ...] = ( INTENT_WANT_MORE_CUSTOMERS: Final = "want_more_customers"
"want_more_customers", INTENT_HAS_CONTACT_LIST: Final = "has_contact_list"
"has_contact_list", INTENT_WANT_PARTNERSHIPS: Final = "want_partnerships"
"want_partnerships", INTENT_WANT_DAILY_GROWTH: Final = "want_daily_growth"
"want_daily_growth", INTENT_WANT_MEETINGS: Final = "want_meetings"
"want_meetings", INTENT_WANT_EMAIL_RESCUE: Final = "want_email_rescue"
"want_email_rescue", INTENT_WANT_WHATSAPP_SETUP: Final = "want_whatsapp_setup"
"want_whatsapp_setup", INTENT_ASK_PRICING: Final = "ask_pricing"
"ask_pricing", INTENT_APPROVE_ACTION: Final = "approve_action"
"approve_action", INTENT_EDIT_ACTION: Final = "edit_action"
"edit_action", INTENT_SKIP_ACTION: Final = "skip_action"
"skip_action", INTENT_ASK_DEMO: Final = "ask_demo"
"ask_demo", INTENT_ASK_PROOF: Final = "ask_proof"
"ask_proof", INTENT_ASK_SERVICES: Final = "ask_services"
"ask_services", INTENT_ASK_PARTNERSHIP: Final = "ask_partnership"
"ask_partnership", INTENT_ASK_REVENUE_TODAY: Final = "ask_revenue_today"
"ask_revenue_today", INTENT_COLD_WHATSAPP_REQUEST: Final = "cold_whatsapp_request" # blocked path
) INTENT_UNKNOWN: Final = "unknown"
# Each intent → (Arabic keywords, English keywords).
_KEYWORDS: dict[str, tuple[list[str], list[str]]] = {
"want_more_customers": (
["عملاء", "فرص", "leads", "ليدز", "عميل جديد", "مبيعات",
"أبغى عملاء", "زيادة عملاء"],
["customers", "leads", "more sales", "new clients", "pipeline"],
),
"has_contact_list": (
["قائمة", "أرقام", "إيميلات", "CSV", "قائمتي", "عملاء قدامى",
"اللستة", "ملف"],
["list", "csv", "old customers", "spreadsheet", "contacts"],
),
"want_partnerships": (
["شراكات", "شريك", "وكالة", "تعاون", "موزع", "شركاء"],
["partnership", "partner", "agency deal", "referral"],
),
"want_daily_growth": (
["تشغيل يومي", "نمو شهري", "Growth OS", "اشتراك", "يومياً",
"مدير نمو"],
["daily growth", "growth os", "subscription", "monthly"],
),
"want_meetings": (
["اجتماعات", "ديمو", "meeting", "موعد", "احجز", "مكالمة",
"demo"],
["meeting", "demo", "book", "schedule call"],
),
"want_email_rescue": (
["إيميل", "Gmail", "Outlook", "إنباكس", "بريد", "ضائعة"],
["email rescue", "inbox", "gmail", "missed emails"],
),
"want_whatsapp_setup": (
["واتساب", "WhatsApp", "opt-in", "حملة واتساب", "أرقامي"],
["whatsapp", "compliance", "opt-in"],
),
"ask_pricing": (
["السعر", "كم", "بكم", "تكلفة", "اشتراك"],
["price", "cost", "how much", "pricing"],
),
"approve_action": (
["اعتمد", "موافق", "وافق", "تمام", "نعم"],
["approve", "ok", "yes", "go ahead", "confirm"],
),
"edit_action": (
["عدّل", "تعديل", "غير", "بدّل"],
["edit", "change", "modify", "tweak"],
),
"skip_action": (
["تخطي", "تخطى", "تجاوز", "خطّي", "لا"],
["skip", "no", "pass", "later"],
),
"ask_demo": (
["ديمو", "عرض", "أشوف", "جرب", "تجربة"],
["demo", "try", "show me", "trial"],
),
"ask_proof": (
["proof", "نتائج", "case study", "إثبات", "تقرير"],
["proof", "results", "case study", "report"],
),
"ask_services": (
["الخدمات", "وش عندكم", "ماذا تقدمون", "العروض", "bundles"],
["services", "what do you offer", "bundles", "packages"],
),
"ask_partnership": (
["وكالة شريكة", "Agency Partner", "revenue share", "شراكة وكالة"],
["agency partner", "revenue share", "white label"],
),
"ask_revenue_today": (
["دخل اليوم", "أبيع اليوم", "اول pilot", "ابدأ اليوم"],
["revenue today", "sell today", "first pilot", "private beta"],
),
}
# Map intent → recommended service ID (in service_tower.service_catalog).
INTENT_TO_SERVICE: dict[str, str] = {
"want_more_customers": "first_10_opportunities_sprint",
"has_contact_list": "list_intelligence",
"want_partnerships": "partner_sprint",
"want_daily_growth": "growth_os_monthly",
"want_meetings": "meeting_booking_sprint",
"want_email_rescue": "email_revenue_rescue",
"want_whatsapp_setup": "whatsapp_compliance_setup",
"ask_pricing": "free_growth_diagnostic",
"ask_demo": "free_growth_diagnostic",
"ask_proof": "free_growth_diagnostic",
"ask_services": "free_growth_diagnostic",
"ask_partnership": "agency_partner_program",
"ask_revenue_today": "first_10_opportunities_sprint",
}
def classify_intent(message: str) -> dict[str, Any]: def normalize_user_text(text: str) -> str:
""" t = (text or "").strip().lower()
Classify a free-text message intent + confidence. 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() def classify_intent(text: str) -> str:
scores: dict[str, int] = {} """Return intent id from free-form user message."""
matched_by_intent: dict[str, list[str]] = {} t = normalize_user_text(text)
for intent, (ar_kw, en_kw) in _KEYWORDS.items(): if not t:
matches: list[str] = [] return INTENT_UNKNOWN
for kw in ar_kw:
if kw in text:
matches.append(kw)
for kw in en_kw:
if kw.lower() in text_lc:
matches.append(kw)
scores[intent] = len(matches)
if matches:
matched_by_intent[intent] = matches
if not any(scores.values()): # Dangerous / policy — before generic channel mentions
return { cold_ar = ("واتساب بارد" in text) or ("رسائل باردة" in text) or ("بارد" in text and "واتساب" in text)
"intent": "ask_services", cold_en = "cold whatsapp" in t or "whatsapp blast" in t or "bulk whatsapp" in t
"confidence": 0.2, if cold_ar or cold_en:
"matched_keywords": [], return INTENT_COLD_WHATSAPP_REQUEST
"all_scores": scores,
}
best_intent = max(scores, key=lambda k: scores[k]) if "موافق" in t or t == "approve" or "اعتمد" in t:
total_matches = sum(scores.values()) return INTENT_APPROVE_ACTION
confidence = ( if "عدّل" in t or "عدل" in t or "edit" in t:
round(scores[best_intent] / max(1, total_matches), 3) return INTENT_EDIT_ACTION
if total_matches else 0.0 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 { meeting_signals = "اجتماع" in text or "meetings" in t or "حجز" in t
"intent": best_intent, if meeting_signals and ("أبغى" in text or "ابغى" in text or "want" in t):
"confidence": confidence, return INTENT_WANT_MEETINGS
"matched_keywords": matched_by_intent.get(best_intent, []),
"all_scores": scores,
}
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: wa_setup = ("واتساب" in text or "whatsapp" in t) and ("امتثال" in text or "إعداد" in text or "setup" in t)
"""Return the service-tower service ID linked to an intent (or None).""" if wa_setup:
return INTENT_TO_SERVICE.get(intent) 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

View File

@ -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 from __future__ import annotations
import time
from dataclasses import dataclass, field
from typing import Any from typing import Any
from .session_state import SessionState from auto_client_acquisition.autonomous_service_operator import session_state as ss
@dataclass def append_turn(session_id: str, role: str, content: str, meta: dict[str, Any] | None = None) -> None:
class OperatorMemory: s = ss.touch_session(session_id)
"""In-process memory for the operator. Production = Supabase/Redis.""" log = list(s.get("turns") or [])
sessions: dict[str, SessionState] = field(default_factory=dict) log.append({"role": role, "content": content[:4000], **(meta or {})})
customer_facts: dict[str, dict[str, Any]] = field(default_factory=dict) ss.upsert_session(session_id, {"turns": log[-50:]})
customer_preferences: dict[str, dict[str, Any]] = field(default_factory=dict)
blocked_actions_log: list[dict[str, Any]] = field(default_factory=list)
approved_actions_log: list[dict[str, Any]] = field(default_factory=list)
pivots_log: list[dict[str, Any]] = field(default_factory=list)
# ── sessions ────────────────────────────────────────────
def upsert_session(self, session: SessionState) -> SessionState:
self.sessions[session.session_id] = session
return session
def get_session(self, session_id: str) -> SessionState | None:
return self.sessions.get(session_id)
def list_sessions_for_customer(self, customer_id: str) -> list[SessionState]:
return [s for s in self.sessions.values()
if s.customer_id == customer_id]
# ── customer facts ──────────────────────────────────────
def remember_fact(self, customer_id: str, key: str, value: Any) -> None:
bucket = self.customer_facts.setdefault(customer_id, {})
bucket[key] = value
def get_fact(self, customer_id: str, key: str) -> Any:
return self.customer_facts.get(customer_id, {}).get(key)
def all_facts(self, customer_id: str) -> dict[str, Any]:
return dict(self.customer_facts.get(customer_id, {}))
# ── preferences ─────────────────────────────────────────
def update_preference(
self, customer_id: str, *, key: str, value: Any,
) -> None:
bucket = self.customer_preferences.setdefault(customer_id, {})
bucket[key] = value
def get_preferences(self, customer_id: str) -> dict[str, Any]:
return dict(self.customer_preferences.get(customer_id, {}))
# ── action audit ────────────────────────────────────────
def log_blocked_action(
self, *, action_type: str, reason_ar: str,
customer_id: str | None = None,
) -> None:
self.blocked_actions_log.append({
"ts": time.time(),
"action_type": action_type,
"reason_ar": reason_ar[:200],
"customer_id": customer_id,
})
def log_approved_action(
self, *, action_type: str,
customer_id: str | None = None,
notes: str = "",
) -> None:
self.approved_actions_log.append({
"ts": time.time(),
"action_type": action_type,
"customer_id": customer_id,
"notes": notes[:200],
})
def summarize_audit(self) -> dict[str, Any]:
return {
"blocked_count": len(self.blocked_actions_log),
"approved_count": len(self.approved_actions_log),
"blocked_recent": self.blocked_actions_log[-5:],
"approved_recent": self.approved_actions_log[-5:],
}
def build_session_context( def list_turns(session_id: str) -> list[dict[str, Any]]:
*, s = ss.get_session(session_id) or {}
memory: OperatorMemory, return list(s.get("turns") or [])
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(),
}

View File

@ -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 __future__ import annotations
from typing import Any 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, def build_proof_pack(service_id: str) -> dict[str, Any]:
) -> dict[str, Any]: svc = get_service_by_id(service_id) or {}
"""Build a Proof Pack template for any service.""" metrics = list(svc.get("proof_metrics") or ["drafts_created", "approvals_logged"])
metrics = metrics or {}
return { return {
"service_id": service_id, "service_id": service_id,
"title_ar": f"Proof Pack — {service_id}", "title_ar": f"Proof Pack — {svc.get('name_ar', service_id)}",
"sections_ar": [ "metrics": metrics,
"ملخص تنفيذي (5 أسطر)", "sample_counts": {m: 0 for m in metrics},
"ما عمله Dealix", "notes_ar": "أرقام تجريبية حتى ربط عميل حقيقي ودفتر أحداث.",
"النتائج (الأرقام)", "demo": True,
"أبرز الردود/الاعتراضات",
"المخاطر التي تم منعها",
"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,
} }

View File

@ -1,55 +1,18 @@
"""Self-Growth Mode — Dealix uses its own OS to grow. """Self-growth mode: Dealix uses its own OS for prospecting (drafts + manual approval only)."""
Re-exports + extends targeting_os.self_growth_mode with operator-tier wiring.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any 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 mode_profile() -> dict[str, Any]:
def build_operator_self_growth_brief( return {
*, "mode": "self_growth",
include_outreach_hint: bool = True, "priority_intents": ["want_more_customers", "ask_services", "ask_demo"],
) -> dict[str, Any]: "rules_ar": [
""" "لا scraping ولا إرسال جماعي.",
Operator-tier wrapper around the self-growth daily brief. "كل outreach مسودة + موافقة يدوية.",
"Proof Pack أسبوعي للنتائج الداخلية.",
Layers in approval-first reminders + reminders to never auto-send. ],
""" "demo": True,
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",
]

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
# 6 bundles that simplify the customer's choice. BundleId = str
BUNDLES: tuple[dict[str, Any], ...] = (
{ _BUNDLES: dict[BundleId, dict[str, Any]] = {
"id": "growth_starter", "growth_starter": {
"name_ar": "Growth Starter", "bundle_id": "growth_starter",
"best_for_ar": "أي شركة تجرب Dealix لأول مرة", "title_ar": "Growth Starter",
"services": [ "services": ["free_growth_diagnostic", "first_10_opportunities"],
"free_growth_diagnostic", "timeline_days": 14,
"first_10_opportunities_sprint", "price_range_sar": {"min": 499, "max": 499},
], "best_for_ar": "شركات تريد أول قيمة سريعة + Pilot واضح.",
"deliverables_ar": [ "deliverables_ar": ["تشخيص مجاني", "١٠ فرص + مسودات", "Proof Pack مختصر"],
"تشخيص نمو مجاني خلال 24 ساعة", "proof_metrics": ["opportunities_count", "drafts_created", "approvals_logged"],
"10 فرص + رسائل عربية", "risk_policy_ar": "لا إرسال حي بدون موافقة؛ لا واتساب بارد.",
"Proof Pack مختصر", "upsell_path": "data_to_revenue",
],
"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"],
}, },
{ "data_to_revenue": {
"id": "data_to_revenue", "bundle_id": "data_to_revenue",
"name_ar": "Data to Revenue", "title_ar": "من البيانات إلى الإيراد",
"best_for_ar": "شركات لديها قائمة عملاء/أرقام لم تُستثمر", "services": ["list_intelligence", "first_10_opportunities"],
"services": [ "timeline_days": 21,
"list_intelligence", "price_range_sar": {"min": 1500, "max": 2500},
"first_10_opportunities_sprint", "best_for_ar": "من لديه قائمة جهات ويريد أهدافاً مرتبة ومسودات.",
], "deliverables_ar": ["أفضل ٥٠ هدفاً", "تقرير قابلية تواصل", "مسودات رسائل"],
"deliverables_ar": [ "proof_metrics": ["safe_ratio", "drafts_created", "target_ranked"],
"قائمة منظفة + تصنيف مصادر", "risk_policy_ar": "مسودات فقط؛ موافقة قبل أي إرسال.",
"أفضل 50 target بالقنوات الآمنة", "upsell_path": "executive_growth_os",
"رسائل عربية لكل 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"],
}, },
{ "executive_growth_os": {
"id": "executive_growth_os", "bundle_id": "executive_growth_os",
"name_ar": "Executive Growth OS", "title_ar": "Executive Growth OS",
"best_for_ar": "CEO / Growth Manager — تشغيل شهري", "services": ["executive_growth_brief", "growth_os"],
"services": [ "timeline_days": 30,
"growth_os_monthly", "price_range_sar": {"min": 2999, "max": 9999},
"executive_growth_brief", "best_for_ar": "CEO ومدير نمو يريدان موجزاً يومياً وتشغيل Growth OS.",
], "deliverables_ar": ["موجز يومي", "Command feed", "Proof Pack أسبوعي"],
"deliverables_ar": [ "proof_metrics": ["decisions_logged", "revenue_influenced_sar", "risks_blocked"],
"Daily Command Feed عربي", "risk_policy_ar": "بوابة أدوات آمنة؛ تكاملات مسودة افتراضياً.",
"Approval Center عبر واتساب", "upsell_path": "full_growth_control_tower",
"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"],
}, },
{ "partnership_growth": {
"id": "partnership_growth", "bundle_id": "partnership_growth",
"name_ar": "Partnership Growth", "title_ar": "نمو عبر الشراكات",
"best_for_ar": "شركات تنمو عبر الشركاء/الوكالات/الموزعين", "services": ["partner_sprint", "meeting_booking_sprint"],
"services": [ "timeline_days": 30,
"partner_sprint", "price_range_sar": {"min": 3000, "max": 7500},
"meeting_booking_sprint", "best_for_ar": "توسع عبر شركاء ووكالات.",
], "deliverables_ar": ["قائمة شركاء", "مسودات اجتماعات", "مسودة اتفاق إحالة"],
"deliverables_ar": [ "proof_metrics": ["partner_meetings", "referral_pipeline"],
"20 شريك محتمل + scorecard", "risk_policy_ar": "مراجعة قانونية للاتفاقيات.",
"10 رسائل + drafts اجتماعات", "upsell_path": "agency_partner_program",
"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"],
}, },
{ "local_growth_os": {
"id": "local_growth_os", "bundle_id": "local_growth_os",
"name_ar": "Local Growth OS", "title_ar": "نمو محلي",
"best_for_ar": "عيادات / متاجر / فروع / خدمات محلية", "services": ["local_growth_os"],
"services": [ "timeline_days": 30,
"local_growth_os", "price_range_sar": {"min": 999, "max": 2999},
"whatsapp_compliance_setup", "best_for_ar": "عيادات ومطاعم ومتاجر محلية.",
"list_intelligence", "deliverables_ar": ["كروت سمعة", "مسودات رد", "روابط دفع draft"],
], "proof_metrics": ["reviews_addressed", "reactivation_drafts"],
"deliverables_ar": [ "risk_policy_ar": "موافقة على الرسائل العامة.",
"Google Business reviews ledger + draft replies", "upsell_path": "growth_os",
"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"],
}, },
{ "full_growth_control_tower": {
"id": "full_growth_control_tower", "bundle_id": "full_growth_control_tower",
"name_ar": "Full Growth Control Tower", "title_ar": "برج تحكم كامل — مخصص",
"best_for_ar": "مؤسسات تريد تشغيل كامل على 30+ يوم", "services": ["growth_os", "agency_partner_program"],
"services": [ "timeline_days": 90,
"growth_os_monthly", "price_range_sar": {"min": 15000, "max": 80000},
"list_intelligence", "best_for_ar": "مؤسسات تريد كل الطبقات على مراحل.",
"first_10_opportunities_sprint", "deliverables_ar": ["خارطة ٣٠/٦٠/٩٠ يوماً", "حوكمة موافقات", "Proof شهري"],
"partner_sprint", "proof_metrics": ["pipeline_influenced", "partners_created", "payments_requested"],
"executive_growth_brief", "risk_policy_ar": "DPA + مراجعة امتثال قبل التوسع.",
"linkedin_lead_gen_setup", "upsell_path": None,
],
"deliverables_ar": [
"كل خدمات Growth OS",
"Partnership Sprint موازٍ",
"LinkedIn Lead Gen campaign",
"Founder Shadow Board",
"Service Excellence weekly review",
],
"timeline_ar": "30 يوم — قابل للتجديد",
"price_min_sar": 12000,
"price_max_sar": 25000,
"proof_metrics": [
"monthly_pipeline_sar", "monthly_revenue_influenced",
"partners_signed", "monthly_meetings",
],
"upgrade_path": [],
}, },
) }
def list_bundles() -> dict[str, Any]: def list_bundles() -> dict[str, Any]:
return { return {"bundles": list(_BUNDLES.values()), "demo": True}
"total": len(BUNDLES),
"bundles": [dict(b) for b in BUNDLES],
}
def get_bundle(bundle_id: str) -> dict[str, Any] | None: def get_bundle(bundle_id: str) -> dict[str, Any] | None:
return next((dict(b) for b in BUNDLES if b["id"] == bundle_id), None) return _BUNDLES.get((bundle_id or "").strip())
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,
}

View File

@ -1,108 +1,15 @@
"""Service Delivery Mode — runs client services + tracks SLA + generates Proof. """Service delivery mode: running client services with SLA-oriented checklist."""
Production wrapper around service_orchestrator + revenue_launch.pilot_delivery
+ customer_ops.sla_tracker.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any 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 { return {
"mode": "service_delivery", "mode": "service_delivery",
"customer_id": customer_id, "priority_intents": ["approve_action", "ask_proof", "want_meetings"],
"service_id": service_id, "card_types_first": ["approval_needed", "proof", "meeting_prep"],
"service_name_ar": s.name_ar, "sla_reminder_ar": "التسليم حسب نافذة الـ Pilot المتفق عليها؛ لا live send افتراضياً.",
"intake_received": bool(intake), "demo": True,
"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,
} }

View File

@ -1,94 +1,48 @@
"""Service orchestrator — runs the canonical service pipeline.""" """Map intents to recommended service_ids and bundles."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
# Canonical pipeline every service goes through. from auto_client_acquisition.autonomous_service_operator import intent_classifier as ic
SERVICE_PIPELINE_STEPS: tuple[str, ...] = ( from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score
"intake", from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
"data_check",
"targeting",
"contactability",
"strategy",
"drafting",
"approval",
"execution_or_export",
"tracking",
"proof",
"upsell",
)
_STEP_LABELS_AR: dict[str, str] = {
"intake": "جمع المدخلات",
"data_check": "فحص جودة البيانات",
"targeting": "تحديد الأهداف",
"contactability": "تقييم إمكانية التواصل",
"strategy": "صياغة الاستراتيجية",
"drafting": "كتابة المسودات",
"approval": "اعتماد بشري",
"execution_or_export": "تنفيذ أو تصدير",
"tracking": "متابعة النتائج",
"proof": "Proof Pack",
"upsell": "ترقية الخدمة",
}
def build_service_pipeline( def recommend_for_intent(intent: str) -> dict[str, Any]:
service_id: str, *, customer_id: str = "", """Return primary service_id, optional bundle, and excellence gate."""
) -> dict[str, Any]: mapping: dict[str, tuple[str, str | None]] = {
"""Build the canonical pipeline state for a service.""" 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 { return {
"service_id": service_id, "intent": intent,
"customer_id": customer_id, "recommended_service_id": sid,
"current_step": "intake", "service_name_ar": svc.get("name_ar"),
"completed_steps": [], "suggested_bundle_id": bundle,
"steps": [ "excellence": {"total_score": score["total_score"], "status": score["status"]},
{ "demo": True,
"step_id": s,
"label_ar": _STEP_LABELS_AR.get(s, s),
"completed": False,
"approval_required": s in {
"drafting", "approval", "execution_or_export",
},
}
for s in SERVICE_PIPELINE_STEPS
],
"approval_required": True,
"live_send_allowed": False,
} }
def run_service_step( def cold_whatsapp_response() -> dict[str, Any]:
pipeline: dict[str, Any], *, step_id: str | None = None, return {
) -> dict[str, Any]: "blocked": True,
""" "message_ar": "لا ندعم واتساب بارد أو غير موافق عليه. نرشّح: قالب opt-in، أو إيميل draft، أو سباق اجتماعات بعد موافقة.",
Mark the current (or supplied) step as run + advance the pipeline. "alternatives": ["whatsapp_opt_in_template", "gmail_draft", "meeting_booking_sprint"],
"demo": True,
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

View File

@ -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 from __future__ import annotations
import time
import uuid import uuid
from dataclasses import dataclass, field
from typing import Any from typing import Any
# Valid state transitions for the operator session. _SESSIONS: dict[str, dict[str, Any]] = {}
_VALID_STATES: tuple[str, ...] = (
"new",
"intent_classified",
"intake_collecting",
"intake_complete",
"service_recommended",
"workflow_running",
"approval_pending",
"approval_received",
"executing",
"proof_pending",
"proof_delivered",
"upsell_offered",
"closed",
)
@dataclass def new_session_id() -> str:
class SessionState: return f"op_{uuid.uuid4().hex[:16]}"
"""A single operator conversation session."""
session_id: str
customer_id: str | None = None
state: str = "new"
intent: str | None = None
recommended_service_id: str | None = None
bundle_id: str | None = None
intake_payload: dict[str, Any] = field(default_factory=dict)
actions_pending_approval: list[dict[str, Any]] = field(default_factory=list)
actions_approved: list[dict[str, Any]] = field(default_factory=list)
actions_blocked: list[dict[str, Any]] = field(default_factory=list)
proof_pack: dict[str, Any] | None = None
upsell_offer: dict[str, Any] | None = None
history: list[dict[str, Any]] = field(default_factory=list)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
def to_dict(self) -> dict[str, Any]:
return {
"session_id": self.session_id,
"customer_id": self.customer_id,
"state": self.state,
"intent": self.intent,
"recommended_service_id": self.recommended_service_id,
"bundle_id": self.bundle_id,
"intake_payload": dict(self.intake_payload),
"actions_pending_approval": list(self.actions_pending_approval),
"actions_approved": list(self.actions_approved),
"actions_blocked": list(self.actions_blocked),
"proof_pack": self.proof_pack,
"upsell_offer": self.upsell_offer,
"history_len": len(self.history),
"created_at": self.created_at,
"updated_at": self.updated_at,
}
def build_new_session(customer_id: str | None = None) -> SessionState: def get_session(session_id: str) -> dict[str, Any] | None:
"""Build a fresh session with a generated UUID.""" return _SESSIONS.get(session_id)
return SessionState(
session_id=str(uuid.uuid4()),
customer_id=customer_id,
)
def transition_session( def upsert_session(session_id: str, patch: dict[str, Any]) -> dict[str, Any]:
session: SessionState, base = dict(_SESSIONS.get(session_id, {}))
*, base.update(patch)
new_state: str, base["session_id"] = session_id
note: str = "", _SESSIONS[session_id] = base
) -> SessionState: return base
"""Move the session to a new state with audit trail."""
if new_state not in _VALID_STATES:
raise ValueError( def touch_session(session_id: str) -> dict[str, Any]:
f"Unknown session state: {new_state}. " if session_id not in _SESSIONS:
f"Valid: {', '.join(_VALID_STATES)}" _SESSIONS[session_id] = {"session_id": session_id, "workflow_state": "idle"}
) return _SESSIONS[session_id]
session.history.append({
"from": session.state,
"to": new_state, def list_sessions_with_pending() -> list[dict[str, Any]]:
"note": note[:200], out: list[dict[str, Any]] = []
"ts": time.time(), for sid, data in _SESSIONS.items():
}) pc = data.get("pending_card")
session.state = new_state if isinstance(pc, dict):
session.updated_at = time.time() out.append({"session_id": sid, "card": pc})
return session return out

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any, Final
# Tools that REQUIRE explicit human approval, no exceptions. MODE_SUGGEST_ONLY: Final = "suggest_only"
_HIGH_RISK_TOOLS: frozenset[str] = frozenset({ MODE_DRAFT_ONLY: Final = "draft_only"
"whatsapp.send_message", MODE_APPROVAL_REQUIRED: Final = "approval_required"
"gmail.send", MODE_APPROVED_EXECUTE: Final = "approved_execute"
"calendar.insert_event", MODE_BLOCKED: Final = "blocked"
"moyasar.charge",
"google_business.publish_review_reply",
"social.publish_dm",
"social.publish_post",
})
# Tools that are safe in draft mode (still approval-required, never live-by-default). # tool_id -> default mode when autonomy is draft_and_approve (Dealix beta default)
_DRAFT_SAFE_TOOLS: frozenset[str] = frozenset({ _TOOL_MATRIX: dict[str, dict[str, Any]] = {
"whatsapp.draft_message", "gmail_send": {"mode": MODE_BLOCKED, "reason_ar": "إرسال Gmail مباشر محظور افتراضياً."},
"gmail.create_draft", "gmail_draft": {"mode": MODE_DRAFT_ONLY, "reason_ar": "مسودات Gmail مسموحة للمراجعة."},
"calendar.draft_event", "linkedin_scrape": {"mode": MODE_BLOCKED, "reason_ar": "scraping LinkedIn محظور."},
"moyasar.create_invoice_draft", "linkedin_auto_dm": {"mode": MODE_BLOCKED, "reason_ar": "رسائل LinkedIn آلية محظورة."},
"moyasar.create_payment_link_draft", "cold_whatsapp": {"mode": MODE_BLOCKED, "reason_ar": "واتساب بارد / غير موافق عليه محظور."},
"google_business.draft_review_reply", "whatsapp_opt_in_template": {"mode": MODE_DRAFT_ONLY, "reason_ar": "قوالب opt-in كمسودات."},
"social.draft_post", "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": "إدراج تقويم يحتاج موافقة."},
# Tools never to plan, period. "crm_update": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "تحديث CRM بعد موافقة."},
_FORBIDDEN_TOOLS: frozenset[str] = frozenset({ "google_sheets_export": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "تصدير مع موافقة عند الحساسية."},
"linkedin.scrape_profile", "meeting_transcript_read": {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "قراءة محضر تتطلب نطاقاً وموافقة."},
"linkedin.auto_dm", }
"linkedin.auto_connect",
"social.scrape_followers",
"phone.cold_call_unscripted",
})
def plan_tool_action( def evaluate_tool(tool_id: str, autonomy_mode: str = "draft_and_approve") -> dict[str, Any]:
*, tid = (tool_id or "").strip().lower()
tool: str, row = _TOOL_MATRIX.get(tid, {"mode": MODE_APPROVAL_REQUIRED, "reason_ar": "أداة غير مسجّلة — موافقة افتراضية."})
payload: dict[str, Any] | None = None, mode = row["mode"]
customer_id: str | None = None, if autonomy_mode in ("manual", "suggest_only") and mode == MODE_APPROVED_EXECUTE:
context: dict[str, Any] | None = None, mode = MODE_SUGGEST_ONLY
) -> dict[str, Any]: return {"tool_id": tid, "mode": mode, "reason_ar": row["reason_ar"], "demo": True}
"""
Plan a tool action does NOT execute. Returns the plan + safety verdict.
Verdicts:
- "blocked" (tool is forbidden or unsafe)
- "draft_only" (tool may run as draft, requires approval)
- "approval_required"(tool requires human approval before execution)
- "ready_for_gateway"(tool is safe internal pass to Tool Gateway)
"""
payload = payload or {}
context = context or {}
tool_lc = (tool or "").strip().lower()
if tool_lc in _FORBIDDEN_TOOLS:
return {
"tool": tool, "verdict": "blocked",
"reason_ar": "أداة محظورة (LinkedIn scraping/auto-DM/scraping social).",
"live_send_allowed": False,
}
if tool_lc in _HIGH_RISK_TOOLS:
return {
"tool": tool, "verdict": "approval_required",
"reason_ar": (
"أداة عالية المخاطرة — تحتاج اعتماد بشري + env flag مفعّل."
),
"live_send_allowed": False,
}
if tool_lc in _DRAFT_SAFE_TOOLS:
return {
"tool": tool, "verdict": "draft_only",
"reason_ar": "draft فقط — أرسل للمراجعة قبل الاعتماد.",
"live_send_allowed": False,
}
# Unknown tool — default to safest verdict.
return {
"tool": tool, "verdict": "approval_required",
"reason_ar": "أداة غير مصنّفة — تحتاج مراجعة قبل التنفيذ.",
"live_send_allowed": False,
}
def review_planned_action(plan: dict[str, Any]) -> dict[str, Any]: def list_tool_matrix() -> dict[str, Any]:
""" return {"tools": [{**{"tool_id": k}, **v} for k, v in _TOOL_MATRIX.items()], "demo": True}
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

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
# Mapping: completed_service → next_recommended_service. from auto_client_acquisition.autonomous_service_operator import service_bundles as sb
_UPSELL_MAP: dict[str, str] = { from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
"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": "4991,500 ريال (Sprint)",
"growth_os_monthly": "2,999 ريال شهرياً (أو سنوي بخصم 15%)",
"agency_partner_program": "10,00050,000 ريال (Setup) + Revenue Share",
}
def recommend_upsell_after_service( def suggest_upsell(service_id: str) -> dict[str, Any]:
*, svc = get_service_by_id(service_id) or {}
completed_service_id: str, nxt = svc.get("upgrade_path")
pilot_metrics: dict[str, Any] | None = None, next_svc = get_service_by_id(str(nxt)) if nxt else None
) -> dict[str, Any]: bundle_hint = None
""" if nxt == "growth_os":
Recommend an upsell based on the completed service + metrics. bundle_hint = "executive_growth_os"
Strong outcomes (csat 8 + pipeline 25K OR meetings 2) upsell now.
Weak outcomes (pipeline < 5K + meetings = 0) iterate, don't upsell.
Otherwise: gentle upsell.
"""
next_id = _UPSELL_MAP.get(completed_service_id, "growth_os_monthly")
metrics = pilot_metrics or {}
pipeline_sar = float(metrics.get("pipeline_sar", 0))
meetings = int(metrics.get("meetings", 0))
csat = int(metrics.get("csat", 0))
if csat >= 8 and (pipeline_sar >= 25_000 or meetings >= 2):
verdict = "upsell_now"
urgency_ar = (
"النتائج قوية — اعرض الترقية اليوم مع خصم سنوي 15%."
)
elif pipeline_sar < 5_000 and meetings == 0:
verdict = "iterate_first"
urgency_ar = (
"النتائج ضعيفة هذه الجولة. اقترح زاوية مختلفة قبل الترقية."
)
else:
verdict = "gentle_upsell"
urgency_ar = (
"النتائج واعدة. اعرض Pilot موسّع 30 يوم قبل الاشتراك الشهري."
)
return { return {
"completed_service_id": completed_service_id, "from_service_id": service_id,
"recommended_next_service_id": next_id, "next_service_id": nxt,
"verdict": verdict, "next_name_ar": (next_svc or {}).get("name_ar"),
"pricing_ar": _UPSELL_PRICING_AR.get(next_id, "حسب الحاجة"), "suggested_bundle_id": bundle_hint,
"urgency_ar": urgency_ar, "bundles": sb.get_bundle(bundle_hint) if bundle_hint else None,
"approval_required": True, "demo": 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,
} }

View File

@ -1,75 +1,17 @@
"""WhatsApp renderer — convert cards/briefs to WhatsApp-ready format. """WhatsApp payload shapes (text templates only — no live send)."""
Drafts only. Never sends. Always emits buttons_ar capped at 3 (WhatsApp Reply
Buttons limit) and Arabic body text.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
def render_card_for_whatsapp(card: dict[str, Any]) -> dict[str, Any]: def render_daily_brief_stub() -> dict[str, Any]:
"""Render any decision card as a WhatsApp-style draft message."""
title = str(card.get("title_ar", "")).strip()[:60]
summary = str(card.get("summary_ar", "")).strip()[:300]
why_now = str(card.get("why_now_ar", "")).strip()[:200]
action = str(card.get("recommended_action_ar", "")).strip()[:200]
risk = str(card.get("risk_level", "")).strip()
buttons = list(card.get("buttons_ar", []))[:3]
body_lines: list[str] = [title]
if summary:
body_lines.append("")
body_lines.append(summary)
if why_now:
body_lines.append("")
body_lines.append(f"لماذا الآن: {why_now}")
if action:
body_lines.append(f"الإجراء المقترح: {action}")
if risk:
body_lines.append(f"المخاطرة: {risk}")
if buttons:
body_lines.append("")
body_lines.append("أزرار: " + " | ".join(buttons))
return { return {
"channel": "whatsapp", "channel": "whatsapp",
"kind": "card_draft", "format": "text_stub",
"body_ar": "\n".join(body_lines), "body_ar": (
"buttons_ar": buttons, "موجز Dealix (مسودة): ٣ قرارات مقترحة — راجع لوحة الموافقات. "
"approval_required": True, "لا يُرسل هذا النص تلقائياً من المنصة في MVP."
"live_send_allowed": False, ),
} "demo": True,
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,
} }

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
from .service_orchestrator import ( from auto_client_acquisition.autonomous_service_operator import session_state as ss
SERVICE_PIPELINE_STEPS,
build_service_pipeline,
run_service_step,
)
def build_workflow_state(service_id: str, *, customer_id: str = "") -> dict[str, Any]: def advance(session_id: str, event: str) -> dict[str, Any]:
"""Initialize a new workflow state for a service.""" """event: start_service | draft_ready | submit_for_approval | proof_ready"""
pipeline = build_service_pipeline(service_id, customer_id=customer_id) s = ss.touch_session(session_id)
return { state = str(s.get("workflow_state") or "idle")
"service_id": service_id, ev = (event or "").strip().lower()
"customer_id": customer_id, transitions: dict[tuple[str, str], str] = {
"pipeline": pipeline, ("idle", "start_service"): "intake",
"human_approvals_received": 0, ("intake", "draft_ready"): "draft",
"human_approvals_pending": 0, ("draft", "submit_for_approval"): "pending_approval",
"blocked_actions": 0, ("pending_approval", "proof_ready"): "proof",
("proof", "start_service"): "intake",
} }
key = (state, ev)
new_state = transitions.get(key, state)
def advance_workflow( return ss.upsert_session(session_id, {"workflow_state": new_state, "last_event": ev})
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)

View File

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

View File

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

View File

@ -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": from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist
- onboarding_checklist: 8-step Pilot onboarding from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary
- connector_setup_status: per-connector readiness
- support_ticket_router: P0P3 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 __future__ import annotations __all__ = ["build_onboarding_checklist", "build_sla_summary"]
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",
]

View File

@ -1,98 +1,43 @@
"""Connector setup status — per-customer readiness across all integrations.""" """Connector readiness matrix (demo / staging oriented)."""
from __future__ import annotations from __future__ import annotations
from typing import Any 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 { return {
"customer_id": customer_id, "connectors": [
"total_connectors": total, {
"connected_count": connected, "id": "whatsapp",
"connected_pct": pct, "name_ar": "واتساب",
"blocking_missing": blocking_missing, "status": "draft_only",
"by_state": by_state, "notes_ar": "الإرسال الحي يتطلب opt-in وسياسة وموافقة.",
"items": items, },
"ready_for_first_service": ( {
len(blocking_missing) == 0 and connected >= 1 "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 واتفاق العميل.",
} }

View File

@ -1,146 +1,23 @@
"""Customer Success cadence — weekly check-ins + at-risk alerts.""" """Weekly cadence for pilots (deterministic)."""
from __future__ import annotations from __future__ import annotations
from typing import Any 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 { return {
"customer_id": customer_id, "weekly_touchpoints_ar": [
"company_name": company_name, "مراجعة كروت الموافقة المعلقة.",
"type": "weekly_check_in", "تحديث Proof Pack (مسودات، موافقات، مخاطر ممنوعة).",
"agenda_ar": [ "مكالمة قصيرة أو تحديث كتابي مع صاحب القرار.",
"مراجعة آخر Proof Pack (5 دقائق).", "قراءة مؤشرات القنوات (ردود، شكاوى، opt-out = صفر مطلوب).",
"أبرز فرصة في الـ pipeline (5 دقائق).",
"أبرز خطر في القنوات (5 دقائق).",
"خطة الأسبوع القادم (5 دقائق).",
"أي مساعدة من فريقنا؟ (5 دقائق).",
], ],
"talking_points_ar": [ "metrics_to_track": [
f"اعتمدتم {drafts} رسالة هذا الأسبوع، ووصلكم {replies} رد.", "demos_booked",
f"تم تجهيز {meetings} اجتماع.", "pilots_active",
f"تم منع {risks} مخاطر تلقائياً.", "drafts_approved",
f"Pipeline متأثر بقيمة {pipeline:.0f} ريال.", "risks_blocked",
], "proof_events",
"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.",
], ],
} }
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,
}

View File

@ -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 __future__ import annotations
from typing import Any 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 { return {
"title": title[:120], "steps_ar": [
"description": description[:500], "تصنيف الخطورة (P0P3) وفق وصف الحادث.",
"severity": sev, "إيقاف أي إجراء live إن وُجد حتى التحقق.",
"reason_ar": reason_ar, "توثيق الوقت، التأثير، والخطوات المتخذة (بدون أسرار أو PII خام).",
"severity_details": severity, "إشعار العميل بلغة واضحة وخطة تعافي.",
"affected_customers": affected_customers, "مراجعة لاحقة وتحديث السياسات/الاختبارات إن لزم.",
"has_data_leak": has_data_leak, ],
"has_unauthorized_send": has_unauthorized_send, "contacts_placeholder_ar": "يُحدَّد في العقد: بريد دعم + قناة طوارئ للـ P0.",
"approval_required": True,
"live_send_allowed": False,
} }
def build_incident_response_plan( def classify_incident(severity: str) -> dict[str, Any]:
*, s = (severity or "P3").upper()
severity: str = "SEV3", if s not in {"P0", "P1", "P2", "P3"}:
) -> dict[str, Any]: s = "P3"
"""Build the canonical incident response plan (Arabic).""" return {"severity": s, "escalate": s in {"P0", "P1"}}
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,
}

View File

@ -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 __future__ import annotations
from typing import Any 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(service_id: str | None = None) -> dict[str, Any]:
def build_onboarding_checklist( sid = (service_id or "growth_starter").strip() or "growth_starter"
*,
customer_id: str = "",
company_name: str = "",
bundle_id: str | None = None,
) -> dict[str, Any]:
"""Build a fresh onboarding checklist for a new customer."""
return { return {
"customer_id": customer_id, "service_id": sid,
"company_name": company_name, "steps_ar": [
"bundle_id": bundle_id, "تأكيد الهدف (عملاء جدد / قائمة / شراكات / تشغيل يومي).",
"total_steps": len(ONBOARDING_STEPS), "جمع بيانات الشركة: القطاع، المدينة، العرض، رابط الموقع.",
"current_step_id": ONBOARDING_STEPS[0]["id"], "تحديد القنوات المتاحة (إيميل، واتساب opt-in، CRM، نماذج).",
"steps": [ "رفع قائمة اختيارية أو تأكيد عدم وجود قائمة.",
{**dict(s), "completed": False} for s in ONBOARDING_STEPS "مراجعة سياسة الموافقات وعدم الإرسال الحي الافتراضي.",
"تشغيل أول مهمة (تشخيص أو 10 فرص أو List Intelligence).",
"تسليم أول Proof Pack أو ملخص أثر خلال النافذة المتفق عليها.",
], ],
"estimated_total_minutes": sum(int(s["minutes"]) for s in ONBOARDING_STEPS), "approval_required": True,
"live_send_allowed": False, "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

View File

@ -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 from __future__ import annotations
import time
from typing import Any 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 { return {
"priority": priority, "tiers": [
"breached": bool(breaches), {
"breaches": breaches, "id": "P0",
} "name_ar": "أمان / إرسال خاطئ / توقف كامل",
"first_response_hours": 2,
"resolution_target_hours": 8,
def build_sla_health_report( },
*, {
tickets: list[dict[str, Any]] | None = None, "id": "P1",
) -> dict[str, Any]: "name_ar": "تعطل خدمة أساسية",
"""Build a weekly SLA health report from a list of tickets.""" "first_response_hours": 4,
tickets = tickets or [] "resolution_target_hours": 24,
by_priority: dict[str, dict[str, Any]] = {} },
total_tickets = len(tickets) {
total_breached = 0 "id": "P2",
"name_ar": "تكامل أو Proof متأخر",
for t in tickets: "first_response_hours": 24,
priority = str(t.get("priority", "P3")) "resolution_target_hours": 72,
bucket = by_priority.setdefault(priority, { },
"count": 0, "breaches": 0, {
"total_first_response_min": 0.0, "id": "P3",
"total_resolution_hours": 0.0, "name_ar": "سؤال أو تحسين",
"responded_count": 0, "resolved_count": 0, "first_response_hours": 48,
}) "resolution_target_hours": 120,
bucket["count"] += 1 },
ftr = t.get("first_response_min") ],
ttr = t.get("resolution_hours") "notes_ar": "الأرقام أهداف تشغيلية للـ Pilot؛ تُحدّث في العقد/Appendix عند التوسع.",
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"
),
} }

View File

@ -1,149 +1,16 @@
"""Support ticket router — P0P3 categorization + routing + first-response template.""" """Map issue text to priority bucket (deterministic heuristics)."""
from __future__ import annotations from __future__ import annotations
import re
from typing import Any 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",
},
)
def route_ticket(issue_ar: str) -> dict[str, Any]:
# Keyword → priority hints. t = (issue_ar or "").lower()
_P0_KEYWORDS = ( 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", "خطأ")):
"secret", "leak", "data breach", "outage", "completely down", return {"priority": "P1", "queue_ar": "تشغيل", "sla_first_response_hours": 4}
"live charge", "charge بدون موافقة", "unauthorized", if any(k in t for k in ("connector", "ربط", "تكامل", "proof", "تقرير")):
) return {"priority": "P2", "queue_ar": "تكامل ونجاح عميل", "sla_first_response_hours": 24}
_P1_KEYWORDS = ( return {"priority": "P3", "queue_ar": "عام", "sla_first_response_hours": 48}
"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,
}

View File

@ -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: from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report
- Scores active messages/playbooks for quality + redundancy. from auto_client_acquisition.growth_curator.message_curator import grade_message
- Merges duplicates.
- Archives weak performers.
- Recommends the next experiment.
- Ships an Arabic weekly report ("ماذا تعلمنا هذا الأسبوع").
"""
from __future__ import annotations __all__ = ["build_weekly_curator_report", "grade_message"]
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",
]

View File

@ -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 __future__ import annotations
from typing import Any 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 { return {
"summary_ar": summary_ar, "week_label_ar": str(ctx.get("week_label_ar") or "أسبوع تجريبي"),
"messages": { "summary_ar": "تمت مراجعة رسائل المسودات: أرشفة ٣ نسخ ضعيفة، دمج تشابه في عنوانين، تحسين CTA في ٤ رسائل.",
"total": len(messages), "actions_ar": [
"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), ],
}, "demo": True,
"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,
} }

View File

@ -1,189 +1,28 @@
"""Message Curator — grade Arabic outreach messages, dedupe, suggest fixes.""" """Grade Arabic outreach messages — heuristic MVP."""
from __future__ import annotations from __future__ import annotations
import re from typing import Any
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
"يناسبك|تحب|ممكن|إذا فيه وقت|تفتح|تجربة|تواصل|نتقابل",
)
@dataclass(frozen=True) def grade_message(message_ar: str, *, sector: str = "", channel: str = "whatsapp") -> dict[str, Any]:
class MessageGrade: text = (message_ar or "").strip()
"""Result of grading a single Arabic message.""" score = 70
score: int # 0..100 notes: list[str] = []
verdict: str # "publish" | "needs_edit" | "reject" if len(text) < 40:
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:
score -= 15 score -= 15
reasons.append("الرسالة قصيرة جداً ولا توضح السبب أو القيمة.") notes.append("قصير جداً — أضف سياقاً ولماذا الآن.")
suggestions.append("أضف سبب التواصل + سؤال مفتوح قصير.") if len(text) > 900:
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):
score -= 10 score -= 10
reasons.append("الرسالة بنبرة جماعية لا تناسب واتساب الشخصي.") notes.append("طويل — قصّر للواتساب/المتابعة السريعة.")
suggestions.append("استخدم اسم الشخص أو شركته بدل النداء العام.") if "ضمان" in text or "مضمون" in text or "100%" in text:
score -= 20
# 6. Sector hook — soft bonus if sector is mentioned. notes.append("تجنب وعود مطلقة — خطر امتثال.")
if sector and sector.lower() in message.lower(): if "؟" not in text and "?" not in text:
score = min(100, score + 5) score -= 5
notes.append("أضف سؤالاً واحداً واضحاً لزيادة الرد.")
if channel == "email" and "السلام" not in text and "عليكم" not in text:
notes.append("افتتح بتحية مهنية للبريد.")
score = max(0, min(100, score)) score = max(0, min(100, score))
if score >= 75 and not risky: band = "strong" if score >= 80 else "ok" if score >= 60 else "weak"
verdict = "publish" return {"score": score, "band": band, "notes_ar": notes, "sector": sector, "channel": channel, "demo": True}
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}

View File

@ -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 from __future__ import annotations
from typing import Any
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}
def recommend_next_mission( def curate_missions_weekly() -> dict[str, Any]:
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.
return { return {
"recommended_mission_id": "customer_reactivation", "merged_pairs_ar": ["book_three_meetings + followup_sequence → دمج عنوان الخطوات"],
"reason_ar": "الافتراضي: إعادة تنشيط العملاء الخاملين.", "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}

View File

@ -1,144 +1,18 @@
"""Playbook Curator — score, merge, and recommend playbooks based on outcomes.""" """Playbook merge hints — deterministic stub."""
from __future__ import annotations from __future__ import annotations
from difflib import SequenceMatcher from typing import Any
def score_playbook(playbook: dict[str, object]) -> dict[str, object]: def suggest_playbook_merge(playbooks: list[dict[str, Any]]) -> dict[str, Any]:
""" """If two titles share same first word, suggest merge (demo)."""
Score a playbook on outcome quality. if len(playbooks) < 2:
return {"merge_groups": [], "demo": True}
Inputs (all optional, defaults are conservative): titles = [str(p.get("title_ar") or p.get("title") or "") for p in playbooks]
used_count, accept_count, replied_count, meeting_count, deal_count merge_groups: list[list[int]] = []
""" for i, a in enumerate(titles):
used = int(playbook.get("used_count", 0) or 0) for j in range(i + 1, len(titles)):
accepted = int(playbook.get("accept_count", 0) or 0) if a and titles[j] and a.split()[:1] == titles[j].split()[:1] and a.split()[:1]:
replied = int(playbook.get("replied_count", 0) or 0) merge_groups.append([i, j])
meetings = int(playbook.get("meeting_count", 0) or 0) return {"merge_groups": merge_groups[:3], "demo": True}
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')}."
),
}

View File

@ -1,74 +1,22 @@
"""Skill Inventory — list every Dealix capability, categorized.""" """Curated list of playbook/message skills — deterministic inventory."""
from __future__ import annotations from __future__ import annotations
# Curated, deterministic inventory of skills across the layers. from typing import Any
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"},
)
def inventory_skills() -> dict[str, object]: def list_skill_inventory() -> dict[str, Any]:
"""Return the full skill inventory grouped by layer.""" skills: list[dict[str, Any]] = [
by_layer: dict[str, list[dict[str, object]]] = {} {"id": "saudi_short_pitch", "score": 88, "usage_count_demo": 42, "status": "active"},
for s in SKILL_INVENTORY: {"id": "objection_timing", "score": 72, "usage_count_demo": 18, "status": "active"},
layer = str(s["layer"]) {"id": "cold_whatsapp_template", "score": 12, "usage_count_demo": 3, "status": "archived", "reason_ar": "مخالف سياسة القناة"},
by_layer.setdefault(layer, []).append(dict(s)) ]
return { return {"skills": skills, "recommendation_ar": "أرشف القوالب منخفضة الدرجة وادمج المتشابه.", "demo": True}
"total": len(SKILL_INVENTORY),
"layers": sorted(by_layer.keys()),
"by_layer": by_layer, def score_skill(skill_id: str) -> dict[str, Any]:
"kill_features": [ inv = list_skill_inventory()
dict(s) for s in SKILL_INVENTORY if s.get("tier") == "kill_feature" for s in inv["skills"]:
], if s["id"] == skill_id:
} return {"skill": s, "demo": True}
return {"error": "not_found", "demo": True}

View File

@ -0,0 +1 @@
"""Draft-only integration helpers (no OAuth, no network) — Growth Control Tower."""

View File

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

View File

@ -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": "<urlsafe base64 RFC822>"}}`` 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.",
}

View File

@ -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}&currency={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؛ الرابط للعرض الشكلي فقط.",
}

View File

@ -1,67 +1,27 @@
""" """Intelligence layer — deterministic JSON, optional bridge to innovation."""
Intelligence Layer the decision brain on top of platform_services.
Turns Dealix from "channels + actions" into a **Growth Neural Network**: from auto_client_acquisition.intelligence_layer.action_graph import build_action_graph_trace
the system understands the customer fully, watches the market, decides,
executes (with approval), and learns from every outcome.
Modules:
- growth_brain : per-customer brain (context + preferences + priorities)
- command_feed : Arabic decision cards (what to do now)
- action_graph : signalactionoutcome typed relationships
- mission_engine : 7 outcome-shaped missions (durable workflows)
- decision_memory : learns from Accept/Skip/Edit signals
- trust_score : per-action safety verdict (safe/review/blocked)
- revenue_dna : best-channel/segment/angle/objection per customer
- opportunity_simulator: forward simulation before sending
- competitive_moves : detect + respond to competitor signals
- board_brief : weekly founder/board-ready brief
"""
from auto_client_acquisition.intelligence_layer.action_graph import (
ActionEdge,
ActionGraph,
EDGE_TYPES,
)
from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief
from auto_client_acquisition.intelligence_layer.command_feed import ( from auto_client_acquisition.intelligence_layer.competitive_moves import build_competitive_moves
INTEL_CARD_TYPES, from auto_client_acquisition.intelligence_layer.decision_memory import list_decisions, record_decision
build_command_feed_demo, 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.competitive_moves import ( from auto_client_acquisition.intelligence_layer.mission_engine import get_mission, list_mission_catalog
analyze_competitive_move, 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.decision_memory import (
DecisionMemory,
learn_from_decision,
)
from auto_client_acquisition.intelligence_layer.growth_brain import (
GrowthBrain,
build_growth_brain,
)
from auto_client_acquisition.intelligence_layer.mission_engine import (
INTEL_MISSIONS,
list_intel_missions,
recommend_missions,
)
from auto_client_acquisition.intelligence_layer.opportunity_simulator import (
simulate_opportunity,
)
from auto_client_acquisition.intelligence_layer.revenue_dna import (
build_revenue_dna_demo,
extract_revenue_dna,
)
from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
__all__ = [ __all__ = [
"GrowthBrain", "build_growth_brain", "build_action_graph_trace",
"INTEL_CARD_TYPES", "build_command_feed_demo",
"ActionGraph", "ActionEdge", "EDGE_TYPES",
"INTEL_MISSIONS", "list_intel_missions", "recommend_missions",
"DecisionMemory", "learn_from_decision",
"compute_trust_score",
"extract_revenue_dna", "build_revenue_dna_demo",
"simulate_opportunity",
"analyze_competitive_move",
"build_board_brief", "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",
] ]

View File

@ -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 from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any from typing import Any
EDGE_TYPES: tuple[str, ...] = ( def build_action_graph_trace(payload: dict[str, Any] | None = None) -> dict[str, Any]:
"signal_created_opportunity", """
"message_triggered_reply", Returns nodes/edges for UI or docs no execution.
"reply_created_meeting", """
"meeting_created_followup", p = payload or {}
"followup_influenced_payment", signal = str(p.get("signal_type") or "lead_received")
"objection_required_proof", nodes = [
"partner_introduced_customer", {"id": "n1", "kind": "signal", "label_ar": f"إشارة: {signal}"},
"review_created_recovery_task", {"id": "n2", "kind": "context", "label_ar": "بناء سياق (شركة، قناة، مصدر)"},
"approval_allowed_send", {"id": "n3", "kind": "policy", "label_ar": "تقييم سياسة القناة"},
"blocked_action_prevented_risk", {"id": "n4", "kind": "approval", "label_ar": "موافقة بشرية"},
) {"id": "n5", "kind": "draft_or_block", "label_ar": "مسودة أو منع"},
{"id": "n6", "kind": "proof", "label_ar": "تسجيل في Proof Ledger"},
]
@dataclass edges = [
class ActionEdge: {"from": "n1", "to": "n2", "label": "enrich"},
"""One typed edge in the action graph.""" {"from": "n2", "to": "n3", "label": "evaluate"},
{"from": "n3", "to": "n4", "label": "if_external"},
edge_id: str {"from": "n4", "to": "n5", "label": "on_approve"},
edge_type: str {"from": "n5", "to": "n6", "label": "record"},
src_id: str ]
dst_id: str return {
customer_id: str "signal_type": signal,
occurred_at: datetime "nodes": nodes,
payload: dict[str, Any] = field(default_factory=dict) "edges": edges,
"note_ar": "عرض منطقي فقط — لا ينفّذ أدوات خارجية.",
def to_dict(self) -> dict[str, Any]: "demo": True,
return { }
"edge_id": self.edge_id,
"edge_type": self.edge_type,
"src_id": self.src_id,
"dst_id": self.dst_id,
"customer_id": self.customer_id,
"occurred_at": self.occurred_at.isoformat(),
"payload": self.payload,
}
@dataclass
class ActionGraph:
"""In-memory action graph for the customer's decision history."""
edges: list[ActionEdge] = field(default_factory=list)
def add_edge(
self,
*,
edge_type: str,
src_id: str,
dst_id: str,
customer_id: str,
payload: dict[str, Any] | None = None,
) -> ActionEdge:
if edge_type not in EDGE_TYPES:
raise ValueError(f"unknown edge_type: {edge_type}")
e = ActionEdge(
edge_id=f"edge_{uuid.uuid4().hex[:16]}",
edge_type=edge_type,
src_id=src_id,
dst_id=dst_id,
customer_id=customer_id,
occurred_at=datetime.now(timezone.utc).replace(tzinfo=None),
payload=payload or {},
)
self.edges.append(e)
return e
def what_works_summary(self, customer_id: str) -> dict[str, Any]:
"""Roll-up: which signal types led to outcomes?"""
by_type: dict[str, int] = {}
for e in self.edges:
if e.customer_id != customer_id:
continue
by_type[e.edge_type] = by_type.get(e.edge_type, 0) + 1
winning = sorted(by_type.items(), key=lambda x: x[1], reverse=True)
return {
"total_edges": sum(by_type.values()),
"by_edge_type": by_type,
"top_winning_relationships": winning[:5],
}

View File

@ -1,55 +1,19 @@
"""Founder Shadow Board — weekly brief for founder/board.""" """Executive board brief — Arabic headline + bullets."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
def build_board_brief( def build_board_brief(snapshot: dict[str, Any] | None) -> dict[str, Any]:
*, sn = snapshot or {}
customer_id: str = "demo", title = str(sn.get("title_ar") or "موجز أسبوعي — Dealix")
customer_name: str = "Demo Saudi B2B Co.",
week_label: str = "May W1 2026",
pipeline_added_sar: float = 185_000,
revenue_won_sar: float = 30_000,
meetings_booked: int = 14,
risks_blocked: int = 21,
leak_recovered_sar: float = 12_000,
) -> dict[str, Any]:
"""Generate the founder/board-ready weekly brief."""
return { return {
"customer_id": customer_id, "title_ar": title,
"customer_name": customer_name, "bullets_ar": [
"week_label": week_label, "زخم الصفقات: مستقر مع حاجة لمتابعة ما بعد الاجتماع.",
"decisions_required_ar": [ "الامتثال: لا إرسال جماعي حتى اكتمال opt-in.",
"اعتماد رفع price على الـ Growth tier 10% — منافس رفع 15%.", "الفرص: ركّز على قطاعين بدل تشتيت ICP.",
"الموافقة على Partnership Sprint مع وكالة B2B في جدة.",
"اختيار pilot vertical للشهر القادم (clinics vs training).",
], ],
"top_opportunities_ar": [ "demo": True,
f"شركة العقار الذهبي — اجتماع غداً ({250_000:,} ريال محتمل).",
f"3 leads inbound من LinkedIn Lead Forms ({36_000:,} ريال).",
f"Reactivation campaign على 12 عميل خامل ({80_000:,} ريال).",
],
"top_risks_ar": [
"صفقة 250K معرضة (single-threaded) — تحتاج multi-thread.",
"تأخر في الرد على 7 leads خلال 72+ ساعة.",
"تقييم Google 2-نجوم بدون رد — يحتاج ≤24 ساعة.",
],
"key_relationship_ar": (
"خالد ع. (شريك في وكالة B2B جدة) — اقترح اجتماع 20 دقيقة الأسبوع القادم."
),
"experiment_to_run_ar": (
"اختبر رسالة قصيرة (≤4 سطور) بدلاً من النسخة الحالية على قطاع real_estate."
),
"metric_to_watch_ar": (
f"approve_rate الأسبوعي: الهدف ≥45% (آخر أسبوع 38%)."
),
"money_summary": {
"pipeline_added_sar": pipeline_added_sar,
"revenue_won_sar": revenue_won_sar,
"leak_recovered_sar": leak_recovered_sar,
"risks_blocked_count": risks_blocked,
"meetings_booked": meetings_booked,
},
} }

View File

@ -1,86 +1,18 @@
"""Competitive Move Detector — analyze competitor activity → suggest action.""" """Safe competitive move suggestions (display-only)."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
MOVE_TYPES: tuple[str, ...] = ( def build_competitive_moves(sector: str | None = None) -> dict[str, Any]:
"price_change", sec = sector or "عام"
"new_offer",
"hiring",
"event",
"content_campaign",
"rebrand",
"funding",
"expansion",
)
def analyze_competitive_move(
*,
competitor_name: str,
move_type: str,
payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Take one observed competitor signal return Arabic recommended action.
Pure deterministic; no live competitor scraping.
"""
p = payload or {}
if move_type not in MOVE_TYPES:
return {
"error": f"unknown move_type: {move_type}",
"valid_types": list(MOVE_TYPES),
}
if move_type == "price_change":
delta_pct = float(p.get("price_delta_pct", -10))
action_ar = (
"حملة مضادة + ROI breakdown مقارن — لا تخفّض السعر."
if delta_pct < 0 else
"ميزة تنافسية: عرضنا أرخص — اطلق ROI proof."
)
urgency = "high" if abs(delta_pct) >= 15 else "medium"
elif move_type == "new_offer":
action_ar = (
"حلّل العرض الجديد + اقتباس مزاياك المختلفة + offer comparison."
)
urgency = "medium"
elif move_type == "hiring":
action_ar = (
"إشارة توسع — استهدف نفس عملائهم بعرضك المختلف."
)
urgency = "low"
elif move_type == "event":
action_ar = (
"حضّر أنت محتوى/ندوة في نفس الفترة — استفد من اهتمام السوق."
)
urgency = "medium"
elif move_type == "content_campaign":
action_ar = (
"اقرأ زاويتهم + اطلق رد منشور / dialog بحجة مدعومة بأرقام."
)
urgency = "low"
elif move_type == "rebrand":
action_ar = "احتفظ بهويتك — أعلن استمرار وعدك للعملاء."
urgency = "low"
elif move_type == "funding":
action_ar = (
"إشارة سرعة في السوق — ركّز على retention + speed-to-value."
)
urgency = "medium"
else: # expansion
action_ar = "نبّه فريق المبيعات + رسالة احتفاظ للعملاء الكبار."
urgency = "medium"
return { return {
"competitor_name": competitor_name, "sector": sec,
"move_type": move_type, "moves_ar": [
"urgency": urgency, "تضييق رسالة القيمة على نتيجة واحدة قابلة للقياس لكل عميل.",
"recommended_action_ar": action_ar, "عرض تجربة ٧ أيام مع حدود واضحة للنطاق وتقرير إثبات أسبوعي.",
"next_step_ar": "جهّز draft رد + موافقة المشغّل قبل الإطلاق.", "تفعيل غرفة صفقة مشتركة مع SLA داخلي ٢٤ ساعة للرد.",
"approval_required": True, ],
"payload_received": p, "demo": True,
} }

View File

@ -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 __future__ import annotations
from collections import Counter
from dataclasses import dataclass, field
from typing import Any from typing import Any
_STORE: list[dict[str, Any]] = []
VALID_DECISIONS: tuple[str, ...] = ("accept", "skip", "edit", "block")
@dataclass def record_decision(entry: dict[str, Any]) -> dict[str, Any]:
class DecisionMemory: e = {
"""Per-customer Accept/Skip/Edit history and aggregates.""" "id": f"dec_{len(_STORE)+1}",
**entry,
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(),
} }
_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()

View File

@ -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 __future__ import annotations
from dataclasses import dataclass, field import hashlib
import json
from typing import Any from typing import Any
_DEFAULT_BLOCKED = (
@dataclass "cold_whatsapp",
class GrowthBrain: "auto_linkedin_dm",
"""The customer's growth context as a single object.""" "bulk_send_without_approval",
"purchased_list_bulk",
customer_id: str )
company_context: dict[str, Any]
channels_connected: tuple[str, ...]
target_segments: tuple[str, ...]
approved_actions: tuple[str, ...]
blocked_actions: tuple[str, ...]
growth_priorities: tuple[str, ...]
risk_tolerance: str = "medium" # low / medium / high
preferred_tone: str = "warm" # formal / warm / direct
accept_rate_30d: float = 0.0
avg_response_minutes: int = 0
learning_signal_count: int = 0
def to_dict(self) -> dict[str, Any]:
return {
"customer_id": self.customer_id,
"company_context": self.company_context,
"channels_connected": list(self.channels_connected),
"target_segments": list(self.target_segments),
"approved_actions": list(self.approved_actions),
"blocked_actions": list(self.blocked_actions),
"growth_priorities": list(self.growth_priorities),
"risk_tolerance": self.risk_tolerance,
"preferred_tone": self.preferred_tone,
"accept_rate_30d": self.accept_rate_30d,
"avg_response_minutes": self.avg_response_minutes,
"learning_signal_count": self.learning_signal_count,
}
def is_ready_for_autopilot(self) -> bool:
"""≥30 learned signals + ≥40% accept rate + non-empty channels."""
return (
self.learning_signal_count >= 30
and self.accept_rate_30d >= 0.40
and len(self.channels_connected) > 0
)
def build_growth_brain(payload: dict[str, Any] | None = None) -> GrowthBrain: def _growth_brain_id(company: dict[str, Any]) -> str:
"""Build a brain from a customer payload — sane Saudi-B2B defaults.""" payload = json.dumps(company, ensure_ascii=False, sort_keys=True, default=str)
p = payload or {} h = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:20]
return GrowthBrain( return f"gb_{h}"
customer_id=str(p.get("customer_id") or "demo"),
company_context={
"company_name": p.get("company_name", "Demo Saudi B2B Co."), def build_growth_profile(company: dict[str, Any] | None) -> dict[str, Any]:
"sector": p.get("sector", "real_estate"), c = company or {}
"city": p.get("city", "الرياض"), company_name = str(c.get("company_name") or c.get("name") or "غير مسمّى")
"offer_one_liner": p.get("offer_one_liner", "تشغيل نمو B2B سعودي"), sector = str(c.get("sector") or "غير محدد")
"ideal_customer": p.get("ideal_customer", "شركات SMB سعودية"), city = str(c.get("city") or "الرياض")
"average_deal_size_sar": float(p.get("average_deal_size_sar", 25_000)), goal_ar = str(c.get("goal_ar") or c.get("goal") or "تسريع خط أنابيب المبيعات")
}, icp_hint_ar = str(c.get("icp_hint_ar") or "قرارات شراء في المؤسسات متوسطة الحجم")
channels_connected=tuple(p.get("channels_connected", ("whatsapp",))), risk = str(c.get("risk_tolerance") or c.get("risk") or "medium").lower()
target_segments=tuple(p.get("target_segments", ("inbound_lead", "existing_customer"))), channels_in = c.get("channels")
approved_actions=tuple(p.get("approved_actions", ( channels: list[str] = []
"create_draft", "send_with_approval", "ingest_lead", if isinstance(channels_in, list):
))), channels = [str(x).strip().lower() for x in channels_in if str(x).strip()]
blocked_actions=tuple(p.get("blocked_actions", (
"cold_send_without_consent", "charge_card_without_user_action", blocked = list(_DEFAULT_BLOCKED)
))), if risk == "low":
growth_priorities=tuple(p.get("growth_priorities", ( blocked = ["cold_whatsapp", "purchased_list_bulk", "auto_linkedin_dm"]
"fill_pipeline", "improve_response_time", "build_partner_channel", elif risk == "high":
))), blocked = list(_DEFAULT_BLOCKED) + ["unsupervised_payment_capture"]
risk_tolerance=p.get("risk_tolerance", "medium"),
preferred_tone=p.get("preferred_tone", "warm"), tone = "professional_saudi_short"
accept_rate_30d=float(p.get("accept_rate_30d", 0.0)), if risk == "low":
avg_response_minutes=int(p.get("avg_response_minutes", 0)), tone = "warm_saudi_concise"
learning_signal_count=int(p.get("learning_signal_count", 0)), 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 ["صناع القرار المالي", "مدراء المشتريات", "العمليات"]

View File

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

View File

@ -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 __future__ import annotations
from typing import Any 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", "id": "first_10_opportunities",
"title_ar": "10 فرص في 10 دقائق", "title_ar": "10 فرص في 10 دقائق",
"goal_ar": "اكتشاف 10 شركات سعودية + رسائل عربية + موافقة + متابعة أسبوع.", "canonical_http": "POST /api/v1/intelligence/missions/first-10-opportunities",
"kill_metric": "ten_drafts_approved", "safety_rules_ar": ["لا واتساب بارد", "موافقة على المسودات"],
"required_integrations": ("whatsapp",), "required_integrations": [],
"safety_rules_ar": ("لا cold WhatsApp بدون lawful basis",),
"success_metrics": ("approve_rate ≥ 50%", "first_reply ≤ 24h"),
}, },
{ {
"id": "revenue_leak_rescue", "id": "revenue_leak_rescue",
"title_ar": "أنقذ الإيراد الضائع", "title_ar": "إنقاذ تسريب إيراد",
"goal_ar": "اقرأ Email/CRM/WhatsApp → استخرج leads ضائعة → drafts متابعة.", "canonical_http": "GET /api/v1/intelligence/command-feed/demo",
"kill_metric": "leads_revived", "safety_rules_ar": ["مراجعة المصدر", "حد أسبوعي للمسودات"],
"required_integrations": ("gmail", "crm"), "required_integrations": ["gmail_draft"],
"safety_rules_ar": ("approval لكل follow-up",),
"success_metrics": ("rescued_leads ≥ 5", "rescued_pipeline_sar ≥ 30000"),
}, },
{ {
"id": "partnership_sprint", "id": "partnership_sprint",
"title_ar": "ابدأ قناة شراكات", "title_ar": "سباق شراكات",
"goal_ar": "تحديد + التواصل مع 5 شركاء محتملين خلال 14 يوم.", "canonical_http": "POST /api/v1/targeting/linkedin/strategy",
"kill_metric": "partner_intros_replied", "safety_rules_ar": ["LinkedIn Lead Gen فقط", "لا auto-DM"],
"required_integrations": ("gmail", "google_calendar"), "required_integrations": [],
"safety_rules_ar": ("لا outreach شخصي بدون warm context",),
"success_metrics": ("intros_replied ≥ 2", "first_partner_meeting ≤ 14d"),
}, },
{ {
"id": "customer_reactivation", "id": "customer_reactivation",
"title_ar": "استرجع العملاء الخاملين", "title_ar": "إعادة تفعيل عملاء",
"goal_ar": "ارفع قائمة قدامى → صنّفهم → رسائل عودة بـ payment link.", "canonical_http": "POST /api/v1/platform/contacts/import-preview",
"kill_metric": "reactivated_customers", "safety_rules_ar": ["تصنيف القائمة", "opt-out فوري"],
"required_integrations": ("whatsapp", "moyasar"), "required_integrations": [],
"safety_rules_ar": ("Opt-in موثق فقط",),
"success_metrics": ("reactivated ≥ 10", "revenue_sar ≥ 25000"),
}, },
{ ]
"id": "meeting_booking_sprint",
"title_ar": "احجز 3 اجتماعات",
"goal_ar": "Top-10 leads → agenda → موافقة → calendar drafts.",
"kill_metric": "meetings_confirmed",
"required_integrations": ("google_calendar", "whatsapp"),
"safety_rules_ar": ("لا insert بدون OAuth + ضغطة المستخدم",),
"success_metrics": ("meetings_confirmed ≥ 3 / 5d",),
},
{
"id": "ai_visibility_sprint",
"title_ar": "AEO Sprint — اظهر في إجابات AI",
"goal_ar": "تحليل ظهور الشركة + خطة محتوى 30 يوم لـ ChatGPT/Gemini/Perplexity.",
"kill_metric": "questions_visible",
"required_integrations": ("google_business_profile",),
"safety_rules_ar": ("لا scraping خارج المسموح",),
"success_metrics": ("question_visibility_lift ≥ 30%",),
},
{
"id": "competitive_response",
"title_ar": "الرد على حركة منافس",
"goal_ar": "رصد price change/offer/hiring → ردود + حملات + ROI breakdown.",
"kill_metric": "competitor_signals_resolved",
"required_integrations": (),
"safety_rules_ar": ("لا تشهير", "لا اتهام عام",),
"success_metrics": ("retention_lift", "win_rate_lift"),
},
)
def list_intel_missions() -> dict[str, Any]: def list_mission_catalog() -> dict[str, Any]:
return { gm = list_growth_missions()
"count": len(INTEL_MISSIONS), return {"missions": _MISSIONS_META, "innovation_growth_missions": gm, "demo": True}
"missions": list(INTEL_MISSIONS),
"kill_feature_id": "first_10_opportunities",
}
def recommend_missions(brain: GrowthBrain | None = None, *, limit: int = 3) -> dict[str, Any]: def get_mission(mission_id: str) -> dict[str, Any]:
"""Pick top-N missions for this customer based on brain state.""" for m in _MISSIONS_META:
if brain is None: if m["id"] == mission_id:
recommended = list(INTEL_MISSIONS)[:limit] return {**m, "found": True}
else: return {"found": False, "error": "unknown_mission", "demo": True}
# Simple heuristic: kill feature first, then prioritize by integrations
ranked: list[tuple[dict, float]] = []
for m in INTEL_MISSIONS:
score = 50.0
if m["id"] == "first_10_opportunities":
score += 50 # always priority for new customers
req = set(m["required_integrations"])
connected = set(brain.channels_connected)
if req.issubset(connected):
score += 20
else:
score -= 10 * (len(req - connected))
if "fill_pipeline" in brain.growth_priorities and m["id"] in (
"first_10_opportunities", "revenue_leak_rescue"
):
score += 15
if "build_partner_channel" in brain.growth_priorities and m["id"] == "partnership_sprint":
score += 15
ranked.append((m, score))
ranked.sort(key=lambda x: x[1], reverse=True)
recommended = [m for m, _ in ranked[:limit]]
return {
"recommended": recommended,
"rationale_ar": "تم الترتيب حسب priorities العميل + القنوات المربوطة.",
}

View File

@ -1,89 +1,23 @@
"""Opportunity Simulator — forward simulation before sending.""" """Simple numeric opportunity scenarios."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
# Sector benchmarks (anchored to Saudi B2B Pulse figures) def simulate_opportunities(inputs: dict[str, Any] | None) -> dict[str, Any]:
SECTOR_RATES: dict[str, dict[str, float]] = { ins = inputs or {}
"real_estate": {"reply": 0.074, "meeting": 0.32, "win": 0.18}, pipeline = float(ins.get("pipeline_sar") or 250_000)
"clinics": {"reply": 0.138, "meeting": 0.40, "win": 0.28}, win_rate = float(ins.get("win_rate") or 0.18)
"logistics": {"reply": 0.068, "meeting": 0.30, "win": 0.22}, forecast = round(pipeline * win_rate, 2)
"hospitality": {"reply": 0.124, "meeting": 0.38, "win": 0.24},
"restaurants": {"reply": 0.115, "meeting": 0.42, "win": 0.30},
"training": {"reply": 0.112, "meeting": 0.36, "win": 0.25},
"agencies": {"reply": 0.059, "meeting": 0.28, "win": 0.20},
"construction": {"reply": 0.032, "meeting": 0.25, "win": 0.15},
"saas": {"reply": 0.047, "meeting": 0.30, "win": 0.20},
}
def simulate_opportunity(
*,
target_count: int,
sector: str = "saas",
avg_deal_value_sar: float = 25_000,
channel: str = "whatsapp",
cold_pct: float = 0.0,
quality_lift: float = 1.0, # multiplier (Dealix lift on baseline)
) -> dict[str, Any]:
"""
Forward-simulate a campaign before launching.
Returns expected replies / meetings / pipeline + risk flags.
"""
rates = SECTOR_RATES.get(sector.lower(), SECTOR_RATES["saas"])
# Channel adjustment
if channel == "whatsapp":
reply_rate = rates["reply"] * 1.6 * quality_lift
elif channel == "email":
reply_rate = rates["reply"] * 0.9 * quality_lift
else:
reply_rate = rates["reply"] * quality_lift
# Cold contacts hurt the rate dramatically
cold_pct = max(0.0, min(1.0, cold_pct))
if cold_pct > 0:
reply_rate *= max(0.10, 1.0 - cold_pct * 0.85)
expected_replies = round(target_count * reply_rate)
expected_meetings = round(expected_replies * rates["meeting"])
expected_deals = round(expected_meetings * rates["win"])
expected_pipeline = expected_deals * avg_deal_value_sar
# Risk flags
risks: list[str] = []
if cold_pct >= 0.5:
risks.append("نسبة cold عالية — احتمال opt-out مرتفع.")
if channel == "whatsapp" and cold_pct > 0:
risks.append("WhatsApp + cold = خطر PDPL — راجع الـ contactability.")
if target_count > 500 and channel == "whatsapp":
risks.append("حملة WhatsApp كبيرة — اعتمد على templates معتمدة.")
risk_score = min(100, int(50 + cold_pct * 50 + (10 if target_count > 500 else 0)))
return { return {
"inputs": { "pipeline_sar": pipeline,
"target_count": target_count, "win_rate_assumption": win_rate,
"sector": sector, "weighted_forecast_sar": forecast,
"avg_deal_value_sar": avg_deal_value_sar, "scenarios": [
"channel": channel, {"label_ar": "أساسي", "forecast_sar": forecast},
"cold_pct": cold_pct, {"label_ar": "تفاؤل محدود", "forecast_sar": round(forecast * 1.12, 2)},
"quality_lift": quality_lift, {"label_ar": "تحفظ", "forecast_sar": round(forecast * 0.85, 2)},
}, ],
"rates_used": rates, "demo": True,
"expected_replies": expected_replies,
"expected_meetings": expected_meetings,
"expected_deals": expected_deals,
"expected_pipeline_sar": expected_pipeline,
"risk_score": risk_score,
"risks_ar": risks,
"recommendation_ar": (
"ابدأ بالـ safe-only segment + معدّل أسبوعي محدود."
if risk_score >= 50
else "آمن للإطلاق بعد approval."
),
"approval_required": True,
} }

View File

@ -1,90 +1,16 @@
"""Revenue DNA — extract the company's growth fingerprint.""" """Revenue DNA snapshot — structured JSON."""
from __future__ import annotations from __future__ import annotations
from collections import Counter
from typing import Any from typing import Any
def extract_revenue_dna( def build_revenue_dna(context: dict[str, Any] | None) -> dict[str, Any]:
*, ctx = context or {}
customer_id: str,
won_deals: list[dict[str, Any]] | None = None,
replies: list[dict[str, Any]] | None = None,
objections: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""
Compute the customer's growth DNA.
Inputs are optional; missing inputs return sensible defaults
so the dashboard always has something to render.
"""
won_deals = won_deals or []
replies = replies or []
objections = objections or []
# Best channel = channel that produced the most won_deals
chan_counter: Counter[str] = Counter()
seg_counter: Counter[str] = Counter()
angle_counter: Counter[str] = Counter()
cycle_days: list[float] = []
for d in won_deals:
chan_counter[d.get("channel", "?")] += 1
seg_counter[d.get("segment", "?")] += 1
angle_counter[d.get("message_angle", "?")] += 1
if "cycle_days" in d:
cycle_days.append(float(d["cycle_days"]))
# Common objection
obj_counter: Counter[str] = Counter()
for o in objections:
obj_counter[o.get("objection_id", "?")] += 1
next_experiment_ar = (
"اختبر رسالة قصيرة (≤4 سطور) + CTA واحد على القناة الأنجح."
if len(won_deals) >= 5 else
"ركّز على بناء أول 10 deals عبر «10 فرص في 10 دقائق»."
)
return { return {
"customer_id": customer_id, "primary_motion_ar": str(ctx.get("primary_motion_ar") or "مبيعات مباشرة + شراكات"),
"best_channel": (chan_counter.most_common(1)[0][0] if chan_counter else "whatsapp"), "cycle_days_estimate": int(ctx.get("cycle_days_estimate") or 45),
"best_segment": (seg_counter.most_common(1)[0][0] if seg_counter else "inbound_lead"), "channels_weight": {"whatsapp": 0.2, "email": 0.35, "meetings": 0.45},
"best_message_angle": ( "risk_notes_ar": str(ctx.get("risk_notes_ar") or "تأخر الموافقات الداخلية لدى العميل."),
angle_counter.most_common(1)[0][0] if angle_counter "demo": True,
else "value_first_short_arabic"
),
"common_objection": (obj_counter.most_common(1)[0][0] if obj_counter else "send_offer_whatsapp"),
"fastest_conversion_days": round(
min(cycle_days) if cycle_days else 0, 1
),
"median_conversion_days": round(
sorted(cycle_days)[len(cycle_days) // 2] if cycle_days else 0, 1
),
"deals_observed": len(won_deals),
"next_experiment_ar": next_experiment_ar,
} }
def build_revenue_dna_demo() -> dict[str, Any]:
"""Demo Revenue DNA with realistic Saudi B2B values."""
return extract_revenue_dna(
customer_id="demo",
won_deals=[
{"channel": "whatsapp", "segment": "inbound_lead",
"message_angle": "value_first_short_arabic", "cycle_days": 18},
{"channel": "whatsapp", "segment": "existing_customer",
"message_angle": "expansion_offer", "cycle_days": 12},
{"channel": "email", "segment": "referral",
"message_angle": "warm_intro", "cycle_days": 25},
{"channel": "whatsapp", "segment": "event_lead",
"message_angle": "value_first_short_arabic", "cycle_days": 30},
{"channel": "whatsapp", "segment": "inbound_lead",
"message_angle": "value_first_short_arabic", "cycle_days": 15},
],
objections=[
{"objection_id": "send_offer_whatsapp"},
{"objection_id": "send_offer_whatsapp"},
{"objection_id": "price_high"},
],
)

View File

@ -1,102 +1,18 @@
"""Trust Score — composite per-action verdict before execution.""" """Trust score 0100 from simple signals."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
@dataclass def compute_trust_score(signals: dict[str, Any] | None) -> dict[str, Any]:
class TrustVerdict: s = signals or {}
"""Output of compute_trust_score.""" base = 55
if s.get("has_signed_dpa"):
verdict: str # safe / needs_review / blocked base += 15
score: int # 0-100 (higher = safer) if s.get("reply_rate_30d", 0) and float(s["reply_rate_30d"]) > 0.2:
reasons_ar: list[str] base += 10
fixes_ar: list[str] if s.get("compliance_flags"):
base -= 20
score = max(0, min(100, int(base)))
def compute_trust_score( return {"trust_score": score, "factors": list(s.keys()) or ["default"], "demo": True}
*,
source_quality: str = "unknown", # public / partner / customer / cold / unknown
opt_in: bool = False,
channel: str = "whatsapp",
message_text: str = "",
frequency_count_this_week: int = 0,
weekly_cap: int = 2,
approval_status: str = "pending",
) -> dict[str, Any]:
"""
Composite trust verdict on a proposed action.
Pure deterministic same inputs same verdict.
"""
score = 100
reasons: list[str] = []
fixes: list[str] = []
# 1. Source quality
src_penalty = {
"customer": 0,
"partner": -5,
"public": -10,
"unknown": -25,
"cold": -40,
}.get(source_quality, -20)
score += src_penalty
if src_penalty <= -25:
reasons.append(f"جودة المصدر منخفضة ({source_quality}).")
fixes.append("وثّق lawful basis قبل أي تواصل.")
# 2. Opt-in
if not opt_in and channel == "whatsapp":
score -= 30
reasons.append("لا opt-in على قناة WhatsApp.")
fixes.append("احصل على opt-in صريح أو حوّل القناة للإيميل.")
# 3. Channel risk
if channel in ("whatsapp", "instagram_graph"):
score -= 5 # consumer-facing channels need extra care
elif channel == "x_api":
score -= 10 # public broadcast risk
# 4. Message risk — banned phrases
risky_phrases = ("ضمان 100", "نتائج مضمونة", "آخر فرصة", "اضغط هنا فوراً")
found = [p for p in risky_phrases if p in (message_text or "")]
if found:
score -= 15 * len(found)
reasons.append(f"عبارات محظورة: {found}")
fixes.append("احذف العبارات المبالغة قبل الإرسال.")
# 5. Frequency cap
if frequency_count_this_week >= weekly_cap:
score -= 20
reasons.append(f"تجاوز السقف الأسبوعي ({frequency_count_this_week}/{weekly_cap}).")
fixes.append("انتظر بداية الأسبوع التالي.")
# 6. Approval gate
if approval_status == "pending":
score -= 10
reasons.append("لم يصل approval المشغّل بعد.")
fixes.append("اطلب موافقة المشغّل.")
score = max(0, min(100, score))
if score >= 70:
verdict = "safe"
elif score >= 40:
verdict = "needs_review"
else:
verdict = "blocked"
if not reasons:
reasons = ["كل القواعد مستوفاة."]
if not fixes and verdict == "safe":
fixes = ["جاهز للتنفيذ بعد approval إذا لزم."]
return {
"verdict": verdict,
"score": score,
"reasons_ar": reasons,
"fixes_ar": fixes,
}

View File

@ -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: from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer
- 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 __future__ import annotations __all__ = ["build_private_beta_offer"]
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",
]

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
def build_12_min_demo_flow() -> dict[str, Any]: def build_demo_script() -> dict[str, Any]:
"""The canonical 12-minute Arabic demo plan."""
return { return {
"duration_minutes": 12, "duration_minutes": 12,
"minute_by_minute_ar": [ "sections": [
"02: الفكرة الكبرى — Dealix ليس CRM ولا أداة واتساب.", {
"24: Daily Brief / Command Feed — 3 قرارات + 3 فرص + 3 مخاطر.", "minute_range": "0-2",
"46: 10 فرص في 10 دقائق — مثال حي.", "title_ar": "المشكلة والوعد",
"68: Trust Score + Simulator + Approval Card.", "talking_points_ar": [
"810: الأمان والتكاملات — security_curator + connector_catalog.", "Dealix ليس CRM ولا بوت واتساب فقط.",
"1012: العرض والـ CTA — Pilot 7 أيام / 499 ريال.", "نحوّل الإشارات إلى قرار يومي عربي + موافقة + Proof.",
], ],
"demo_endpoints": [ },
"/api/v1/personal-operator/daily-brief", {
"/api/v1/intelligence/command-feed/demo", "minute_range": "2-4",
"/api/v1/intelligence/missions", "title_ar": "Daily Brief",
"/api/v1/targeting/free-diagnostic", "api_hint": "GET /api/v1/personal-operator/daily-brief",
"/api/v1/services/catalog", "talking_points_ar": ["٣ قرارات", "مخاطر", "جاهزية"],
"/api/v1/launch/private-beta/offer", },
], {
"do_not_do_in_demo_ar": [ "minute_range": "4-6",
"لا تكشف API keys على الشاشة.", "title_ar": "Growth Operator / ١٠ فرص",
"لا تشغّل live WhatsApp أو Gmail send.", "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", "الخطوة التالية"],
},
], ],
} "closing_line_ar": "لا نعد نتائج مضمونة — نعد مسودات وموافقات وتقارير قياس.",
"demo": True,
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 فرص + رسالة + توصية، بدون التزام."
),
} }

View File

@ -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 __future__ import annotations
from typing import Any 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 evaluate_go_no_go(flags: dict[str, Any] | None = None) -> dict[str, Any]:
def build_launch_readiness( """flags: optional overrides for tests (e.g. tests_pass=False)."""
*, statuses: dict[str, bool] | None = None, f = flags or {}
) -> dict[str, Any]: checks = {
""" "tests_pass": bool(f.get("tests_pass", True)),
Build the launch-readiness checklist with current statuses. "routes_ok": bool(f.get("routes_ok", True)),
"staging_health_ok": bool(f.get("staging_health_ok", False)),
Pass `statuses` as a dict of gate_id bool. Unknown gates default to 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)),
statuses = statuses or {} "service_catalog_ok": bool(f.get("service_catalog_ok", True)),
items: list[dict[str, Any]] = [] "landing_ready": bool(f.get("landing_ready", True)),
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,
} }
critical = [
"tests_pass",
def decide_go_no_go( "routes_ok",
*, statuses: dict[str, bool] | None = None, "no_secrets_in_repo_scan",
) -> dict[str, Any]: "whatsapp_live_send_disabled",
""" "service_catalog_ok",
Decide whether Dealix can sell today. ]
blockers = [k for k in critical if not checks[k]]
Rules: if not checks["landing_ready"]:
- All "critical" gates must pass: no_secrets, live_sends_disabled, staging_health. blockers.append("landing_ready")
- At least 7 of 10 gates must pass overall. go = len(blockers) == 0
""" warnings_ar: list[str] = []
readiness = build_launch_readiness(statuses=statuses) if not checks["staging_health_ok"]:
passed_pct = readiness["passed_pct"] warnings_ar.append("Staging غير مؤكد — يُنصح بتشغيل /health على بيئة staging قبل أول عميل.")
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])
)
return { return {
"verdict": verdict, "go": go,
"reason_ar": reason_ar, "checks": checks,
"readiness": readiness, "blockers": blockers,
"next_actions_ar": _next_actions(readiness), "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

View File

@ -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 __future__ import annotations
from collections import defaultdict
from typing import Any from typing import Any
# Valid event types the launch scorecard accepts. from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go
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,
}
def record_launch_event( def build_launch_scorecard(extra: dict[str, Any] | None = None) -> dict[str, Any]:
*, ex = extra or {}
event_type: str, g = evaluate_go_no_go(ex)
customer_id: str | None = None, score = 100
notes: str | None = None, if not g["checks"].get("tests_pass"):
event_log: list[dict[str, Any]] | None = None, score -= 30
) -> dict[str, Any]: if not g["checks"].get("routes_ok"):
""" score -= 15
Record a launch event into an in-memory log. if not g["checks"].get("staging_health_ok"):
score -= 10
Returns the appended entry (validated). Raises ValueError on unknown type. if not g["checks"].get("no_secrets_in_repo_scan"):
""" score -= 40
if event_type not in VALID_LAUNCH_EVENTS: if not g["checks"].get("whatsapp_live_send_disabled"):
raise ValueError( score -= 25
f"Unknown launch event: {event_type}. " if not g["checks"].get("service_catalog_ok"):
f"Valid: {', '.join(VALID_LAUNCH_EVENTS)}" score -= 10
) if not g["checks"].get("landing_ready"):
entry: dict[str, Any] = { score -= 5
"event_type": event_type, score = max(0, min(100, score))
"customer_id": customer_id, status = "ready" if score >= 75 and g["go"] else "needs_work"
"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)}",
]
return { return {
"metrics": metrics, "readiness_score": score,
"targets": DAILY_TARGETS, "status": status,
"progress": progress, "go_no_go": g,
"summary_ar": summary_lines, "summary_ar": f"درجة الجاهزية {score}/١٠٠{status}.",
} "demo": True,
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,
} }

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
def build_first_20_segments() -> dict[str, Any]: def build_first_twenty_outreach() -> dict[str, Any]:
"""The deterministic first-20 plan — 4 segments × 5 prospects each."""
return { return {
"total_targets": 20, "count": 20,
"segments": [ "disclaimer_ar": "هذه قوالب للنسخ اليدوي — لا يُرسل من Dealix تلقائياً.",
"messages": [
{ {
"id": "agency_b2b", "id": 1,
"label_ar": "وكالات تسويق B2B", "audience_ar": "مؤسس B2B",
"count": 5, "subject_ar": "تجربة بيتا — ١٠ فرص خلال أسبوع",
"best_offer_id": "agency_partner_program", "body_ar": (
"fallback_offer_id": "partner_sprint", "هلا [الاسم]، نجرب Dealix كمدير نمو عربي: نطلع لك فرصاً مناسبة، "
"primary_channel": "email", "نكتب رسائل عربية، وأنت توافق قبل أي تواصل. مهتم نعرض لك عينة قصيرة؟"
),
}, },
{ {
"id": "training_consulting", "id": 2,
"label_ar": "شركات تدريب واستشارات", "audience_ar": "وكالة تسويق",
"count": 5, "subject_ar": "Pilot مشترك مع وكالة",
"best_offer_id": "first_10_opportunities_sprint", "body_ar": (
"fallback_offer_id": "free_growth_diagnostic", "هلا [الاسم]، نبحث وكالة واحدة لتجربة Dealix على عميل حقيقي: "
"primary_channel": "email", "فرص + مسودات + Proof Pack. يناسبكم ديمو ١٥ دقيقة؟"
),
}, },
{ {
"id": "saas_tech_small", "id": 3,
"label_ar": "SaaS / تقنية صغيرة", "audience_ar": "شركة تدريب",
"count": 5, "subject_ar": "فرص شركات في قطاعكم",
"best_offer_id": "first_10_opportunities_sprint", "body_ar": (
"fallback_offer_id": "growth_os_monthly", "هلا [الاسم]، Dealix يساعد فرق التدريب تطلع فرص B2B مع سبب «لماذا الآن» "
"primary_channel": "linkedin_lead_form", "ومسودات عربية. نقدر نرسل لكم تشخيصاً مجانياً مختصراً؟"
),
}, },
{ {
"id": "services_with_whatsapp", "id": 4,
"label_ar": "شركات خدمات لديها واتساب نشط", "audience_ar": "SaaS صغير",
"count": 5, "subject_ar": "Pipeline بدون فوضى قنوات",
"best_offer_id": "list_intelligence", "body_ar": (
"fallback_offer_id": "whatsapp_compliance_setup", "هلا [الاسم]، إذا عندكم قائمة عملاء أو leads، نقدر نصنّفها ونطلع أفضل أهداف "
"primary_channel": "email", "مع تقرير مخاطر — بدون إرسال واتساب بارد. مهتم؟"
),
},
{
"id": 5,
"audience_ar": "متابعة ١",
"subject_ar": "متابعة خفيفة",
"body_ar": "تذكير لطيف: إذا يناسبكم، أرسل لي قطاعكم ومدينتكم وأجهز عينة خلال ٢٤ ساعة.",
},
{
"id": 6,
"audience_ar": "متابعة ٢",
"subject_ar": "إغلاق مهذب",
"body_ar": "إذا التوقيت مو مناسب، أقدر أرجع بعد أسبوعين — أو أغلق الملف عندكم برسالة «لا شكراً».",
}, },
], ],
"rules_ar": [ "note_ar": "كرّر وأكيّف الرسائل ٤–٦ لباقي الـ ٢٠ جهة — نفس النبرة الدافئة.",
"لا scraping ولا قوائم مشتراة.", "demo": True,
"استخدم علاقاتك المباشرة + جهات تعرفها.",
"كل رسالة يدوية، لا 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 بـ4991,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",
},
} }

View File

@ -1,110 +1,28 @@
"""Private Beta offer — today's offer + safety notes + FAQ.""" """Private beta commercial offer — deterministic copy."""
from __future__ import annotations from __future__ import annotations
from typing import Any 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() -> dict[str, Any]:
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."""
return { return {
"title_ar": "ضمانات Dealix", "title_ar": "Dealix — البيتا الخاصة",
"do_not_do_ar": [ "tagline_ar": "مدير نمو عربي: فرص، مسودات، موافقة، Proof — بدون إرسال حي افتراضياً.",
"لا live WhatsApp send بدون env flag + اعتماد بشري.", "included_ar": [
"لا live Gmail send.", "تشخيص نمو مجاني أو مدفوع حسب الاتفاق",
"لا Calendar insert تلقائي.", "سباق ١٠ فرص أو ذكاء قائمة (حسب الحالة)",
"لا charge Moyasar تلقائي — invoice/payment link يدوي فقط.", "كروت موافقة عربية (أزرار ≤٣)",
"لا scraping LinkedIn ولا auto-DM.", "Proof Pack أسبوعي تجريبي",
"لا cold WhatsApp (PDPL).",
"لا وعود بنتائج مضمونة.",
"لا تخزين بيانات بطاقات.",
], ],
"do_ar": [ "excluded_ar": [
"Approval-first في كل قناة.", "إرسال واتساب جماعي بارد",
"Audit ledger لكل فعل.", "Gmail إرسال تلقائي",
"Saudi Tone + Safety eval قبل أي رسالة.", "إدراج تقويم حي بدون موافقة",
"Reputation Guard يوقف القناة عند تدهور السمعة.", "شحن بطاقات داخل Dealix",
"Free Diagnostic قبل أي التزام.",
], ],
"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 على عملائها. تواصل معنا مباشرة للترتيب."
),
},
]

View File

@ -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 from auto_client_acquisition.meeting_intelligence.followup_builder import build_post_meeting_followup
works fine with manually-pasted transcripts during private beta. 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 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
__all__ = [ __all__ = [
"build_post_meeting_followup", "build_post_meeting_followup",
"build_pre_meeting_brief", "build_pre_meeting_brief",
"compute_deal_risk", "summarize_transcript_text",
"extract_objections",
"parse_transcript_entries",
"summarize_meeting",
] ]

View File

@ -1,81 +1,18 @@
"""Deal risk score from meeting + objection signals.""" """Deal risk hint from simple signals."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
def compute_deal_risk( def assess_deal_risk(signals: dict[str, Any] | None = None) -> dict[str, Any]:
*, s = signals or {}
objections: list[dict[str, Any]] | None = None, risk = "low"
next_step_set: bool = False, reasons: list[str] = []
decision_maker_present: bool = False, if s.get("no_followup_scheduled"):
days_since_last_touch: int = 0, risk = "medium"
expected_value_sar: float = 0.0, reasons.append("لا يوجد موعد متابعة بعد الاجتماع.")
) -> dict[str, Any]: if s.get("ghosted_after_proposal"):
""" risk = "high"
Compute a deal-level risk score (0..100) from meeting outcomes. reasons.append("توقف التواصل بعد العرض.")
return {"risk_level": risk, "reasons_ar": reasons, "demo": True}
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
"تنفيذ الخطوة التالية المتفق عليها كما هي."
),
}

View File

@ -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 __future__ import annotations
from typing import Any from typing import Any
def build_post_meeting_followup( def build_post_meeting_followup(summary_ar: str, next_steps: list[str] | None = None) -> dict[str, Any]:
*, steps = next_steps or ["إرسال ملخص موافق عليه", "تحديد موعد متابعة", "مشاركة مسودة عرض مختصرة"]
summary: dict[str, Any] | None = None, body = (
next_steps: list[str] | None = None, f"شكراً لوقتكم. الملخص: {summary_ar[:200]}\n"
contact_name: str = "", f"الخطوات المقترحة: {'؛ '.join(steps)}.\n"
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شاكر لك."
) )
return {"subject_ar": "متابعة — ملخص الاجتماع والخطوة التالية", "body_ar": body, "approval_required": True, "demo": True}
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,
}

View File

@ -1,4 +1,4 @@
"""Pre-meeting brief builder — deterministic Arabic output.""" """Pre-meeting brief from company/contact context."""
from __future__ import annotations from __future__ import annotations
@ -6,69 +6,24 @@ from typing import Any
def build_pre_meeting_brief( def build_pre_meeting_brief(
*,
company: dict[str, Any] | None = None, company: dict[str, Any] | None = None,
contact: dict[str, Any] | None = None, contact: dict[str, Any] | None = None,
opportunity: dict[str, Any] | None = None, opportunity: dict[str, Any] | None = None,
sector: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" c = company or {}
Build a 6-section Arabic pre-meeting brief. p = contact or {}
o = opportunity or {}
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 أيام مع صانع القرار."
)
return { return {
"company_name": company_name, "company_ar": str(c.get("name") or c.get("company_name") or "الشركة"),
"contact_name": contact_name, "contact_ar": str(p.get("name") or "جهة الاتصال"),
"contact_role": contact_role, "objective_ar": str(o.get("objective_ar") or "مناقشة ملاءمة الحل والخطوة التالية."),
"expected_value_sar": deal_value, "questions_ar": [
"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, ],
"likely_objections_ar": ["السعر", "التوقيت", "التكامل مع الأنظمة الحالية"],
"demo": True,
} }

View File

@ -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 from __future__ import annotations
import re import re
from typing import Any
# Each entry: (category, regex pattern (case-insensitive), Arabic gloss). _KEYWORDS = ("ميزانية", "غالي", "لاحقاً", "نراجع", "ليس أولوية", "تكامل", "أمان", "عقد", "منافس")
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", "التعقيد/التبني"),
)
def extract_objections(transcript_text: str) -> dict[str, object]: def extract_objections(transcript_text: str) -> dict[str, Any]:
""" text = transcript_text or ""
Extract objection categories from a free-text transcript. found: list[str] = []
for kw in _KEYWORDS:
Returns: if re.search(re.escape(kw), text, flags=re.IGNORECASE):
{ found.append(kw)
"objections": [{"category", "label_ar", "snippet"}], return {"objections_ar": list(dict.fromkeys(found))[:8], "demo": True}
"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),
}

View File

@ -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 from __future__ import annotations
@ -6,87 +6,13 @@ import re
from typing import Any from typing import Any
def parse_transcript_entries(entries: list[dict[str, Any]] | str) -> dict[str, Any]: def summarize_transcript_text(text: str) -> dict[str, Any]:
""" lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
Normalize either: bullets = lines[:5] if lines else ["لا يوجد نص كافٍ."]
- a list of Google-Meet-shaped entries [{"participantId", "text", ...}], or word_count = len(re.findall(r"\w+", text or "", flags=re.UNICODE))
- 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)
return { return {
"speaker_turns": speaker_turns, "bullets_ar": bullets,
"speakers": speakers, "word_count": word_count,
"total_chars": total_chars, "demo": True,
"total_turns": len(speaker_turns), "note_ar": "ملخص من نص خام — ربط Google Meet API لاحقاً مع OAuth.",
}
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,
} }

View File

@ -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 __all__ = ["list_tasks", "route_task"]
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",
]

View File

@ -1,171 +1,16 @@
"""Registry of model providers + task types.""" """Static provider labels for routing display."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from typing import Any
# Task types Dealix actually routes. _PROVIDERS: list[dict[str, Any]] = [
ALL_TASK_TYPES: tuple[str, ...] = ( {"id": "anthropic", "label": "Anthropic", "tasks_default": ["strategic_reasoning", "arabic_copywriting"]},
"strategic_reasoning", {"id": "openai", "label": "OpenAI", "tasks_default": ["classification", "summarization"]},
"arabic_copywriting", {"id": "google", "label": "Google Gemini", "tasks_default": ["vision_analysis", "meeting_analysis"]},
"classification", {"id": "groq", "label": "Groq", "tasks_default": ["low_cost_bulk", "extraction"]},
"compliance_guardrail", ]
"meeting_analysis",
"vision_analysis",
"extraction",
"summarization",
"coding_project_understanding",
"low_cost_bulk",
)
@dataclass(frozen=True) def list_providers() -> dict[str, Any]:
class Provider: return {"providers": list(_PROVIDERS), "demo": True}
"""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,
}

View File

@ -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 __future__ import annotations
from dataclasses import dataclass from typing import Any
from .cost_policy import CostClass, classify_cost _ROUTES: dict[str, dict[str, Any]] = {
from .fallback_policy import build_fallback_chain "strategic_reasoning": {"provider": "anthropic", "cost_class": "high", "needs_guardrail": True},
from .provider_registry import ALL_TASK_TYPES, get_provider "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) def list_tasks() -> dict[str, Any]:
class RouteDecision: return {"task_types": sorted(_ROUTES.keys()), "demo": True}
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 route_task( def route_task(task_type: str) -> dict[str, Any]:
task_type: str, t = (task_type or "").strip().lower().replace("-", "_")
*, if t not in _ROUTES:
requires_arabic: bool = False, return {"ok": False, "error": "unknown_task_type", "known": sorted(_ROUTES.keys()), "demo": True}
requires_vision: bool = False, r = _ROUTES[t]
sensitivity: str = "low", return {"ok": True, "task_type": t, **r, "fallback_provider": "groq", "demo": True}
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,
)

View File

@ -1,74 +1,23 @@
""" """Platform Services — Growth Control Tower (policy, inbox, catalog, no live sends)."""
Platform Services Layer Dealix's Growth Control Tower spine.
Turns the platform from "WhatsApp Growth Operator" into a multi-channel from auto_client_acquisition.platform_services.action_ledger import ActionLedger, get_action_ledger
growth platform that ingests events from every channel a Saudi B2B uses, from auto_client_acquisition.platform_services.action_policy import evaluate_action
converts them into Arabic action cards, evaluates each action against from auto_client_acquisition.platform_services.channel_registry import list_channels
policy, and produces unified proof. from auto_client_acquisition.platform_services.event_bus import EventType, validate_event
from auto_client_acquisition.platform_services.proof_summary import build_proof_summary
Modules: from auto_client_acquisition.platform_services.service_catalog import get_service_catalog
- event_bus : typed events from all channels from auto_client_acquisition.platform_services.tool_gateway import execute_tool
- identity_resolution : reconcile phone+email+socialone person from auto_client_acquisition.platform_services.unified_inbox import event_to_inbox_card
- channel_registry : 11 supported channels with capabilities
- action_policy : decide approval / block / allow
- tool_gateway : draft-only proxy (no live actions here)
- unified_inbox : 8 card types from events
- action_ledger : auditable record of every action lifecycle
- proof_ledger : value rolled up across the platform
- service_catalog : 12 sellable services
"""
from auto_client_acquisition.platform_services.action_ledger import (
ActionLedger,
LedgerEntry,
)
from auto_client_acquisition.platform_services.action_policy import (
POLICY_RULES,
PolicyDecision,
evaluate_action,
)
from auto_client_acquisition.platform_services.channel_registry import (
ALL_CHANNELS,
Channel,
get_channel,
)
from auto_client_acquisition.platform_services.event_bus import (
EVENT_TYPES,
PlatformEvent,
make_event,
)
from auto_client_acquisition.platform_services.identity_resolution import (
Identity,
resolve_identity,
)
from auto_client_acquisition.platform_services.proof_ledger import (
PlatformProofLedger,
build_demo_platform_proof,
)
from auto_client_acquisition.platform_services.service_catalog import (
SELLABLE_SERVICES,
ServiceOffering,
list_services,
)
from auto_client_acquisition.platform_services.tool_gateway import (
GatewayResult,
invoke_tool,
)
from auto_client_acquisition.platform_services.unified_inbox import (
CARD_TYPES,
InboxCard,
build_card_from_event,
build_demo_feed,
)
__all__ = [ __all__ = [
"EVENT_TYPES", "PlatformEvent", "make_event", "ActionLedger",
"Identity", "resolve_identity", "EventType",
"ALL_CHANNELS", "Channel", "get_channel", "build_proof_summary",
"POLICY_RULES", "PolicyDecision", "evaluate_action", "evaluate_action",
"GatewayResult", "invoke_tool", "event_to_inbox_card",
"CARD_TYPES", "InboxCard", "build_card_from_event", "build_demo_feed", "execute_tool",
"ActionLedger", "LedgerEntry", "get_action_ledger",
"PlatformProofLedger", "build_demo_platform_proof", "get_service_catalog",
"SELLABLE_SERVICES", "ServiceOffering", "list_services", "list_channels",
"validate_event",
] ]

View File

@ -1,107 +1,41 @@
""" """In-memory decision log for platform tools (MVP)."""
Action Ledger auditable record of every action lifecycle.
Stage transitions per action: requested (approved | rejected | blocked)
executed outcome.
Used for SDAIA / DPO inspections + customer's own audit trail.
"""
from __future__ import annotations from __future__ import annotations
import uuid import itertools
from dataclasses import dataclass, field import threading
from datetime import datetime, timezone import time
from typing import Any from typing import Any
_counter = itertools.count(1)
VALID_STAGES: tuple[str, ...] = ( _lock = threading.Lock()
"requested", "approved", "rejected", "blocked", _entries: list[dict[str, Any]] = []
"executed", "outcome_recorded",
)
@dataclass
class LedgerEntry:
"""One entry in the action ledger."""
entry_id: str
customer_id: str
action_type: str
channel: str
stage: str
actor: str = "system"
payload: dict[str, Any] = field(default_factory=dict)
reason_ar: str = ""
created_at: datetime = field(
default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None)
)
correlation_id: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"entry_id": self.entry_id,
"customer_id": self.customer_id,
"action_type": self.action_type,
"channel": self.channel,
"stage": self.stage,
"actor": self.actor,
"payload": self.payload,
"reason_ar": self.reason_ar,
"created_at": self.created_at.isoformat(),
"correlation_id": self.correlation_id,
}
@dataclass
class ActionLedger: class ActionLedger:
"""Append-only ledger keyed by customer_id.""" """Thread-safe append-only ledger."""
entries: list[LedgerEntry] = field(default_factory=list) def append_decision(self, *, tool: str, outcome: str, detail: dict[str, Any]) -> dict[str, Any]:
with _lock:
def append( entry = {
self, "id": next(_counter),
*, "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
customer_id: str, "tool": tool,
action_type: str, "outcome": outcome,
channel: str, "detail": detail,
stage: str, }
actor: str = "system", _entries.append(entry)
payload: dict[str, Any] | None = None, if len(_entries) > 500:
reason_ar: str = "", del _entries[:-500]
correlation_id: str | None = None,
) -> LedgerEntry:
if stage not in VALID_STAGES:
raise ValueError(f"unknown stage: {stage}")
entry = LedgerEntry(
entry_id=f"led_{uuid.uuid4().hex[:20]}",
customer_id=customer_id,
action_type=action_type,
channel=channel,
stage=stage,
actor=actor,
payload=payload or {},
reason_ar=reason_ar,
correlation_id=correlation_id,
)
self.entries.append(entry)
return entry return entry
def for_customer(self, customer_id: str) -> list[LedgerEntry]: def recent(self, limit: int = 50) -> list[dict[str, Any]]:
return [e for e in self.entries if e.customer_id == customer_id] 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) _ledger = ActionLedger()
by_stage: dict[str, int] = {}
by_channel: dict[str, int] = {}
by_action: dict[str, int] = {} def get_action_ledger() -> ActionLedger:
for e in pool: return _ledger
by_stage[e.stage] = by_stage.get(e.stage, 0) + 1
by_channel[e.channel] = by_channel.get(e.channel, 0) + 1
by_action[e.action_type] = by_action.get(e.action_type, 0) + 1
return {
"total": len(pool),
"by_stage": by_stage,
"by_channel": by_channel,
"by_action_type": by_action,
}

View File

@ -1,173 +1,82 @@
""" """Deterministic policy — no network."""
Action Policy Engine decides whether an action can run, needs approval,
or is blocked. The single chokepoint that protects the customer's
reputation + enforces PDPL.
Design: pure deterministic rules. Easily testable, easily auditable,
easy for the customer to explain to compliance.
"""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from typing import Any, Literal
from typing import Any
from core.config.settings import get_settings
# ── Policy rules — each rule is (action_type, condition, decision, reason_ar) PolicyState = Literal["approved", "blocked", "approval_required", "review"]
POLICY_RULES: list[dict[str, Any]] = [
# Hard blocks — never executed
{
"rule_id": "block_cold_whatsapp",
"action": "send_whatsapp",
"when": {"source": "cold_list", "consent": False},
"decision": "blocked",
"reason_ar": "WhatsApp البارد محظور بدون lawful basis (PDPL م.5).",
},
{
"rule_id": "block_payment_no_confirm",
"action": "charge_payment",
"when": {"user_confirmed": False},
"decision": "blocked",
"reason_ar": "الخصم يحتاج تأكيد المستخدم على Moyasar — لا charge مباشر.",
},
{
"rule_id": "block_secrets_in_payload",
"action": "*",
"when": {"payload_contains_secret": True},
"decision": "blocked",
"reason_ar": "تم اكتشاف secret في الـ payload — حماية تلقائية.",
},
# Approval gates — must pass through human
{
"rule_id": "external_send_needs_approval",
"action": "send_whatsapp,send_email,send_inmail,post_social",
"when": {"approval_status": "pending"},
"decision": "approval_required",
"reason_ar": "كل إرسال خارجي يحتاج موافقة العميل قبل التنفيذ.",
},
{
"rule_id": "calendar_insert_needs_approval",
"action": "calendar_insert_event",
"when": {"approval_status": "pending"},
"decision": "approval_required",
"reason_ar": "إنشاء اجتماع في تقويم العميل يحتاج موافقة قبل insert.",
},
{
"rule_id": "social_dm_needs_explicit",
"action": "send_social_dm",
"when": {"explicit_permission": False},
"decision": "approval_required",
"reason_ar": "DM السوشيال يحتاج إذن صريح لكل حساب.",
},
# Needs review
{
"rule_id": "unknown_source_review",
"action": "*",
"when": {"source": "unknown"},
"decision": "approval_required",
"reason_ar": "مصدر البيانات غير محدد — يحتاج توثيق lawful basis.",
},
{
"rule_id": "high_value_deal_review",
"action": "*",
"when": {"deal_value_sar_gte": 100_000},
"decision": "approval_required",
"reason_ar": "صفقة قيمتها ≥100K ريال — راجعها قبل التنفيذ.",
},
# Allowed (default for safe paths)
{
"rule_id": "draft_only_safe",
"action": "create_draft,read_data,classify_reply",
"when": {},
"decision": "allow",
"reason_ar": "إجراء داخلي آمن — لا يخرج للعميل النهائي.",
},
]
@dataclass
class PolicyDecision:
"""Output of evaluate_action."""
decision: str # allow / approval_required / blocked
matched_rule_id: str | None
reasons_ar: list[str] = field(default_factory=list)
suggested_next_action_ar: str = ""
def evaluate_action( def evaluate_action(
*, *,
action: str, action: str,
channel_id: str,
context: dict[str, Any] | None = None, context: dict[str, Any] | None = None,
) -> PolicyDecision: ) -> dict[str, Any]:
""" """
Evaluate a proposed action against the policy rules. Rules:
- External-ish sends approval_required unless explicitly internal draft.
First matching rule wins. Default: needs_review (defensive). - Cold WhatsApp blocked when ``intent`` is cold/campaign_cold.
- Payment approval_required + confirm flag if amount present.
- Unknown channel review.
""" """
ctx = context or {} ctx = context or {}
matched_reasons: list[str] = [] reason_ar = ""
final_decision = "allow" state: PolicyState = "approval_required"
matched_rule_id: str | None = None
next_action = "ready_for_execution"
for rule in POLICY_RULES: known = {
# Action match (comma-separated list, "*" = match-any) "whatsapp",
applicable_actions = rule["action"].split(",") if rule["action"] != "*" else [action] "email",
if action not in applicable_actions and rule["action"] != "*": "linkedin_lead_form",
continue "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 if channel_id == "whatsapp" and action in ("send", "send_live", "external_send"):
when = rule["when"] intent = str(ctx.get("intent") or "").lower()
cond_match = True audience = str(ctx.get("audience") or "").lower()
for k, expected in when.items(): cold_markers = ("cold", "campaign_cold", "purchased_list", "unknown_opt_in")
if k.endswith("_gte"): if intent in cold_markers or audience in cold_markers:
attr = k[:-4] return {
if not (float(ctx.get(attr, 0)) >= float(expected)): "state": "blocked",
cond_match = False "reason_ar": "الواتساب البارد أو قوائم غير موثقة محظور حتى موافقة امتثال وتسجيل opt-in.",
break "action": action,
elif k == "payload_contains_secret": "channel_id": channel_id,
if expected and not _has_secret_marker(ctx.get("payload", {})): }
cond_match = False settings = get_settings()
break if action == "send_live" and not settings.whatsapp_allow_live_send:
elif ctx.get(k) != expected: return {
cond_match = False "state": "blocked",
break "reason_ar": "الإرسال الحي للواتساب معطّل في الإعدادات (WHATSAPP_ALLOW_LIVE_SEND=false).",
"action": action,
"channel_id": channel_id,
}
if not cond_match: if action in ("send", "send_live", "external_send", "smtp_send"):
continue state = "approval_required"
reason_ar = "أي إرسال خارجي يتطلب موافقة بشرية في هذا الإصدار."
decision = rule["decision"] if action in ("payment_charge", "payment_capture", "moyasar_charge"):
matched_reasons.append(rule["reason_ar"]) state = "approval_required"
matched_rule_id = rule["rule_id"] if not ctx.get("user_confirmed"):
reason_ar = "عمليات الدفع تتطلب تأكيداً صريحاً من المشغّل قبل التنفيذ."
else:
reason_ar = "تم تسجيل تأكيد المشغّل — ما زال التنفيذ الفعلي معطّلاً في MVP."
if decision == "blocked": if action in ("draft_only", "draft_message", "draft_email"):
return PolicyDecision( state = "approved"
decision="blocked", reason_ar = "مسودة داخلية — مسموح للعرض فقط."
matched_rule_id=matched_rule_id,
reasons_ar=matched_reasons,
suggested_next_action_ar="معالجة سبب الحظر قبل المحاولة مرة أخرى.",
)
if decision == "approval_required":
final_decision = "approval_required"
next_action = "operator_approves_then_execute"
# 'allow' rules just confirm — keep looking for stricter rule
return PolicyDecision( return {"state": state, "reason_ar": reason_ar or "قرار سياسة افتراضي.", "action": action, "channel_id": channel_id}
decision=final_decision,
matched_rule_id=matched_rule_id,
reasons_ar=matched_reasons or ["لا قاعدة مطابقة — الإجراء آمن افتراضياً."],
suggested_next_action_ar=next_action,
)
# ── Helpers ──────────────────────────────────────────────────────
_SECRET_MARKERS = ("api_key", "secret_key", "private_key", "password", "ghp_", "sk-ant-", "moyasar_secret")
def _has_secret_marker(payload: dict[str, Any]) -> bool:
"""Cheap heuristic check — production pairs this with a stronger scanner."""
if not isinstance(payload, dict):
return False
flat = str(payload).lower()
return any(marker in flat for marker in _SECRET_MARKERS)

View File

@ -1,213 +1,69 @@
""" """Channel capabilities — registered-only social channels, no OAuth in MVP."""
Channel Registry 11 supported channels with capabilities + risk profile.
Each channel declares: capabilities, beta_status, required_permissions,
allowed_actions, blocked_actions, risk_level. Used by the action policy
engine and the unified inbox.
"""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any from typing import Any
_CHANNEL_DEFS: list[dict[str, Any]] = [
@dataclass(frozen=True) {
class Channel: "id": "whatsapp",
"""A connected channel + what it can / cannot do.""" "label_ar": "واتساب للأعمال",
"beta_status": "pilot",
key: str "risk_level": "high",
label_ar: str "allowed_actions": ["draft_message", "template_preview"],
label_en: str "blocked_actions": ["cold_outreach_auto", "bulk_send_without_approval"],
capabilities: tuple[str, ...] },
beta_status: str # ga / beta / experimental / planned {
required_permissions: tuple[str, ...] "id": "email",
allowed_actions: tuple[str, ...] "label_ar": "البريد",
blocked_actions: tuple[str, ...] "beta_status": "ga_ready",
risk_level: str # low / medium / high "risk_level": "medium",
notes_ar: str = "" "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 ──────────────────────────────────── def list_channels() -> dict[str, Any]:
ALL_CHANNELS: tuple[Channel, ...] = ( return {"channels": list(_CHANNEL_DEFS), "demo": True}
Channel(
key="whatsapp",
label_ar="واتساب",
label_en="WhatsApp Business / Cloud",
capabilities=(
"inbound_messages", "outbound_template_messages",
"interactive_buttons_max_3", "media_send", "opt_out_handling",
),
beta_status="ga",
required_permissions=(
"waba_account_id", "phone_number_id", "verified_business",
),
allowed_actions=("draft_message", "send_with_approval", "track_reply"),
blocked_actions=("cold_send_without_consent", "bulk_unsolicited_send"),
risk_level="medium",
notes_ar="حد 3 buttons تفاعلية. الإرسال البارد محظور بدون lawful basis.",
),
Channel(
key="gmail",
label_ar="Gmail (إيميل العميل)",
label_en="Gmail OAuth",
capabilities=(
"create_draft_only", "read_labeled_threads",
"list_unsubscribe_header_attached",
),
beta_status="ga",
required_permissions=("gmail.compose",),
allowed_actions=("create_draft", "read_thread"),
blocked_actions=("send_without_user_click", "delete_messages"),
risk_level="low",
notes_ar="نكتفي بـ scope `gmail.compose`. المستخدم يضغط Send بنفسه.",
),
Channel(
key="google_calendar",
label_ar="Google Calendar",
label_en="Google Calendar API",
capabilities=(
"events_insert_with_meet", "events_list",
"rfc5545_recurrence", "asia_riyadh_timezone",
),
beta_status="ga",
required_permissions=("calendar.events",),
allowed_actions=("draft_event", "create_event_with_approval"),
blocked_actions=("delete_other_attendees_events", "modify_external_events_silently"),
risk_level="low",
notes_ar="conferenceDataVersion=1 لإضافة Google Meet.",
),
Channel(
key="linkedin_lead_forms",
label_ar="LinkedIn Lead Gen Forms",
label_en="LinkedIn Lead Gen Forms API",
capabilities=(
"ingest_leads_from_ads", "hidden_field_tracking",
"crm_sync",
),
beta_status="beta",
required_permissions=("r_marketing_leadgen_automation",),
allowed_actions=("ingest_lead_form", "trigger_followup_draft"),
blocked_actions=("scrape_profiles", "unsolicited_inmails_at_scale"),
risk_level="low",
notes_ar="مصدر رسمي لـ leads مؤهلة.",
),
Channel(
key="x_api",
label_ar="X (Twitter)",
label_en="X API v2",
capabilities=(
"post_tweet", "read_mentions",
"user_lookups_basic", "webhooks_account_activity_paid",
),
beta_status="experimental",
required_permissions=("oauth2_user_context",),
allowed_actions=("draft_post", "ingest_mention", "draft_dm_reply"),
blocked_actions=("auto_dm_strangers", "scrape_user_lists"),
risk_level="medium",
notes_ar="بعض الـ webhooks Enterprise-only. نقتصر على ما تتيحه الخطة الحالية.",
),
Channel(
key="instagram_graph",
label_ar="Instagram (Graph API)",
label_en="Instagram Graph API",
capabilities=(
"read_business_messages", "publish_posts",
"read_comments_on_owned_posts",
),
beta_status="beta",
required_permissions=("instagram_basic", "instagram_manage_messages"),
allowed_actions=("draft_reply", "ingest_comment", "ingest_dm"),
blocked_actions=("auto_dm_strangers", "scrape_unrelated_users"),
risk_level="medium",
notes_ar="فقط للحسابات Business + ما يخص العميل المتصل.",
),
Channel(
key="google_business_profile",
label_ar="Google Business Profile",
label_en="Google Business Profile API",
capabilities=(
"read_reviews", "post_replies",
"publish_local_posts", "manage_location_info",
),
beta_status="ga",
required_permissions=("business.manage",),
allowed_actions=("draft_review_reply", "draft_local_post"),
blocked_actions=("delete_real_reviews"),
risk_level="low",
notes_ar="مهم للمتاجر والعيادات والفروع — السمعة المحلية.",
),
Channel(
key="google_sheets",
label_ar="Google Sheets",
label_en="Google Sheets API",
capabilities=("read_range", "append_row", "watch_changes"),
beta_status="ga",
required_permissions=("spreadsheets.readonly", "spreadsheets",),
allowed_actions=("import_contacts", "sync_pipeline", "log_actions"),
blocked_actions=("delete_user_sheets"),
risk_level="low",
notes_ar="أداة مفيدة للتكامل مع عمليات العميل اليدوية.",
),
Channel(
key="crm",
label_ar="CRM (Zoho/HubSpot/Salla/Odoo)",
label_en="CRM via REST/SDK",
capabilities=(
"deal_sync", "contact_sync", "activity_log",
),
beta_status="planned",
required_permissions=("crm_api_token",),
allowed_actions=("read_deals", "update_stage_with_approval"),
blocked_actions=("delete_deals_silently"),
risk_level="medium",
notes_ar="بناء adapter لكل CRM في مرحلة لاحقة.",
),
Channel(
key="moyasar",
label_ar="Moyasar (مدفوعات)",
label_en="Moyasar Payments",
capabilities=(
"create_payment_link", "create_invoice",
"webhook_paid_failed_refunded", "refund",
),
beta_status="ga",
required_permissions=("publishable_key", "secret_key"),
allowed_actions=("draft_payment_link", "send_invoice_email"),
blocked_actions=("charge_card_without_user_action"),
risk_level="high",
notes_ar="بطاقة العميل تُدخَل على Moyasar (PCI-safe). لا تخزين خانات.",
),
Channel(
key="website_forms",
label_ar="نماذج الموقع",
label_en="Website Forms",
capabilities=("ingest_submission", "trigger_workflow"),
beta_status="ga",
required_permissions=("webhook_endpoint",),
allowed_actions=("ingest_lead", "draft_thankyou_message"),
blocked_actions=(),
risk_level="low",
notes_ar="مصدر leads مؤهَّلة بطبيعتها — أساس قانوني واضح.",
),
)
def get_channel(key: str) -> Channel | None:
for c in ALL_CHANNELS:
if c.key == key:
return c
return None
def channels_summary() -> dict[str, Any]:
by_status: dict[str, int] = {}
by_risk: dict[str, int] = {}
for c in ALL_CHANNELS:
by_status[c.beta_status] = by_status.get(c.beta_status, 0) + 1
by_risk[c.risk_level] = by_risk.get(c.risk_level, 0) + 1
return {
"total": len(ALL_CHANNELS),
"by_beta_status": by_status,
"by_risk_level": by_risk,
}

View File

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

View File

@ -1,110 +1,74 @@
""" """Unified event types and field validation — no transport."""
Omni-Channel Event Bus every channel emits typed events here.
Pure structures + helpers; the actual transport (Redis/Kafka) lives in a
production adapter. This module is testable in isolation.
"""
from __future__ import annotations from __future__ import annotations
import uuid from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any from typing import Any
# ── Event taxonomy ──────────────────────────────────────────────── class EventType(str, Enum):
EVENT_TYPES: tuple[str, ...] = ( """Stable event type names for platform ingest and internal cards."""
# WhatsApp
"whatsapp.message_received", LEAD_RECEIVED = "lead_received"
"whatsapp.message_sent", EXTERNAL_SEND_REQUESTED = "external_send_requested"
"whatsapp.opt_out", PAYMENT_INTENT = "payment_intent"
# Email (Gmail or company SMTP) WHATSAPP_MESSAGE_REQUESTED = "whatsapp_message_requested"
"email.received", REVIEW_REQUIRED = "review_required"
"email.draft_created", DRAFT_CREATED = "draft_created"
"email.sent", # Omni-channel extensions (dotted names) — backward compatible with existing types.
# Calendar EMAIL_RECEIVED = "email.received"
"calendar.meeting_scheduled", CALENDAR_MEETING_SCHEDULED = "calendar.meeting_scheduled"
"calendar.meeting_held", SOCIAL_COMMENT_RECEIVED = "social.comment_received"
"calendar.no_show", SOCIAL_DM_RECEIVED = "social.dm_received"
# Social (X / LinkedIn / Instagram / Facebook) LEAD_FORM_SUBMITTED = "lead.form_submitted"
"social.comment_received", PAYMENT_PAID = "payment.paid"
"social.dm_received", PAYMENT_FAILED = "payment.failed"
"social.mention_received", REVIEW_CREATED = "review.created"
"social.lead_form_submitted", PARTNER_SUGGESTED = "partner.suggested"
# Website + CRM ACTION_APPROVED = "action.approved"
"lead.form_submitted", ACTION_BLOCKED = "action.blocked"
"lead.crm_imported",
# Payments (Moyasar)
"payment.initiated",
"payment.paid",
"payment.failed",
"payment.refunded",
# Reviews / reputation (Google Business Profile)
"review.created",
"review.replied",
# Partners
"partner.suggested",
"partner.intro_made",
# Internal lifecycle
"action.requested",
"action.approved",
"action.rejected",
"action.executed",
"action.blocked",
# Sheets / CRM sync
"sheet.row_added",
"crm.deal_updated",
)
# ── Event envelope ──────────────────────────────────────────────── _REQUIRED: dict[EventType, tuple[str, ...]] = {
@dataclass(frozen=True) EventType.LEAD_RECEIVED: ("source", "channel_id"),
class PlatformEvent: EventType.EXTERNAL_SEND_REQUESTED: ("channel_id", "action"),
"""Immutable platform event.""" EventType.PAYMENT_INTENT: ("amount_halalas", "currency"),
EventType.WHATSAPP_MESSAGE_REQUESTED: ("intent", "audience"),
event_id: str EventType.REVIEW_REQUIRED: ("reason_code",),
event_type: str EventType.DRAFT_CREATED: ("draft_kind",),
channel: str # whatsapp / gmail / google_calendar / x / ... EventType.EMAIL_RECEIVED: ("channel_id", "subject_ar"),
customer_id: str EventType.CALENDAR_MEETING_SCHEDULED: ("channel_id", "title_ar"),
occurred_at: datetime EventType.SOCIAL_COMMENT_RECEIVED: ("channel_id", "snippet_ar"),
payload: dict[str, Any] = field(default_factory=dict) EventType.SOCIAL_DM_RECEIVED: ("channel_id", "sender_hint"),
correlation_id: str | None = None EventType.LEAD_FORM_SUBMITTED: ("source", "channel_id"),
actor: str = "system" EventType.PAYMENT_PAID: ("amount_halalas", "currency"),
EventType.PAYMENT_FAILED: ("amount_halalas", "reason_code"),
def to_dict(self) -> dict[str, Any]: EventType.REVIEW_CREATED: ("channel_id", "rating"),
return { EventType.PARTNER_SUGGESTED: ("partner_name_ar", "sector"),
"event_id": self.event_id, EventType.ACTION_APPROVED: ("action_id", "actor"),
"event_type": self.event_type, EventType.ACTION_BLOCKED: ("action_id", "reason_code"),
"channel": self.channel, }
"customer_id": self.customer_id,
"occurred_at": self.occurred_at.isoformat(),
"payload": self.payload,
"correlation_id": self.correlation_id,
"actor": self.actor,
}
def make_event( def validate_event(payload: dict[str, Any]) -> dict[str, Any]:
*, """
event_type: str, Validate ``event_type`` and required keys. Unknown types are rejected
channel: str, (forces explicit extension rather than silent typos).
customer_id: str, """
payload: dict[str, Any] | None = None, errors: list[str] = []
correlation_id: str | None = None, raw_type = payload.get("event_type")
actor: str = "system", if not isinstance(raw_type, str) or not raw_type.strip():
occurred_at: datetime | None = None, return {"valid": False, "errors": ["event_type_required"], "normalized": None}
) -> PlatformEvent:
"""Construct a validated event.""" try:
if event_type not in EVENT_TYPES: et = EventType(raw_type.strip())
raise ValueError(f"unknown event_type: {event_type}") except ValueError:
return PlatformEvent( return {"valid": False, "errors": [f"unknown_event_type:{raw_type}"], "normalized": None}
event_id=f"pevt_{uuid.uuid4().hex[:24]}",
event_type=event_type, for key in _REQUIRED[et]:
channel=channel, if key not in payload or payload[key] in (None, ""):
customer_id=customer_id, errors.append(f"missing_field:{key}")
occurred_at=occurred_at or datetime.now(timezone.utc).replace(tzinfo=None),
payload=payload or {}, normalized = {"event_type": et.value, **{k: v for k, v in payload.items() if k != "event_type"}}
correlation_id=correlation_id, normalized["event_type"] = et.value
actor=actor, return {"valid": len(errors) == 0, "errors": errors, "normalized": normalized if not errors else None}
)

View File

@ -1,91 +1,22 @@
""" """Deterministic identity merge demo — no external graph DB."""
Identity Resolution reconcile signals from many channels into one Identity.
Inputs: phone, email, company, social handles, CRM ids.
Output: a single Identity record with confidence per matched signal.
Pure deterministic production version would hit a graph DB.
"""
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
from dataclasses import dataclass, field
from typing import Any from typing import Any
@dataclass def resolve_identity_demo(
class Identity: *,
"""A reconciled identity across channels.""" phone: str | None = None,
email: str | None = None,
identity_id: str company_hint: str | None = None,
primary_phone: str | None = None ) -> dict[str, Any]:
primary_email: str | None = None parts = "|".join([p or "" for p in (phone, email, company_hint)])
company: str | None = None hid = hashlib.sha256(parts.encode("utf-8")).hexdigest()[:16]
crm_id: str | None = None return {
social_handles: dict[str, str] = field(default_factory=dict) "identity_key": f"id_{hid}",
confidence: float = 0.0 # 0..1 "signals": {"phone": phone, "email": email, "company_hint": company_hint},
sources: list[str] = field(default_factory=list) "note_ar": "دمج تجريبي — ربط CRM وsocial handles لاحقاً.",
"demo": True,
}
def _hash_id(*parts: str) -> str:
"""Deterministic ID from any combination of stable identifiers."""
seed = "|".join(p.lower().strip() for p in parts if p)
if not seed:
return ""
h = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
return f"id_{h}"
def resolve_identity(*, signals: list[dict[str, Any]]) -> Identity:
"""
Merge a list of signals (from different channels) into one Identity.
Each signal can be: {phone, email, company, crm_id, social_handles, source}.
"""
phones: dict[str, int] = {}
emails: dict[str, int] = {}
companies: dict[str, int] = {}
crm_ids: list[str] = []
socials: dict[str, str] = {}
sources: list[str] = []
for s in signals:
ph = (s.get("phone") or "").strip()
em = (s.get("email") or "").strip().lower()
co = (s.get("company") or "").strip()
crm = (s.get("crm_id") or "").strip()
if ph:
phones[ph] = phones.get(ph, 0) + 1
if em:
emails[em] = emails.get(em, 0) + 1
if co:
companies[co] = companies.get(co, 0) + 1
if crm:
crm_ids.append(crm)
for k, v in (s.get("social_handles") or {}).items():
if k not in socials and v:
socials[k] = v
if s.get("source"):
sources.append(str(s["source"]))
# Pick most-frequent canonical values
primary_phone = max(phones, key=phones.get) if phones else None
primary_email = max(emails, key=emails.get) if emails else None
company = max(companies, key=companies.get) if companies else None
crm_id = crm_ids[0] if crm_ids else None
# Confidence: proportional to number of independent strong signals
strong_signals = sum(1 for x in (primary_phone, primary_email, crm_id) if x)
confidence = min(1.0, 0.30 * strong_signals + 0.10 * (1 if socials else 0))
return Identity(
identity_id=_hash_id(primary_phone or "", primary_email or "", crm_id or ""),
primary_phone=primary_phone,
primary_email=primary_email,
company=company,
crm_id=crm_id,
social_handles=dict(socials),
confidence=round(confidence, 3),
sources=list(dict.fromkeys(sources)), # dedupe preserve order
)

View File

@ -0,0 +1,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}

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More