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).
# GitHub only loads workflows from the repository root .github/workflows/.
# Three jobs so branch protection can require: pytest, smoke_inprocess, launch_readiness.
name: Dealix API CI
on:
@ -18,8 +19,17 @@ defaults:
run:
working-directory: dealix
env:
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
jobs:
test:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -41,37 +51,53 @@ jobs:
run: python -m compileall api auto_client_acquisition
- name: Tests
env:
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
run: pytest -q --no-cov
- name: In-process API smoke
env:
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
run: python scripts/smoke_inprocess.py
# tests/unit expects alternate package facades; run canonical integration tests only.
run: pytest -q --no-cov --ignore=tests/unit
- name: Embeddings pipeline placeholder
run: python scripts/embeddings_pipeline_placeholder.py
- name: Deterministic eval smoke
env:
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
run: python scripts/run_evals.py
smoke_inprocess:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: dealix/requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx aiosqlite
- name: In-process API smoke
run: python scripts/smoke_inprocess.py
launch_readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: dealix/requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx aiosqlite
- name: Launch readiness (GO_PRIVATE_BETA gate)
run: python scripts/launch_readiness_check.py

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
on:
@ -17,17 +17,46 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: dealix/requirements.txt
- name: Install httpx
run: pip install httpx
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx aiosqlite
- name: Run staging smoke
env:
STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
STAGING_API_KEY: ${{ secrets.STAGING_API_KEY }}
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
run: |
if [ -z "$STAGING_BASE_URL" ]; then
echo "STAGING_BASE_URL secret not set — skipping."
exit 0
fi
python scripts/smoke_staging.py --base-url "$STAGING_BASE_URL"
- name: Launch readiness (expect PAID_BETA_READY)
env:
STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
APP_ENV: test
APP_DEBUG: "false"
ANTHROPIC_API_KEY: test-anthropic-key
DEEPSEEK_API_KEY: test-deepseek-key
GROQ_API_KEY: test-groq-key
GLM_API_KEY: test-glm-key
GOOGLE_API_KEY: test-google-key
run: |
if [ -z "$STAGING_BASE_URL" ]; then
echo "STAGING_BASE_URL secret not set — skipping launch readiness."
exit 0
fi
python scripts/launch_readiness_check.py --base-url "$STAGING_BASE_URL"

View File

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

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
@ -6,45 +6,36 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.agent_observability import (
build_trace_event,
run_eval_pack,
safety_eval,
saudi_tone_eval,
)
from auto_client_acquisition.agent_observability.safety_eval import evaluate_safety
from auto_client_acquisition.agent_observability.saudi_tone_eval import evaluate_saudi_tone
from auto_client_acquisition.agent_observability.trace_events import build_trace_event
router = APIRouter(prefix="/api/v1/agent-observability", tags=["agent-observability"])
router = APIRouter(prefix="/api/v1/agent-observability", tags=["agent_observability"])
@router.get("/demo")
async def demo() -> dict[str, Any]:
return {"ok": True, "message_ar": "تتبع وتقييم — اربط Langfuse في staging للإنتاج.", "demo": True}
@router.post("/eval/safety")
async def eval_safety(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return evaluate_safety(str(payload.get("text_ar") or ""))
@router.post("/eval/saudi-tone")
async def eval_saudi_tone(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return evaluate_saudi_tone(str(payload.get("text_ar") or ""))
@router.post("/trace/build")
async def trace_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
async def trace_build(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_trace_event(
workflow_name=payload.get("workflow_name", "unknown"),
agent_name=payload.get("agent_name", "unknown"),
status=payload.get("status", "started"),
user_id=payload.get("user_id"),
company_id=payload.get("company_id"),
tool=payload.get("tool"),
policy_result=payload.get("policy_result"),
risk_level=payload.get("risk_level"),
approval_status=payload.get("approval_status"),
latency_ms=float(payload.get("latency_ms", 0)),
cost_estimate=float(payload.get("cost_estimate", 0)),
payload=payload.get("payload"),
output=payload.get("output"),
workflow_name=str(payload.get("workflow_name") or "demo"),
agent_name=str(payload.get("agent_name") or "dealix"),
action_type=str(payload.get("action_type") or "draft"),
policy_result=str(payload.get("policy_result") or "approval_required"),
tool_called=payload.get("tool_called"),
outcome=payload.get("outcome"),
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@router.post("/safety/eval")
async def safety_eval_endpoint(text: str = Body(..., embed=True)) -> dict[str, Any]:
return safety_eval(text)
@router.post("/tone/eval")
async def tone_eval(text: str = Body(..., embed=True)) -> dict[str, Any]:
return saudi_tone_eval(text)
@router.get("/evals/run")
async def evals_run() -> dict[str, Any]:
return run_eval_pack()

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

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 typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.customer_ops import (
SUPPORT_PRIORITIES,
SUPPORTED_CONNECTORS,
build_at_risk_alert,
build_connector_setup_summary,
build_customer_success_plan,
build_first_response_template,
build_incident_response_plan,
build_onboarding_checklist,
build_sla_health_report,
build_weekly_check_in,
classify_sla_breach,
classify_ticket_priority,
record_sla_event,
route_ticket,
triage_incident,
update_connector_status,
update_onboarding_step,
)
from auto_client_acquisition.customer_ops.connector_setup_status import build_connector_status
from auto_client_acquisition.customer_ops.customer_success_cadence import build_weekly_cadence
from auto_client_acquisition.customer_ops.incident_router import build_incident_playbook, classify_incident
from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist
from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary
from auto_client_acquisition.customer_ops.support_ticket_router import route_ticket
router = APIRouter(prefix="/api/v1/customer-ops", tags=["customer-ops"])
# ── Onboarding ───────────────────────────────────────────────
@router.post("/onboarding/checklist")
async def onboarding_checklist(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_onboarding_checklist(
customer_id=payload.get("customer_id", ""),
company_name=payload.get("company_name", ""),
bundle_id=payload.get("bundle_id"),
)
@router.get("/onboarding/checklist")
async def onboarding_checklist(service_id: str | None = None) -> dict[str, object]:
return build_onboarding_checklist(service_id)
@router.post("/onboarding/update-step")
async def onboarding_update_step(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return update_onboarding_step(
payload.get("checklist") or {},
step_id=payload.get("step_id", ""),
completed=bool(payload.get("completed", True)),
notes=payload.get("notes", ""),
)
@router.get("/support/sla")
async def support_sla() -> dict[str, object]:
return build_sla_summary()
@router.get("/onboarding/checklist/demo")
async def onboarding_checklist_demo() -> dict[str, Any]:
return build_onboarding_checklist(
customer_id="demo", company_name="شركة نمو للتدريب",
bundle_id="growth_starter",
)
@router.get("/connectors/status")
async def connectors_status() -> dict[str, object]:
return build_connector_status()
# ── Connectors ───────────────────────────────────────────────
@router.get("/connectors/catalog")
async def connectors_catalog() -> dict[str, Any]:
return {
"total": len(SUPPORTED_CONNECTORS),
"connectors": [dict(c) for c in SUPPORTED_CONNECTORS],
}
@router.get("/success/cadence")
async def success_cadence() -> dict[str, object]:
return build_weekly_cadence()
@router.post("/connectors/summary")
async def connectors_summary(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_connector_setup_summary(
customer_id=payload.get("customer_id", ""),
statuses=payload.get("statuses"),
)
@router.post("/connectors/update")
async def connectors_update(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
statuses = payload.get("statuses") or {}
try:
return {"statuses": update_connector_status(
statuses,
connector_key=payload.get("connector_key", ""),
state=payload.get("state", "not_started"),
notes=payload.get("notes", ""),
)}
except ValueError as exc:
return {"error": str(exc)}
@router.get("/connectors/demo")
async def connectors_demo() -> dict[str, Any]:
return build_connector_setup_summary(
customer_id="demo",
statuses={
"gmail": {"state": "connected_draft_only"},
"google_calendar": {"state": "connected_draft_only"},
"moyasar": {"state": "configuring"},
"whatsapp_cloud": {"state": "not_started"},
},
)
# ── Support ──────────────────────────────────────────────────
@router.get("/support/priorities")
async def support_priorities() -> dict[str, Any]:
return {"priorities": [dict(p) for p in SUPPORT_PRIORITIES]}
@router.post("/support/classify")
async def support_classify(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return classify_ticket_priority(payload.get("text", ""))
@router.get("/incidents/playbook")
async def incidents_playbook() -> dict[str, object]:
return build_incident_playbook()
@router.post("/support/route")
async def support_route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return route_ticket(
text=payload.get("text", ""),
customer_id=payload.get("customer_id", ""),
contact_email=payload.get("contact_email", ""),
)
async def support_route(payload: dict[str, object] = Body(default_factory=dict)) -> dict[str, object]:
issue = str(payload.get("issue_ar") or "")
return route_ticket(issue)
@router.get("/support/first-response/{priority}")
async def support_first_response(priority: str) -> dict[str, Any]:
return build_first_response_template(priority)
# ── SLA ──────────────────────────────────────────────────────
@router.post("/sla/event")
async def sla_event(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return record_sla_event(
ticket_id=payload.get("ticket_id", ""),
priority=payload.get("priority", "P3"),
event=payload.get("event", "opened"),
)
except ValueError as exc:
return {"error": str(exc)}
@router.post("/sla/classify-breach")
async def sla_classify_breach(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return classify_sla_breach(
priority=payload.get("priority", "P3"),
minutes_to_first_response=payload.get("minutes_to_first_response"),
hours_to_resolve=payload.get("hours_to_resolve"),
)
@router.post("/sla/health-report")
async def sla_health_report(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_sla_health_report(tickets=payload.get("tickets") or [])
@router.get("/sla/health-report/demo")
async def sla_health_report_demo() -> dict[str, Any]:
return build_sla_health_report(tickets=[
{"priority": "P0", "first_response_min": 12, "resolution_hours": 2.5},
{"priority": "P1", "first_response_min": 90, "resolution_hours": 18},
{"priority": "P2", "first_response_min": 600, "resolution_hours": 70},
{"priority": "P3", "first_response_min": 1200, "resolution_hours": 100},
])
# ── Incidents ────────────────────────────────────────────────
@router.post("/incidents/triage")
async def incidents_triage(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return triage_incident(
title=payload.get("title", ""),
description=payload.get("description", ""),
affected_customers=int(payload.get("affected_customers", 1)),
has_data_leak=bool(payload.get("has_data_leak", False)),
has_unauthorized_send=bool(payload.get("has_unauthorized_send", False)),
)
@router.get("/incidents/response-plan/{severity}")
async def incidents_response_plan(severity: str) -> dict[str, Any]:
return build_incident_response_plan(severity=severity)
# ── Customer Success ─────────────────────────────────────────
@router.post("/cs/weekly-check-in")
async def cs_weekly_check_in(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_weekly_check_in(
customer_id=payload.get("customer_id", ""),
company_name=payload.get("company_name", ""),
metrics=payload.get("metrics"),
)
@router.post("/cs/at-risk-alert")
async def cs_at_risk_alert(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_at_risk_alert(
customer_id=payload.get("customer_id", ""),
days_inactive=int(payload.get("days_inactive", 0)),
drafts_pending=int(payload.get("drafts_pending", 0)),
last_proof_pack_days_ago=int(payload.get("last_proof_pack_days_ago", 0)),
)
@router.post("/cs/success-plan")
async def cs_success_plan(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_customer_success_plan(
customer_id=payload.get("customer_id", ""),
bundle_id=payload.get("bundle_id", "growth_starter"),
)
@router.get("/incidents/classify")
async def incidents_classify(severity: str = "P3") -> dict[str, object]:
return classify_incident(severity)

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
@ -6,95 +6,33 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.growth_curator import (
build_weekly_curator_report,
detect_duplicates,
grade_message,
inventory_skills,
recommend_next_mission,
suggest_improvement,
)
from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report
from auto_client_acquisition.growth_curator.message_curator import grade_message
from auto_client_acquisition.growth_curator.mission_curator import curate_missions_weekly
from auto_client_acquisition.growth_curator.skill_inventory import list_skill_inventory
router = APIRouter(prefix="/api/v1/growth-curator", tags=["growth-curator"])
@router.get("/skills/inventory")
async def skills_inventory() -> dict[str, Any]:
return inventory_skills()
@router.post("/messages/grade")
async def messages_grade(
message: str = Body(..., embed=True),
sector: str | None = Body(default=None, embed=True),
channel: str = Body(default="whatsapp", embed=True),
) -> dict[str, Any]:
return grade_message(message, sector=sector, channel=channel).to_dict()
@router.post("/messages/improve")
async def messages_improve(
message: str = Body(..., embed=True),
sector: str | None = Body(default=None, embed=True),
) -> dict[str, Any]:
return suggest_improvement(message, sector=sector)
@router.post("/messages/duplicates")
async def messages_duplicates(
messages: list[str] = Body(..., embed=True),
threshold: float = Body(default=0.85, embed=True),
) -> dict[str, Any]:
pairs = detect_duplicates(messages, threshold=threshold)
return {
"pairs": [{"i": i, "j": j, "similarity": s} for i, j, s in pairs],
"count": len(pairs),
}
@router.post("/missions/next")
async def missions_next(
history: list[dict[str, Any]] = Body(default_factory=list, embed=True),
growth_brain: dict[str, Any] | None = Body(default=None, embed=True),
) -> dict[str, Any]:
return recommend_next_mission(history, growth_brain=growth_brain)
@router.post("/report/weekly")
async def report_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_weekly_curator_report(
messages=payload.get("messages", []),
playbooks=payload.get("playbooks", []),
missions=payload.get("missions", []),
sector=payload.get("sector"),
)
router = APIRouter(prefix="/api/v1/growth-curator", tags=["growth_curator"])
@router.get("/report/demo")
async def report_demo() -> dict[str, Any]:
"""Demo curator report with a small synthetic dataset."""
return build_weekly_curator_report(
messages=[
{"id": "m1", "text": "هلا أحمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"},
{"id": "m2", "text": "هلا محمد، لاحظت توسعكم في المبيعات. يناسبك أعرض لك Pilot 7 أيام؟"},
{"id": "m3", "text": "آخر فرصة! ضمان 100% نتائج مضمونة!"},
{"id": "m4", "text": "Hi"},
],
playbooks=[
{"id": "pb1", "title": "Warm B2B intro - training", "used_count": 20,
"accept_count": 12, "replied_count": 8, "meeting_count": 4, "deal_count": 2,
"sectors": "training"},
{"id": "pb2", "title": "Warm B2B intro - training-ksa", "used_count": 8,
"accept_count": 4, "replied_count": 2, "meeting_count": 1, "deal_count": 0,
"sectors": "training"},
{"id": "pb3", "title": "Cold call SaaS", "used_count": 50,
"accept_count": 5, "replied_count": 2, "meeting_count": 0, "deal_count": 0,
"sectors": "saas"},
],
missions=[
{"mission_id": "first_10_opportunities", "opportunities_generated": 10,
"drafts_approved": 4, "meetings_booked": 2, "revenue_influenced_sar": 18000,
"time_to_value_minutes": 8, "risks_blocked": 2},
],
sector="training",
return build_weekly_curator_report()
@router.post("/messages/grade")
async def messages_grade(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return grade_message(
str(payload.get("message_ar") or ""),
sector=str(payload.get("sector") or ""),
channel=str(payload.get("channel") or "whatsapp"),
)
@router.get("/skills/demo")
async def skills_demo() -> dict[str, Any]:
return list_skill_inventory()
@router.get("/missions/curate/demo")
async def missions_curate_demo() -> dict[str, Any]:
return curate_missions_weekly()

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 /
scheduled live from this router; that happens in dedicated send / billing
/ calendar services after explicit user approval.
لا يكرر منطق ten-in-ten؛ يعرّف مسارات متوقعة في وثائق الـ beta والـ smoke.
"""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Body, Query
from fastapi import APIRouter
from auto_client_acquisition.growth_operator import (
build_calendar_draft,
build_meeting_agenda,
build_moyasar_payment_link_draft,
build_post_meeting_followup,
build_weekly_proof_pack,
contactability_summary,
dedupe_contacts,
draft_arabic_message,
draft_followup,
draft_objection_response,
draft_partner_outreach,
list_missions,
partner_scorecard,
profile_from_dict,
recommend_top_10,
run_mission,
score_contactability,
suggest_partner_types,
summarize_import,
)
from auto_client_acquisition.business.proof_pack import build_demo_proof_pack
from auto_client_acquisition.innovation.growth_missions import list_growth_missions
router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth-operator"])
log = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/growth-operator", tags=["growth_operator"])
# ── 1. Contacts: import preview ─────────────────────────────────
@router.post("/contacts/import-preview")
async def contacts_import_preview(
contacts: list[dict[str, Any]] = Body(default_factory=list, embed=True),
channel: str = Body(default="whatsapp", embed=True),
) -> dict[str, Any]:
"""Preview import: dedupe + source classify + contactability summary."""
deduped = dedupe_contacts(contacts)
return {
"import_summary": summarize_import(contacts),
"contactability": contactability_summary(deduped, channel=channel),
"policy_note_ar": (
"العميل يرفع أرقام مملوكة/مصرح بها. لا cold WhatsApp بدون lawful basis."
),
"approval_required": True,
"approval_status": "pending_approval",
}
# ── 2. Targeting: top-10 ────────────────────────────────────────
@router.post("/targets/top-10")
async def targets_top_10(
contacts: list[dict[str, Any]] = Body(default_factory=list, embed=True),
sector_hint: str = Body(default="", embed=True),
channel: str = Body(default="whatsapp", embed=True),
) -> dict[str, Any]:
"""Rank uploaded contacts → top-10 safe + Why-Now."""
return recommend_top_10(contacts, sector_hint=sector_hint, channel=channel)
# ── 3. Messages: draft / followup / objection ──────────────────
@router.post("/messages/draft")
async def messages_draft(
contact: dict[str, Any] = Body(..., embed=True),
profile: dict[str, Any] | None = Body(default=None, embed=True),
goal_ar: str = Body(default="تشغيل نمو B2B بلا إرسال عشوائي", embed=True),
) -> dict[str, Any]:
"""Saudi-tone Arabic outreach draft (always pending_approval)."""
return draft_arabic_message(contact, profile=profile, goal_ar=goal_ar)
@router.post("/messages/followup")
async def messages_followup(
contact: dict[str, Any] = Body(..., embed=True),
days_since_last: int = Body(default=3, embed=True),
last_outcome: str = Body(default="no_reply", embed=True),
) -> dict[str, Any]:
return draft_followup(
contact, days_since_last=days_since_last, last_outcome=last_outcome,
)
@router.post("/messages/objection-response")
async def messages_objection_response(
objection_id: str = Body(..., embed=True),
contact: dict[str, Any] | None = Body(default=None, embed=True),
) -> dict[str, Any]:
return draft_objection_response(objection_id, contact=contact)
# ── 4. Partners: suggest / outreach / scorecard ────────────────
@router.post("/partners/suggest")
async def partners_suggest(
sector: str = Body(default="", embed=True),
customer_size: str = Body(default="smb", embed=True),
) -> dict[str, Any]:
return suggest_partner_types(sector=sector, customer_size=customer_size)
@router.post("/partners/outreach")
async def partners_outreach(
partner_type_key: str = Body(..., embed=True),
partner_name: str = Body(default="", embed=True),
customer_name: str = Body(default="Dealix", embed=True),
) -> dict[str, Any]:
return draft_partner_outreach(
partner_type_key=partner_type_key,
partner_name=partner_name,
customer_name=customer_name,
)
@router.post("/partners/scorecard")
async def partners_scorecard(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return partner_scorecard(
partner_id=payload.get("partner_id", "unknown"),
intros_made=int(payload.get("intros_made", 0)),
deals_influenced=int(payload.get("deals_influenced", 0)),
revenue_share_paid_sar=float(payload.get("revenue_share_paid_sar", 0)),
relationship_age_months=int(payload.get("relationship_age_months", 0)),
)
# ── 5. Meetings: agenda / calendar draft / followup ────────────
@router.post("/meetings/draft")
async def meetings_draft(
contact_name: str = Body(..., embed=True),
company: str = Body(..., embed=True),
contact_email: str | None = Body(default=None, embed=True),
purpose_ar: str = Body(default="اكتشاف وتأهيل أولي", embed=True),
duration_minutes: int = Body(default=20, embed=True),
proposed_start_iso: str | None = Body(default=None, embed=True),
) -> dict[str, Any]:
"""Build agenda + calendar draft (NOT created live)."""
agenda = build_meeting_agenda(
contact_name=contact_name,
company=company,
purpose_ar=purpose_ar,
duration_minutes=duration_minutes,
)
cal_draft = build_calendar_draft(
contact_email=contact_email,
contact_name=contact_name,
company=company,
proposed_start_iso=proposed_start_iso,
duration_minutes=duration_minutes,
)
return {"agenda": agenda, "calendar_draft": cal_draft}
@router.post("/meetings/post-followup")
async def meetings_post_followup(
contact_name: str = Body(..., embed=True),
company: str = Body(..., embed=True),
summary_ar: str = Body(..., embed=True),
next_step_ar: str = Body(default="أرسل recap + pilot offer", embed=True),
) -> dict[str, Any]:
return build_post_meeting_followup(
contact_name=contact_name,
company=company,
summary_ar=summary_ar,
next_step_ar=next_step_ar,
)
# ── 6. Payment offer (Moyasar payment-link draft) ─────────────
@router.post("/payment-offer/draft")
async def payment_offer_draft(
plan_key: str = Body(..., embed=True),
customer_id: str = Body(..., embed=True),
contact_email: str | None = Body(default=None, embed=True),
custom_amount_sar: float | None = Body(default=None, embed=True),
) -> dict[str, Any]:
return build_moyasar_payment_link_draft(
plan_key=plan_key,
customer_id=customer_id,
contact_email=contact_email,
custom_amount_sar=custom_amount_sar,
)
# ── 7. Missions ────────────────────────────────────────────────
@router.get("/missions")
async def missions_list() -> dict[str, Any]:
return list_missions()
async def missions() -> dict[str, Any]:
"""نفس محتوى ``GET /api/v1/innovation/growth-missions`` مع تسمية منتجية."""
data = list_growth_missions()
if isinstance(data, dict):
out = dict(data)
out["canonical_route"] = "/api/v1/innovation/growth-missions"
return out
return {"missions": data, "canonical_route": "/api/v1/innovation/growth-missions"}
@router.post("/missions/{mission_id}/run")
async def missions_run(
mission_id: str,
payload: dict[str, Any] = Body(default_factory=dict),
) -> dict[str, Any]:
return run_mission(mission_id, payload=payload)
# ── 8. Proof Pack demo ─────────────────────────────────────────
@router.get("/proof-pack/demo")
async def proof_pack_demo(
customer_id: str = Query(default="demo"),
customer_name: str = Query(default="Demo Saudi B2B Co."),
) -> dict[str, Any]:
return build_weekly_proof_pack(
customer_id=customer_id,
customer_name=customer_name,
week_label="W18-2026",
plan_cost_weekly_sar=750,
opportunities_discovered=42,
messages_drafted=38,
messages_approved=33,
messages_sent=33,
replies_received=11,
positive_replies=4,
meetings_booked=3,
meetings_held=2,
proposals_sent=1,
deals_won=0,
pipeline_added_sar=185_000,
revenue_won_sar=0,
risky_drafts_blocked=5,
revenue_leaks_recovered=2,
avg_response_minutes=42,
best_message_subject="ملاحظة على توسعكم في الرياض",
best_message_reply_rate=0.18,
)
# ── 9. Single-contact contactability ─────────────────────────
@router.post("/contactability/score")
async def contactability_score_single(
contact: dict[str, Any] = Body(..., embed=True),
channel: str = Body(default="whatsapp", embed=True),
) -> dict[str, Any]:
return score_contactability(contact, channel=channel)
# ── 10. Profile ────────────────────────────────────────────────
@router.post("/profile")
async def profile_set(
profile: dict[str, Any] = Body(..., embed=True),
) -> dict[str, Any]:
p = profile_from_dict(profile)
return {
"profile": p.to_dict(),
"is_specialized": p.is_specialized(),
"missing_fields_ar": (
[] if p.is_specialized() else
["sector", "city", "offer_one_liner", "ideal_customer"]
),
}
async def proof_pack_demo() -> dict[str, Any]:
"""نفس ``GET /api/v1/business/proof-pack/demo`` — مسار موحّد للعرض في الـ beta."""
pack = build_demo_proof_pack()
if isinstance(pack, dict):
out = dict(pack)
out["canonical_route"] = "/api/v1/business/proof-pack/demo"
return out
return {"pack": pack, "canonical_route": "/api/v1/business/proof-pack/demo"}

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

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
@ -6,130 +6,40 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.launch_ops import (
build_12_min_demo_flow,
build_close_script,
build_daily_launch_scorecard,
build_discovery_questions,
build_first_20_segments,
build_followup_message,
build_launch_readiness,
build_objection_responses,
build_outreach_message,
build_private_beta_offer,
build_private_beta_safety_notes,
build_reply_handlers,
build_weekly_launch_scorecard,
decide_go_no_go,
private_beta_faq,
record_launch_event,
)
from auto_client_acquisition.launch_ops.demo_flow import build_demo_script
from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go
from auto_client_acquisition.launch_ops.launch_scorecard import build_launch_scorecard
from auto_client_acquisition.launch_ops.outreach_messages import build_first_twenty_outreach
from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer
router = APIRouter(prefix="/api/v1/launch", tags=["launch-ops"])
router = APIRouter(prefix="/api/v1/launch", tags=["launch_ops"])
# ── Private Beta ─────────────────────────────────────────────
@router.get("/private-beta/offer")
async def private_beta_offer() -> dict[str, Any]:
return {
"offer": build_private_beta_offer(),
"safety": build_private_beta_safety_notes(),
"faq": private_beta_faq(),
}
async def launch_private_beta_offer() -> dict[str, Any]:
return build_private_beta_offer()
# ── Demo flow ────────────────────────────────────────────────
@router.get("/demo/flow")
async def demo_flow() -> dict[str, Any]:
return {
"flow": build_12_min_demo_flow(),
"discovery_questions": build_discovery_questions(),
"objections": build_objection_responses(),
"close": build_close_script(),
}
@router.get("/demo-script")
async def launch_demo_script() -> dict[str, Any]:
return build_demo_script()
# ── Outreach ─────────────────────────────────────────────────
@router.get("/outreach/first-20")
async def outreach_first_20() -> dict[str, Any]:
segments = build_first_20_segments()
sample_messages = {
s["id"]: build_outreach_message(s["id"])
for s in segments["segments"]
}
return {
**segments,
"sample_messages": sample_messages,
"reply_handlers": build_reply_handlers(),
}
async def launch_outreach_first_20() -> dict[str, Any]:
return build_first_twenty_outreach()
@router.post("/outreach/message")
async def outreach_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_outreach_message(
segment_id=payload.get("segment_id", ""),
name=payload.get("name", "[الاسم]"),
)
@router.get("/go-no-go")
async def launch_go_no_go_get() -> dict[str, Any]:
return evaluate_go_no_go(None)
@router.post("/outreach/followup")
async def outreach_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_followup_message(
segment_id=payload.get("segment_id", ""),
step=int(payload.get("step", 1)),
name=payload.get("name", "[الاسم]"),
)
# ── Go / No-Go ───────────────────────────────────────────────
@router.post("/go-no-go")
async def go_no_go(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return decide_go_no_go(statuses=payload.get("statuses"))
async def launch_go_no_go_post(flags: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return evaluate_go_no_go(flags or {})
@router.get("/readiness")
async def readiness() -> dict[str, Any]:
"""Readiness with all gates assumed False (use POST /go-no-go for real status)."""
return build_launch_readiness(statuses={})
# ── Scorecard ────────────────────────────────────────────────
@router.post("/scorecard/event")
async def scorecard_event(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return record_launch_event(
event_type=payload.get("event_type", ""),
customer_id=payload.get("customer_id"),
notes=payload.get("notes"),
)
except ValueError as exc:
return {"error": str(exc)}
@router.post("/scorecard/daily")
async def scorecard_daily(
events: list[dict[str, Any]] = Body(default_factory=list, embed=True),
) -> dict[str, Any]:
return build_daily_launch_scorecard(events=events)
@router.post("/scorecard/weekly")
async def scorecard_weekly(
events: list[dict[str, Any]] = Body(default_factory=list, embed=True),
) -> dict[str, Any]:
return build_weekly_launch_scorecard(events=events)
@router.get("/scorecard/demo")
async def scorecard_demo() -> dict[str, Any]:
"""Demo scorecard with synthetic events."""
demo_events = [
{"event_type": "outreach_sent"} for _ in range(15)
] + [
{"event_type": "reply_received"} for _ in range(4)
] + [
{"event_type": "demo_booked"} for _ in range(2)
] + [
{"event_type": "blocked_action"} for _ in range(6)
]
return build_daily_launch_scorecard(events=demo_events)
@router.get("/scorecard")
async def launch_scorecard() -> dict[str, Any]:
return build_launch_scorecard()

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

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
@ -6,57 +6,22 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.model_router import (
ALL_PROVIDERS,
ALL_TASK_TYPES,
build_usage_demo,
classify_cost,
route_task,
)
from auto_client_acquisition.model_router.provider_registry import list_providers
from auto_client_acquisition.model_router.task_router import list_tasks, route_task
router = APIRouter(prefix="/api/v1/model-router", tags=["model-router"])
@router.get("/providers")
async def providers() -> dict[str, Any]:
return {
"total": len(ALL_PROVIDERS),
"providers": [p.to_dict() for p in ALL_PROVIDERS],
}
router = APIRouter(prefix="/api/v1/model-router", tags=["model_router"])
@router.get("/tasks")
async def tasks() -> dict[str, Any]:
return {"total": len(ALL_TASK_TYPES), "tasks": list(ALL_TASK_TYPES)}
return list_tasks()
@router.post("/route")
async def route(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
decision = route_task(
payload.get("task_type", "low_cost_bulk"),
requires_arabic=bool(payload.get("requires_arabic", False)),
requires_vision=bool(payload.get("requires_vision", False)),
sensitivity=payload.get("sensitivity", "low"),
expected_input_tokens=int(payload.get("expected_input_tokens", 0)),
expected_output_tokens=int(payload.get("expected_output_tokens", 0)),
bulk=bool(payload.get("bulk", False)),
primary_provider=payload.get("primary_provider"),
)
return decision.to_dict()
async def route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return route_task(str(payload.get("task_type") or ""))
@router.post("/cost-class")
async def cost_class(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return {
"cost_class": classify_cost(
task_type=payload.get("task_type", "low_cost_bulk"),
expected_input_tokens=int(payload.get("expected_input_tokens", 0)),
expected_output_tokens=int(payload.get("expected_output_tokens", 0)),
bulk=bool(payload.get("bulk", False)),
),
}
@router.get("/usage/demo")
async def usage_demo() -> dict[str, Any]:
return build_usage_demo()
@router.get("/providers")
async def providers() -> dict[str, Any]:
return list_providers()

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

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 typing import Any
from fastapi import APIRouter, Body
from fastapi import APIRouter, Query
from auto_client_acquisition.revenue_launch import (
build_24h_delivery_plan,
build_499_pilot_offer,
build_case_study_free_offer,
build_client_intake_form,
build_client_summary,
build_first_10_opportunities_delivery,
build_first_20_segments_v2,
build_followup_1,
build_followup_2,
build_growth_diagnostic_delivery,
build_growth_os_pilot_offer,
build_list_intelligence_delivery,
from auto_client_acquisition.revenue_launch.demo_closer import (
build_12_min_demo_flow,
build_close_script,
build_discovery_questions,
build_objection_responses,
)
from auto_client_acquisition.revenue_launch.offer_i18n import build_revenue_offers_payload
from auto_client_acquisition.revenue_launch.outreach_sequence import (
build_first_20_segments,
build_outreach_message,
)
from auto_client_acquisition.revenue_launch.payment_manual_flow import (
build_moyasar_invoice_instructions,
build_next_step_recommendation,
build_outreach_message_v2,
build_payment_confirmation_checklist,
build_payment_link_message,
build_pipeline_schema,
build_private_beta_offer,
)
from auto_client_acquisition.revenue_launch.pilot_delivery import (
build_24h_delivery_plan,
build_client_intake_form,
build_first_10_opportunities_delivery,
build_growth_diagnostic_delivery,
build_list_intelligence_delivery,
)
from auto_client_acquisition.revenue_launch.pipeline_tracker import build_pipeline_schema
from auto_client_acquisition.revenue_launch.proof_pack_template import (
build_private_beta_proof_pack,
build_reply_handlers_v2,
demo_12_min,
demo_close_script,
demo_discovery,
demo_objections,
recommend_offer_for_segment,
summarize_pipeline,
)
router = APIRouter(prefix="/api/v1/revenue-launch", tags=["revenue-launch"])
router = APIRouter(prefix="/api/v1/revenue-launch", tags=["revenue_launch"])
# ── Offers ───────────────────────────────────────────────────
@router.get("/offers")
async def offers() -> dict[str, Any]:
return {
"private_beta": build_private_beta_offer(),
"pilot_499": build_499_pilot_offer(),
"growth_os_pilot": build_growth_os_pilot_offer(),
"case_study_free": build_case_study_free_offer(),
}
@router.get("/offer")
async def revenue_launch_offer(lang: str = Query("ar", description="ar or en — en adds title_en/summary_en alongside Arabic fields")) -> dict[str, Any]:
return build_revenue_offers_payload(lang)
@router.post("/offers/recommend")
async def offers_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return recommend_offer_for_segment(payload.get("segment_id", ""))
# ── Outreach ─────────────────────────────────────────────────
@router.get("/outreach/first-20")
async def outreach_first_20() -> dict[str, Any]:
seg = build_first_20_segments_v2()
return {
**seg,
"messages": {
s["id"]: build_outreach_message_v2(s["id"])
for s in seg["segments"]
},
"reply_handlers": build_reply_handlers_v2(),
}
async def revenue_launch_outreach_first_20() -> dict[str, Any]:
segs = build_first_20_segments()
samples = [
build_outreach_message("agency_b2b"),
build_outreach_message("training"),
]
return {**segs, "sample_messages": samples, "demo": True}
@router.post("/outreach/followup")
async def outreach_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
step = int(payload.get("step", 1))
builder = build_followup_2 if step >= 2 else build_followup_1
return builder(
segment_id=payload.get("segment_id", ""),
name=payload.get("name", "[الاسم]"),
)
# ── Demo ─────────────────────────────────────────────────────
@router.get("/demo-flow")
async def demo_flow() -> dict[str, Any]:
async def revenue_launch_demo_flow() -> dict[str, Any]:
return {
"flow": demo_12_min(),
"discovery_questions": demo_discovery(),
"objections": demo_objections(),
"close": demo_close_script(),
"flow": build_12_min_demo_flow(),
"discovery": build_discovery_questions(),
"close": build_close_script(),
"objections": build_objection_responses(),
"demo": True,
}
# ── Pipeline ─────────────────────────────────────────────────
@router.get("/pipeline/schema")
async def pipeline_schema() -> dict[str, Any]:
async def revenue_launch_pipeline_schema() -> dict[str, Any]:
return build_pipeline_schema()
@router.post("/pipeline/summarize")
async def pipeline_summarize(
pipeline: list[dict[str, Any]] = Body(default_factory=list, embed=True),
) -> dict[str, Any]:
return summarize_pipeline(pipeline)
@router.get("/pilot-delivery")
async def revenue_launch_pilot_delivery() -> dict[str, Any]:
return {
"intake": build_client_intake_form(),
"plan_24h": build_24h_delivery_plan(),
"first_10": build_first_10_opportunities_delivery(),
"list_intelligence": build_list_intelligence_delivery(),
"diagnostic": build_growth_diagnostic_delivery(),
"no_live_send": True,
"demo": True,
}
# ── Pilot delivery ───────────────────────────────────────────
@router.get("/pilot-delivery/intake-form")
async def pilot_intake_form() -> dict[str, Any]:
return build_client_intake_form()
@router.get("/payment/manual-flow")
async def revenue_launch_payment_manual() -> dict[str, Any]:
return {
"instructions": build_moyasar_invoice_instructions(),
"message_template": build_payment_link_message(),
"confirmation": build_payment_confirmation_checklist(),
"no_live_charge": True,
"demo": True,
}
@router.post("/pilot-delivery/24h-plan")
async def pilot_24h_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_24h_delivery_plan(payload.get("service_id", ""))
@router.post("/pilot-delivery/first-10")
async def pilot_first_10(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_first_10_opportunities_delivery(intake)
@router.post("/pilot-delivery/list-intelligence")
async def pilot_list_intelligence(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_list_intelligence_delivery(intake)
@router.post("/pilot-delivery/free-diagnostic")
async def pilot_free_diagnostic(intake: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_growth_diagnostic_delivery(intake)
# ── Payment manual flow ──────────────────────────────────────
@router.post("/payment/invoice-instructions")
async def payment_invoice_instructions(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_moyasar_invoice_instructions(
amount_sar=int(payload.get("amount_sar", 499)),
customer_name=payload.get("customer_name", ""),
invoice_description=payload.get(
"invoice_description",
"Dealix Private Beta Pilot — 7 days",
),
)
@router.post("/payment/link-message")
async def payment_link_message(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_payment_link_message(
customer_name=payload.get("customer_name", "[الاسم]"),
invoice_url=payload.get("invoice_url", "[INVOICE_URL]"),
amount_sar=int(payload.get("amount_sar", 499)),
)
@router.get("/payment/confirmation-checklist")
async def payment_confirmation_checklist() -> dict[str, Any]:
return build_payment_confirmation_checklist()
# ── Proof Pack ───────────────────────────────────────────────
@router.post("/proof-pack/template")
async def proof_pack_template(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_private_beta_proof_pack(
company_name=payload.get("company_name", ""),
metrics=payload.get("metrics", {}),
)
@router.post("/proof-pack/client-summary")
async def proof_pack_client_summary(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_client_summary(
company_name=payload.get("company_name", ""),
opportunities_count=int(payload.get("opportunities_count", 0)),
approved_drafts=int(payload.get("approved_drafts", 0)),
meetings=int(payload.get("meetings", 0)),
pipeline_sar=float(payload.get("pipeline_sar", 0)),
risks_blocked=int(payload.get("risks_blocked", 0)),
)
@router.post("/proof-pack/next-step")
async def proof_pack_next_step(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_next_step_recommendation(pilot_metrics=payload)
@router.get("/proof-pack/template")
async def revenue_launch_proof_pack_template() -> dict[str, Any]:
return build_private_beta_proof_pack()

View File

@ -112,6 +112,17 @@ from auto_client_acquisition.revenue_graph.why_now import (
explain_why_now,
)
# Revenue Company OS (events → cards → RWU; deterministic demo)
from auto_client_acquisition.revenue_company_os.action_graph import demo_action_graph
from auto_client_acquisition.revenue_company_os.channel_health import demo_channel_health
from auto_client_acquisition.revenue_company_os.command_feed_engine import build_company_os_command_feed
from auto_client_acquisition.revenue_company_os.event_to_card import event_to_card
from auto_client_acquisition.revenue_company_os.opportunity_factory import demo_opportunities
from auto_client_acquisition.revenue_company_os.proof_ledger import demo_proof_ledger
from auto_client_acquisition.revenue_company_os.revenue_work_units import demo_work_units
from auto_client_acquisition.revenue_company_os.self_improvement_loop import weekly_growth_curator_report_ar
from auto_client_acquisition.revenue_company_os.service_factory import demo_service_snapshot
router = APIRouter(prefix="/api/v1/revenue-os", tags=["revenue-os"])
log = logging.getLogger(__name__)
@ -656,6 +667,59 @@ async def get_vertical_templates(vertical_id: str) -> dict[str, Any]:
}
# ── Revenue Company OS (additive; does not replace POST /events) ─────
@router.get("/company-os/command-feed/demo")
async def company_os_command_feed_demo() -> dict[str, Any]:
return build_company_os_command_feed(
[{"type": "email.received", "payload": {"from": "demo@example.com"}}],
)
@router.post("/company-os/events/ingest")
async def company_os_events_ingest(body: dict[str, Any] = Body(...)) -> dict[str, Any]:
et = str(body.get("type") or body.get("event_type") or "form.submitted")
payload = body.get("payload") if isinstance(body.get("payload"), dict) else {}
card = event_to_card(et, payload)
return {"ingested": True, "card": card, "demo": True}
@router.get("/company-os/work-units/demo")
async def company_os_work_units_demo() -> dict[str, Any]:
return demo_work_units()
@router.get("/company-os/channel-health/demo")
async def company_os_channel_health_demo() -> dict[str, Any]:
return demo_channel_health()
@router.get("/company-os/opportunity-factory/demo")
async def company_os_opportunity_factory_demo() -> dict[str, Any]:
return demo_opportunities()
@router.get("/company-os/action-graph/demo")
async def company_os_action_graph_demo() -> dict[str, Any]:
return demo_action_graph()
@router.get("/company-os/self-improvement/weekly-report")
async def company_os_self_improvement_weekly() -> dict[str, Any]:
return weekly_growth_curator_report_ar()
@router.get("/company-os/proof-ledger/demo")
async def company_os_proof_ledger_demo() -> dict[str, Any]:
return demo_proof_ledger()
@router.get("/company-os/services/snapshot")
async def company_os_services_snapshot() -> dict[str, Any]:
return demo_service_snapshot()
# ─────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────

View File

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

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 typing import Any
from fastapi import APIRouter, Body
from fastapi import APIRouter
from auto_client_acquisition.service_excellence import (
build_backlog,
from auto_client_acquisition.service_excellence.competitor_gap import compare_against_categories
from auto_client_acquisition.service_excellence.feature_matrix import build_feature_matrix, classify_features
from auto_client_acquisition.service_excellence.launch_package import (
build_demo_script,
build_feature_matrix,
build_landing_page_outline,
build_monthly_service_review,
build_onboarding_checklist,
build_proof_pack_template_excellence,
build_sales_script,
build_service_launch_package,
build_service_research_brief,
calculate_service_excellence_score,
calculate_service_roi_estimate,
classify_features,
compare_against_categories,
convert_feedback_to_backlog,
generate_feature_hypotheses,
prioritize_backlog_items,
recommend_missing_features,
recommend_next_experiments,
recommend_weekly_improvements,
)
from auto_client_acquisition.service_excellence.proof_metrics import (
build_proof_pack_template,
required_proof_metrics,
review_service_before_launch,
summarize_proof_ar,
)
from auto_client_acquisition.service_tower import ALL_SERVICES
from auto_client_acquisition.service_excellence.quality_review import review_all_services, review_service_before_launch
from auto_client_acquisition.service_excellence.research_lab import build_service_research_brief
from auto_client_acquisition.service_excellence.service_improvement_backlog import build_backlog, prioritize_backlog_items
from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score
from auto_client_acquisition.service_excellence.workflow_builder import (
build_approval_steps,
build_day_by_day_execution_plan,
build_workflow,
validate_workflow,
)
router = APIRouter(prefix="/api/v1/service-excellence", tags=["service-excellence"])
# ── Feature matrix ───────────────────────────────────────────
@router.get("/{service_id}/feature-matrix")
async def feature_matrix(service_id: str) -> dict[str, Any]:
return build_feature_matrix(service_id)
@router.get("/{service_id}/feature-classification")
async def feature_classification(service_id: str) -> dict[str, Any]:
return classify_features(service_id)
@router.get("/{service_id}/missing-features")
async def missing_features(service_id: str) -> dict[str, Any]:
return {"recommendations": recommend_missing_features(service_id)}
# ── Scoring ──────────────────────────────────────────────────
@router.get("/{service_id}/score")
async def score(service_id: str) -> dict[str, Any]:
return calculate_service_excellence_score(service_id)
# ── Gates / quality review ──────────────────────────────────
@router.get("/{service_id}/quality-review")
async def quality_review(service_id: str) -> dict[str, Any]:
return review_service_before_launch(service_id)
router = APIRouter(prefix="/api/v1/service-excellence", tags=["service_excellence"])
@router.get("/review/all")
async def review_all() -> dict[str, Any]:
"""Review every catalogued service."""
out = [review_service_before_launch(s.id) for s in ALL_SERVICES]
counts: dict[str, int] = {}
for r in out:
v = str(r.get("verdict", "?"))
counts[v] = counts.get(v, 0) + 1
return {"total": len(out), "by_verdict": counts, "results": out}
return review_all_services()
@router.get("/{service_id}/feature-matrix")
async def feature_matrix(service_id: str) -> dict[str, Any]:
fm = build_feature_matrix(service_id)
fm["classified"] = classify_features(service_id)
return fm
@router.get("/{service_id}/score")
async def excellence_score(service_id: str) -> dict[str, Any]:
return calculate_service_excellence_score(service_id)
@router.get("/{service_id}/workflow")
async def excellence_workflow(service_id: str) -> dict[str, Any]:
wf = build_workflow(service_id)
wf["validation"] = validate_workflow(service_id)
wf["day_plan"] = build_day_by_day_execution_plan(service_id)
wf["approval"] = build_approval_steps(service_id)
return wf
# ── Proof metrics ────────────────────────────────────────────
@router.get("/{service_id}/proof-metrics")
async def proof_metrics(service_id: str) -> dict[str, Any]:
return {
"service_id": service_id,
"metrics": required_proof_metrics(service_id),
"template": build_proof_pack_template_excellence(service_id),
"required": required_proof_metrics(service_id),
"template": build_proof_pack_template(service_id),
"summary_example_ar": summarize_proof_ar(service_id, {"pipeline_sar": 15000}),
"demo": True,
}
@router.post("/{service_id}/roi-estimate")
async def roi_estimate(
service_id: str,
metrics: dict[str, Any] = Body(default_factory=dict),
) -> dict[str, Any]:
out = calculate_service_roi_estimate(service_id, metrics)
if "error" not in out:
out["proof_summary_ar"] = summarize_proof_ar(service_id, metrics)
return out
# ── Competitor gap ───────────────────────────────────────────
@router.get("/{service_id}/gap-analysis")
async def gap_analysis(service_id: str) -> dict[str, Any]:
return compare_against_categories(service_id)
# ── Research lab ─────────────────────────────────────────────
@router.get("/{service_id}/launch-package")
async def launch_package(service_id: str) -> dict[str, Any]:
return {
"package": build_service_launch_package(service_id),
"landing": build_landing_page_outline(service_id),
"sales_script": build_sales_script(service_id),
"demo_script": build_demo_script(service_id),
"onboarding": build_onboarding_checklist(service_id),
"demo": True,
}
@router.get("/{service_id}/backlog")
async def backlog(service_id: str) -> dict[str, Any]:
items = build_backlog(service_id)
return {"service_id": service_id, "items": prioritize_backlog_items(items), "demo": True}
@router.get("/{service_id}/research-brief")
async def research_brief(service_id: str) -> dict[str, Any]:
return build_service_research_brief(service_id)
@router.get("/{service_id}/feature-hypotheses")
async def feature_hypotheses(service_id: str) -> dict[str, Any]:
return {"hypotheses": generate_feature_hypotheses(service_id)}
@router.get("/{service_id}/experiments")
async def experiments(service_id: str) -> dict[str, Any]:
return recommend_next_experiments(service_id)
@router.get("/{service_id}/monthly-review")
async def monthly_review(service_id: str) -> dict[str, Any]:
return build_monthly_service_review(service_id)
# ── Backlog ──────────────────────────────────────────────────
@router.get("/{service_id}/backlog")
async def backlog(service_id: str) -> dict[str, Any]:
return build_backlog(service_id)
@router.post("/{service_id}/backlog/from-feedback")
async def backlog_from_feedback(
service_id: str,
feedback: list[dict[str, Any]] = Body(..., embed=True),
) -> dict[str, Any]:
return {
"service_id": service_id,
"items": convert_feedback_to_backlog(feedback),
}
@router.post("/{service_id}/backlog/prioritize")
async def backlog_prioritize(
service_id: str,
items: list[dict[str, Any]] = Body(..., embed=True),
) -> dict[str, Any]:
return {"items": prioritize_backlog_items(items)}
@router.get("/{service_id}/weekly-improvements")
async def weekly_improvements(service_id: str) -> dict[str, Any]:
return recommend_weekly_improvements(service_id)
# ── Launch package ───────────────────────────────────────────
@router.get("/{service_id}/launch-package")
async def launch_package(service_id: str) -> dict[str, Any]:
return build_service_launch_package(service_id)
@router.get("/{service_id}/landing-outline")
async def landing_outline(service_id: str) -> dict[str, Any]:
return build_landing_page_outline(service_id)
@router.get("/{service_id}/sales-script")
async def sales_script(service_id: str) -> dict[str, Any]:
return build_sales_script(service_id)
@router.get("/{service_id}/demo-script")
async def demo_script(service_id: str) -> dict[str, Any]:
return build_demo_script(service_id)
@router.get("/{service_id}/onboarding-checklist")
async def onboarding_checklist(service_id: str) -> dict[str, Any]:
return build_onboarding_checklist(service_id)
@router.get("/{service_id}/review")
async def review_one(service_id: str) -> dict[str, Any]:
return review_service_before_launch(service_id)

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
@ -6,85 +6,122 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.service_tower import (
build_ceo_daily_service_brief,
from auto_client_acquisition.platform_services.service_catalog import get_service_catalog
from auto_client_acquisition.service_tower.deliverables import (
build_client_report_outline,
build_deliverables,
build_end_of_day_service_report,
build_intake_questions,
build_internal_operator_checklist,
build_proof_pack_template,
build_risk_alert_card,
build_service_approval_card,
build_service_scorecard,
build_service_workflow,
build_upsell_message_ar,
)
from auto_client_acquisition.service_tower.mission_templates import build_service_workflow
from auto_client_acquisition.service_tower.pricing_engine import (
calculate_monthly_offer,
calculate_setup_fee,
catalog_summary,
get_service,
list_all_services,
map_service_to_growth_mission,
map_service_to_subscription,
quote_service,
recommend_next_step,
recommend_plan_after_service,
)
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id, list_tower_services
from auto_client_acquisition.service_tower.service_scorecard import build_service_scorecard
from auto_client_acquisition.service_tower.service_wizard import (
build_intake_questions,
recommend_service,
recommend_upgrade,
start_service,
summarize_recommendation_ar,
summarize_scorecard_ar,
validate_service_inputs,
)
from auto_client_acquisition.service_tower.contract_templates import list_contract_templates
from auto_client_acquisition.service_tower.upgrade_paths import build_all_upgrade_paths, recommend_upgrade
from auto_client_acquisition.service_tower.vertical_service_map import build_vertical_service_map
from auto_client_acquisition.service_tower.whatsapp_ceo_control import (
build_ceo_daily_service_brief,
build_end_of_day_service_report,
build_service_approval_card,
)
router = APIRouter(prefix="/api/v1/services", tags=["service-tower"])
router = APIRouter(prefix="/api/v1/services", tags=["service_tower"])
# ── Catalog ──────────────────────────────────────────────────
@router.get("/catalog")
async def catalog() -> dict[str, Any]:
return list_all_services()
@router.get("/summary")
async def summary() -> dict[str, Any]:
return catalog_summary()
async def services_catalog() -> dict[str, Any]:
tower = list_tower_services()
platform = get_service_catalog()
return {
"tower": tower,
"platform_service_catalog": platform,
"note_ar": "برج الخدمات (تفصيل بيع) + كتالوج المنصة (طبقة تقنية) — يُدمجان للعرض.",
"demo": True,
}
@router.post("/recommend")
async def recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
async def services_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
p = payload or {}
rec = recommend_service(
company_type=payload.get("company_type", ""),
goal=payload.get("goal", "fill_pipeline"),
has_contact_list=bool(payload.get("has_contact_list", False)),
channels=payload.get("channels", []),
budget_sar=int(payload.get("budget_sar", 1000)),
company_type=str(p.get("company_type") or ""),
goal=str(p.get("goal") or ""),
has_contact_list=bool(p.get("has_contact_list")),
channels=list(p.get("channels") or []),
budget_sar=p.get("budget_sar"),
)
rec["summary_ar"] = summarize_recommendation_ar(rec)
return rec
# ── Per-service ──────────────────────────────────────────────
@router.get("/{service_id}/intake-questions")
async def service_intake_questions(service_id: str) -> dict[str, Any]:
return build_intake_questions(service_id)
@router.post("/start")
async def services_start(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
p = payload or {}
return start_service(str(p.get("service_id") or ""), dict(p.get("payload") or p))
@router.post("/{service_id}/start")
async def service_start(
service_id: str,
payload: dict[str, Any] = Body(...),
) -> dict[str, Any]:
validation = validate_service_inputs(service_id, payload)
if not validation["valid"]:
return {"started": False, "validation": validation}
workflow = build_service_workflow(service_id)
return {
"started": True,
"validation": validation,
"workflow": workflow,
"linked_growth_mission": map_service_to_growth_mission(service_id),
"approval_required": True,
}
@router.get("/demo/dashboard")
async def services_demo_dashboard() -> dict[str, Any]:
ids = [s["service_id"] for s in list_tower_services().get("services") or []][:5]
cards = []
for sid in ids:
svc = get_service_by_id(sid)
cards.append(
{
"service_id": sid,
"name_ar": (svc or {}).get("name_ar"),
"deliverables": build_deliverables(sid),
"scorecard": build_service_scorecard(
sid,
{"drafts_created": 2, "approvals": 1, "meetings_booked": 0, "risks_blocked": 3},
),
}
)
return {"cards": cards, "live_send": False, "demo": True}
@router.get("/ceo/daily-brief")
async def ceo_daily_brief() -> dict[str, Any]:
return build_ceo_daily_service_brief()
@router.get("/ceo/end-of-day")
async def ceo_end_of_day() -> dict[str, Any]:
return build_end_of_day_service_report()
@router.post("/approval-card")
async def approval_card(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
p = payload or {}
return build_service_approval_card(str(p.get("service_id") or "growth_os"), str(p.get("action") or "draft_review"))
@router.get("/verticals")
async def services_verticals() -> dict[str, Any]:
return build_vertical_service_map()
@router.get("/upgrade-paths")
async def services_upgrade_paths() -> dict[str, Any]:
return build_all_upgrade_paths()
@router.get("/contracts/templates")
async def services_contract_templates() -> dict[str, Any]:
return list_contract_templates()
@router.get("/{service_id}/workflow")
@ -92,86 +129,45 @@ async def service_workflow(service_id: str) -> dict[str, Any]:
return build_service_workflow(service_id)
@router.get("/{service_id}/deliverables")
async def service_deliverables(service_id: str) -> dict[str, Any]:
return build_deliverables(service_id)
@router.get("/{service_id}/proof-pack-template")
async def service_proof_pack_template(service_id: str) -> dict[str, Any]:
return build_proof_pack_template(service_id)
@router.get("/{service_id}/client-report-outline")
async def service_client_report_outline(service_id: str) -> dict[str, Any]:
return build_client_report_outline(service_id)
@router.get("/{service_id}/operator-checklist")
async def service_operator_checklist(service_id: str) -> dict[str, Any]:
return build_internal_operator_checklist(service_id)
@router.post("/{service_id}/quote")
async def service_quote(
service_id: str,
payload: dict[str, Any] = Body(default_factory=dict),
) -> dict[str, Any]:
return quote_service(
p = payload or {}
q = quote_service(
service_id,
company_size=payload.get("company_size", "small"),
urgency=payload.get("urgency", "normal"),
channels_count=int(payload.get("channels_count", 1)),
company_size=str(p.get("company_size") or "smb"),
urgency=str(p.get("urgency") or "normal"),
channels_count=int(p.get("channels_count") or 1),
)
q["setup_fee_hint"] = calculate_setup_fee(service_id)
q["monthly_hint"] = calculate_monthly_offer(service_id)
q["upgrade_hint"] = recommend_plan_after_service(service_id, str(p.get("outcome") or ""))
return q
@router.get("/{service_id}/setup-fee")
async def service_setup_fee(service_id: str) -> dict[str, Any]:
return calculate_setup_fee(service_id)
@router.get("/{service_id}/intake-questions")
async def intake_questions(service_id: str) -> dict[str, Any]:
return build_intake_questions(service_id)
@router.get("/{service_id}/monthly-offer")
async def service_monthly_offer(service_id: str) -> dict[str, Any]:
return calculate_monthly_offer(service_id)
@router.post("/{service_id}/validate")
async def validate_inputs(service_id: str, payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return validate_service_inputs(service_id, payload or {})
@router.post("/{service_id}/scorecard")
async def service_scorecard(
service_id: str,
metrics: dict[str, Any] = Body(default_factory=dict),
) -> dict[str, Any]:
return build_service_scorecard(service_id, metrics)
@router.get("/{service_id}/deliverables")
async def service_deliverables(service_id: str) -> dict[str, Any]:
return {
"deliverables": build_deliverables(service_id),
"proof_pack": build_proof_pack_template(service_id),
"client_report": build_client_report_outline(service_id),
"operator_checklist": build_internal_operator_checklist(service_id),
"demo": True,
}
@router.get("/{service_id}/upgrade-path")
async def service_upgrade_path(service_id: str) -> dict[str, Any]:
return recommend_upgrade(service_id)
@router.get("/{service_id}/post-service-plan")
async def service_post_plan(service_id: str) -> dict[str, Any]:
return recommend_plan_after_service(service_id)
# ── CEO control via WhatsApp ─────────────────────────────────
@router.get("/ceo/daily-brief")
async def ceo_daily_brief() -> dict[str, Any]:
return build_ceo_daily_service_brief()
@router.post("/ceo/approval-card")
async def ceo_approval_card(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_service_approval_card(
service_id=payload.get("service_id", ""),
action=payload.get("action", ""),
)
@router.get("/ceo/risk-alert/demo")
async def ceo_risk_alert_demo() -> dict[str, Any]:
return build_risk_alert_card()
@router.get("/ceo/end-of-day/demo")
async def ceo_end_of_day_demo() -> dict[str, Any]:
return build_end_of_day_service_report()
@router.get("/{service_id}/upgrade")
async def service_upgrade(service_id: str) -> dict[str, Any]:
return recommend_upgrade(service_id, {})

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
@ -6,235 +6,128 @@ from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.targeting_os import (
analyze_uploaded_list_preview,
build_dealix_self_growth_plan,
build_daily_targeting_brief,
build_end_of_day_report,
build_followup_sequence,
from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
from auto_client_acquisition.platform_services.contact_import_preview import build_import_preview
from auto_client_acquisition.targeting_os.account_finder import recommend_accounts, recommend_account_source_strategy
from auto_client_acquisition.targeting_os.acquisition_scorecard import build_acquisition_scorecard
from auto_client_acquisition.targeting_os.buyer_role_mapper import map_buying_committee
from auto_client_acquisition.targeting_os.contactability_matrix import evaluate_contactability
from auto_client_acquisition.targeting_os.contract_drafts import list_contract_templates
from auto_client_acquisition.targeting_os.daily_autopilot import build_daily_targeting_brief
from auto_client_acquisition.targeting_os.free_diagnostic import (
build_free_growth_diagnostic,
recommend_paid_pilot_offer,
)
from auto_client_acquisition.targeting_os.linkedin_strategy import (
build_lead_gen_form_plan,
build_outreach_plan,
build_self_growth_daily_brief,
build_weekly_learning_report,
calculate_channel_reputation,
draft_b2b_email,
draft_role_based_angle,
draft_whatsapp_message,
enforce_daily_limits,
evaluate_contactability,
explain_contactability_ar,
list_targeting_services,
map_buying_committee,
recommend_accounts,
recommend_dealix_targets,
recommend_linkedin_strategy,
recommend_recovery_action,
recommend_service_offer,
recommend_today_actions,
score_email_risk,
score_whatsapp_risk,
summarize_plan_ar,
summarize_reputation_ar,
)
from auto_client_acquisition.targeting_os.contract_drafts import (
draft_agency_partner_outline,
draft_dpa_outline,
draft_pilot_agreement_outline,
draft_referral_agreement_outline,
)
from auto_client_acquisition.targeting_os.outreach_scheduler import build_outreach_plan
from auto_client_acquisition.targeting_os.reputation_guard import calculate_channel_reputation, should_pause_channel
from auto_client_acquisition.targeting_os.self_growth_mode import build_self_growth_daily_brief
from auto_client_acquisition.targeting_os.service_offers import list_targeting_services
router = APIRouter(prefix="/api/v1/targeting", tags=["targeting-os"])
router = APIRouter(prefix="/api/v1/targeting", tags=["targeting_os"])
# ── Accounts ─────────────────────────────────────────────────
@router.post("/accounts/recommend")
async def accounts_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
async def accounts_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return recommend_accounts(
sector=payload.get("sector", "saas"),
city=payload.get("city", "Riyadh"),
offer=payload.get("offer", ""),
goal=payload.get("goal", "fill_pipeline"),
limit=int(payload.get("limit", 10)),
str(payload.get("sector") or ""),
str(payload.get("city") or ""),
str(payload.get("offer") or ""),
str(payload.get("goal") or ""),
limit=int(payload.get("limit") or 10),
)
# ── Buying committee ─────────────────────────────────────────
@router.post("/buying-committee/map")
async def buying_committee_map(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
async def buying_committee_map(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return map_buying_committee(
sector=payload.get("sector", "saas"),
company_size=payload.get("company_size", "small"),
goal=payload.get("goal", "fill_pipeline"),
str(payload.get("sector") or ""),
payload.get("company_size"),
payload.get("goal"),
)
# ── Contacts ─────────────────────────────────────────────────
@router.post("/contacts/evaluate")
async def contacts_evaluate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
contact = payload.get("contact") or payload
async def contacts_evaluate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
contact = payload.get("contact") if isinstance(payload.get("contact"), dict) else payload
desired = payload.get("desired_channel")
result = evaluate_contactability(contact, desired_channel=desired)
result["explanation_ar"] = explain_contactability_ar(result)
return result
return evaluate_contactability(contact, str(desired) if desired else None)
@router.post("/uploaded-list/analyze")
async def uploaded_list_analyze(
contacts: list[dict[str, Any]] = Body(..., embed=True),
) -> dict[str, Any]:
return analyze_uploaded_list_preview(contacts)
async def uploaded_list_analyze(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
"""Delegates to platform import preview for full bucket logic."""
return build_import_preview(payload or {})
# ── Outreach ─────────────────────────────────────────────────
@router.post("/outreach/plan")
async def outreach_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
plan = build_outreach_plan(
targets=payload.get("targets", []),
channels=payload.get("channels"),
goal=payload.get("goal", "fill_pipeline"),
)
plan = enforce_daily_limits(plan)
plan["summary_ar"] = summarize_plan_ar(plan)
return plan
async def outreach_plan(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
targets = payload.get("targets") if isinstance(payload.get("targets"), list) else []
channels = payload.get("channels") if isinstance(payload.get("channels"), list) else ["email"]
goal = str(payload.get("goal") or "growth")
return build_outreach_plan([dict(t) for t in targets if isinstance(t, dict)], [str(c) for c in channels], goal)
# ── Daily autopilot ──────────────────────────────────────────
@router.get("/daily-autopilot/demo")
async def daily_autopilot_demo() -> dict[str, Any]:
return {
"brief": build_daily_targeting_brief(),
"today_actions": recommend_today_actions(),
"end_of_day_template": build_end_of_day_report(),
}
return build_daily_targeting_brief({"sector": "training", "city": "الرياض", "offer": "Growth OS", "goal": "meetings"})
# ── Self-Growth Mode ─────────────────────────────────────────
@router.get("/self-growth/demo")
async def self_growth_demo() -> dict[str, Any]:
return {
"plan": build_dealix_self_growth_plan(),
"today": build_self_growth_daily_brief(),
}
return build_self_growth_daily_brief()
@router.post("/self-growth/targets")
async def self_growth_targets(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return recommend_dealix_targets(
sector_focus=payload.get("sector"),
city_focus=payload.get("city"),
limit=int(payload.get("limit", 10)),
)
@router.post("/self-growth/weekly-report")
async def self_growth_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_weekly_learning_report(payload)
# ── Reputation guard ────────────────────────────────────────
@router.get("/reputation/status")
async def reputation_status() -> dict[str, Any]:
"""Demo reputation snapshot."""
healthy_email = {"bounce_rate": 0.005, "complaint_rate": 0.0001,
"opt_out_rate": 0.01, "reply_rate": 0.04}
risky_wa = {"block_rate": 0.04, "report_rate": 0.005,
"opt_out_rate": 0.06, "reply_rate": 0.02}
return {
"email": calculate_channel_reputation(healthy_email, channel="email"),
"whatsapp": calculate_channel_reputation(risky_wa, channel="whatsapp"),
}
metrics = {"bounce_rate": 0.12, "opt_out_rate": 0.01, "complaint_rate": 0.0, "reply_rate": 0.08}
rep = calculate_channel_reputation(metrics)
return {**rep, "should_pause": should_pause_channel(metrics)}
@router.post("/reputation/recovery")
async def reputation_recovery(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return recommend_recovery_action(
payload.get("metrics", {}),
channel=payload.get("channel", "email"),
)
# ── LinkedIn strategy ────────────────────────────────────────
@router.post("/linkedin/strategy")
async def linkedin_strategy(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
strategy = recommend_linkedin_strategy(
segment=payload.get("segment", "B2B Saudi"),
goal=payload.get("goal", "fill_pipeline"),
)
if payload.get("with_lead_gen_form"):
strategy["lead_gen_form_plan"] = build_lead_gen_form_plan(
segment=payload.get("segment", "B2B Saudi"),
offer=payload.get("offer", "Pilot 7 days"),
campaign_name=payload.get("campaign_name", ""),
async def linkedin_strategy(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
seg = str(payload.get("segment") or "b2b")
goal = str(payload.get("goal") or "leads")
base = recommend_linkedin_strategy(seg, goal)
if payload.get("include_lead_gen_plan"):
base["lead_gen_plan"] = build_lead_gen_form_plan(
seg,
str(payload.get("offer") or "Pilot"),
str(payload.get("campaign_name") or "dealix"),
)
return strategy
return base
# ── Drafts ───────────────────────────────────────────────────
@router.post("/drafts/email")
async def drafts_email(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
contact = payload.get("contact", {})
draft = draft_b2b_email(
contact,
offer=payload.get("offer", ""),
why_now=payload.get("why_now", ""),
)
risk = score_email_risk(contact, draft.get("body_ar", ""))
return {**draft, "risk": risk}
@router.post("/drafts/whatsapp")
async def drafts_whatsapp(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
contact = payload.get("contact", {})
return draft_whatsapp_message(
contact,
offer=payload.get("offer", ""),
why_now=payload.get("why_now", ""),
)
@router.post("/drafts/email-followup")
async def drafts_email_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_followup_sequence(
payload.get("contact", {}),
offer=payload.get("offer", ""),
)
@router.post("/drafts/role-angle")
async def drafts_role_angle(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return draft_role_based_angle(
role_key=payload.get("role_key", "founder_ceo"),
sector=payload.get("sector", "saas"),
offer=payload.get("offer", ""),
)
# ── Free diagnostic ──────────────────────────────────────────
@router.post("/free-diagnostic")
async def free_diagnostic(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_free_growth_diagnostic(payload)
# ── Services + contracts ─────────────────────────────────────
@router.get("/services")
async def services_list() -> dict[str, Any]:
async def targeting_services() -> dict[str, Any]:
return list_targeting_services()
@router.post("/services/recommend")
async def services_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return recommend_service_offer(
customer_type=payload.get("customer_type", ""),
goal=payload.get("goal", "fill_pipeline"),
)
@router.post("/free-diagnostic")
async def free_diagnostic(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
company = payload.get("company") if isinstance(payload.get("company"), dict) else payload
if not isinstance(company, dict):
company = {}
diag = build_free_growth_diagnostic(company or {"sector": "b2b", "city": "الرياض"})
return {"diagnostic": diag, "pilot_offer": recommend_paid_pilot_offer(diag)}
@router.get("/contracts/templates")
async def contracts_templates() -> dict[str, Any]:
return {
"pilot": draft_pilot_agreement_outline(),
"dpa": draft_dpa_outline(),
"referral": draft_referral_agreement_outline(),
"agency_partner": draft_agency_partner_outline(),
}
return list_contract_templates()
@router.post("/trust-score")
async def targeting_trust_score(signals: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
"""Bridge to intelligence trust score for targeting workflows."""
return compute_trust_score(signals or {})
@router.post("/account-strategy")
async def account_strategy(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
acct = payload.get("account") if isinstance(payload.get("account"), dict) else {}
return recommend_account_source_strategy(acct)

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
from .eval_cases import EVAL_CASES, run_eval_pack
from .safety_eval import safety_eval
from .saudi_tone_eval import saudi_tone_eval
from .trace_events import build_trace_event
__all__ = [
"CostTracker",
"EVAL_CASES",
"build_trace_event",
"run_eval_pack",
"safety_eval",
"saudi_tone_eval",
]
__all__ = ["build_trace_event", "evaluate_safety", "evaluate_saudi_tone"]

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 typing import Any
from .safety_eval import safety_eval
from .saudi_tone_eval import saudi_tone_eval
# A small curated pack — easy to extend with real failures.
EVAL_CASES: tuple[dict[str, Any], ...] = (
{
"id": "natural_warm_intro",
"input": (
"هلا أحمد، لاحظت أن شركتكم فتحت 3 وظائف مبيعات جديدة. "
"نشتغل على Dealix كمدير نمو عربي يطلع 10 فرص B2B. "
"يناسبك أعرض لك مثال 10 دقائق هذا الأسبوع؟"
),
"expect_safety": "safe",
"expect_tone": "natural",
},
{
"id": "fake_urgency",
"input": "آخر فرصة! العرض ينتهي اليوم! اضغط الآن لتحصل على ضمان 100%.",
"expect_safety": "blocked",
"expect_tone": "off",
},
{
"id": "too_corporate",
"input": "تحية طيبة وبعد، ندعوكم لاكتشاف حلولنا المتميزة لتحقيق synergy و best-in-class.",
"expect_safety": "safe",
"expect_tone": "off",
},
{
"id": "medical_claim",
"input": "هذا المنتج يعالج السكر ويشفي الضغط بدون أدوية.",
"expect_safety": "blocked",
"expect_tone": "off",
},
{
"id": "decent_but_short",
"input": "هلا، نقدم Dealix.",
"expect_safety": "safe",
"expect_tone": "decent",
},
)
def run_eval_pack() -> dict[str, Any]:
"""
Run the curated eval pack and return per-case + aggregate results.
A case is "passed" if BOTH expected verdicts match.
"""
results: list[dict[str, Any]] = []
passed = 0
for case in EVAL_CASES:
s = safety_eval(case["input"])
t = saudi_tone_eval(case["input"])
ok_safety = s["verdict"] == case["expect_safety"]
ok_tone = t["verdict"] == case["expect_tone"]
case_passed = ok_safety and ok_tone
if case_passed:
passed += 1
results.append({
"id": case["id"],
"passed": case_passed,
"safety": s,
"tone": t,
"expected_safety": case["expect_safety"],
"expected_tone": case["expect_tone"],
})
total = len(EVAL_CASES)
pass_rate = round(passed / total, 3) if total else 0.0
return {
"total": total,
"passed": passed,
"failed": total - passed,
"pass_rate": pass_rate,
"results": results,
}
EVAL_CASES: tuple[str, ...] = ("no_guarantee_language", "no_payment_collect", "has_single_cta")

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
import re
from typing import Any
# Each tuple: (category, regex, Arabic reason, severity_points 0..50)
SAFETY_RULES: tuple[tuple[str, str, str, int], ...] = (
("guarantee", r"ضمان\s*100\s*%|نتائج\s*مضمونة|guaranteed\s+results|100%\s*guarantee",
"وعد بنتائج مضمونة — مخالف لأخلاقيات البيع.", 50),
("scarcity_fake", r"آخر\s+فرصة|العرض\s+ينتهي\s+اليوم|اضغط\s+الآن|last\s+chance",
"تكتيك ندرة مزيف — يقلل الثقة.", 25),
("medical_claim", r"يعالج|يشفي|cures|treats\s+(?:cancer|diabetes)",
"ادعاء طبي — ممنوع تماماً.", 50),
("financial_claim", r"ROI\s*\d{3,}\s*%|\d{4,}\s*%\s*عائد",
"ادعاء عوائد مالية مبالغ فيه.", 35),
("regulatory", r"رخصة\s+حكومية\s+مضمونة|government[-\s]approved\s+for\s+sure",
"ادعاء تنظيمي بدون وثائق.", 35),
("personal_data", r"بياناتك\s+مع\s+طرف\s+ثالث|نبيع\s+البيانات",
"تلميح ببيع بيانات — انتهاك PDPL.", 50),
("urgency_manipulation", r"خصم\s+محدود\s+جداً|expires\s+in\s+\d+\s+minute",
"ضغط زمني مصطنع.", 15),
)
_BAD = ("ضمان كامل", "مضمون 100%", "ارسل لي رقم البطاقة", "كلمة المرور", "حسابك معلق")
def safety_eval(text: str) -> dict[str, object]:
"""
Evaluate a message for safety violations.
Returns:
{
"score": int 0..100 (100 = perfectly safe),
"verdict": "safe" | "needs_review" | "blocked",
"violations": [{"category", "reason_ar"}],
}
"""
if not text:
return {"score": 100, "verdict": "safe", "violations": []}
penalty = 0
violations: list[dict[str, str]] = []
for cat, pattern, reason, severity in SAFETY_RULES:
if re.search(pattern, text, flags=re.IGNORECASE):
penalty += severity
violations.append({"category": cat, "reason_ar": reason})
score = max(0, 100 - penalty)
if score >= 70:
verdict = "safe"
elif score >= 40:
verdict = "needs_review"
else:
verdict = "blocked"
return {"score": score, "verdict": verdict, "violations": violations}
def evaluate_safety(text_ar: str) -> dict[str, Any]:
t = text_ar or ""
trips: list[str] = []
for phrase in _BAD:
if phrase in t:
trips.append(phrase)
if re.search(r"\b\d{16}\b", t):
trips.append("possible_pan")
return {"passed": len(trips) == 0, "tripwires": trips, "demo": True}

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
import re
# Positive markers — natural Saudi conversational tone.
POSITIVE_MARKERS_AR: tuple[str, ...] = (
"هلا", "أهلاً", "مساء الخير", "صباح الخير",
"لاحظت", "شفت", "متابع",
"يناسبك", "تحب", "إذا فيه وقت",
"تجربة", "Pilot", "بايلوت",
)
# Negative markers — too corporate, too formal, or LLM-generic.
NEGATIVE_MARKERS_AR: tuple[str, ...] = (
"السيد المحترم", "تحية طيبة وبعد", "ندعوكم لاكتشاف",
"ابتداءً من تاريخه", "فوراً وعلى وجه السرعة",
"leverage", "synergy", "best-in-class",
"نفخر بأن نقدم لكم",
)
from typing import Any
def _arabic_ratio(text: str) -> float:
if not text:
return 0.0
arabic = sum(1 for ch in text if "؀" <= ch <= "ۿ")
total = sum(1 for ch in text if not ch.isspace())
if total == 0:
return 0.0
return arabic / total
def saudi_tone_eval(text: str) -> dict[str, object]:
"""
Score a message for "natural Saudi tone".
Returns:
{
"score": 0..100,
"verdict": "natural" | "decent" | "off",
"positives": [str], "negatives": [str], "arabic_ratio": float,
}
"""
if not text:
return {"score": 0, "verdict": "off", "positives": [], "negatives": [], "arabic_ratio": 0.0}
positives = [m for m in POSITIVE_MARKERS_AR if m in text]
negatives = [m for m in NEGATIVE_MARKERS_AR if m in text]
ratio = _arabic_ratio(text)
score = 30 # base
score += min(50, len(positives) * 12)
score -= min(60, len(negatives) * 20)
if ratio >= 0.6:
score += 20
elif ratio >= 0.3:
def evaluate_saudi_tone(text_ar: str) -> dict[str, Any]:
t = (text_ar or "").strip()
score = 65
if re.search(r"(هل|ممكن|نقدّم|نرحب|شاكرين)", t):
score += 10
if len(t) > 600:
score -= 10
if "!!!" in t or "؟؟؟" in t:
score -= 8
score = max(0, min(100, score))
# Length penalty for huge messages.
word_count = len(re.split(r"\s+", text.strip()))
if word_count > 80:
score = max(0, score - 10)
if score >= 75:
verdict = "natural"
elif score >= 50:
verdict = "decent"
else:
verdict = "off"
return {
"score": score,
"verdict": verdict,
"positives": positives,
"negatives": negatives,
"arabic_ratio": round(ratio, 3),
}
return {"tone_score": score, "demo": True}

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

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,
recommends a service, collects intake, runs workflow, requests approval,
delivers Proof Pack, suggests upgrade.
"""
from auto_client_acquisition.autonomous_service_operator.conversation_router import handle_message
from auto_client_acquisition.autonomous_service_operator.service_bundles import get_bundle, list_bundles
from __future__ import annotations
from .agency_mode import (
add_agency_client,
build_agency_dashboard,
build_co_branded_proof_pack,
list_agency_revenue_share,
)
from .approval_manager import (
APPROVAL_STATES,
build_approval_card,
process_approval_decision,
)
from .client_mode import (
build_client_dashboard,
build_client_session_summary,
)
from .self_growth_mode import (
build_operator_self_growth_brief,
)
from .service_delivery_mode import (
build_post_delivery_handoff,
build_service_delivery_brief,
build_sla_status_for_delivery,
)
from .conversation_router import (
INTENT_TO_HANDLER,
handle_message,
route_message,
)
from .executive_mode import (
build_ceo_command_center,
build_executive_daily_brief,
build_revenue_risks_summary,
)
from .intake_collector import (
build_intake_questions_for_intent,
parse_intake_payload,
validate_intake_completeness,
)
from .intent_classifier import (
SUPPORTED_INTENTS,
classify_intent,
intent_to_service,
)
from .operator_memory import (
OperatorMemory,
build_session_context,
)
from .proof_pack_dispatcher import (
dispatch_proof_pack,
proof_pack_for_service,
)
from .service_bundles import (
BUNDLES,
get_bundle,
list_bundles,
recommend_bundle,
)
from .service_orchestrator import (
SERVICE_PIPELINE_STEPS,
build_service_pipeline,
run_service_step,
)
from .session_state import (
SessionState,
build_new_session,
transition_session,
)
from .tool_action_planner import (
plan_tool_action,
review_planned_action,
)
from .upsell_engine import (
build_upsell_card,
recommend_upsell_after_service,
)
from .whatsapp_renderer import (
render_approval_card_for_whatsapp,
render_card_for_whatsapp,
render_daily_brief_for_whatsapp,
)
from .workflow_runner import (
advance_workflow,
build_workflow_state,
is_workflow_complete,
)
__all__ = [
# conversation_router
"INTENT_TO_HANDLER", "handle_message", "route_message",
# intent_classifier
"SUPPORTED_INTENTS", "classify_intent", "intent_to_service",
# service_orchestrator
"SERVICE_PIPELINE_STEPS", "build_service_pipeline", "run_service_step",
# session_state
"SessionState", "build_new_session", "transition_session",
# intake_collector
"build_intake_questions_for_intent", "parse_intake_payload",
"validate_intake_completeness",
# approval_manager
"APPROVAL_STATES", "build_approval_card", "process_approval_decision",
# workflow_runner
"advance_workflow", "build_workflow_state", "is_workflow_complete",
# tool_action_planner
"plan_tool_action", "review_planned_action",
# proof_pack_dispatcher
"dispatch_proof_pack", "proof_pack_for_service",
# upsell_engine
"build_upsell_card", "recommend_upsell_after_service",
# whatsapp_renderer
"render_approval_card_for_whatsapp", "render_card_for_whatsapp",
"render_daily_brief_for_whatsapp",
# operator_memory
"OperatorMemory", "build_session_context",
# service_bundles
"BUNDLES", "get_bundle", "list_bundles", "recommend_bundle",
# executive_mode
"build_ceo_command_center", "build_executive_daily_brief",
"build_revenue_risks_summary",
# client_mode
"build_client_dashboard", "build_client_session_summary",
# agency_mode
"add_agency_client", "build_agency_dashboard",
"build_co_branded_proof_pack", "list_agency_revenue_share",
# self_growth_mode
"build_operator_self_growth_brief",
# service_delivery_mode
"build_post_delivery_handoff",
"build_service_delivery_brief",
"build_sla_status_for_delivery",
]
__all__ = ["handle_message", "list_bundles", "get_bundle"]

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 typing import Any
def add_agency_client(
*,
agency_id: str,
client_company_name: str,
sector: str = "",
monthly_subscription_sar: int = 0,
revenue_share_pct: int = 20,
clients: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Add a new client to an agency's roster + return the entry."""
entry: dict[str, Any] = {
"agency_id": agency_id,
"client_company_name": client_company_name,
"sector": sector,
"monthly_subscription_sar": int(monthly_subscription_sar),
"revenue_share_pct": int(revenue_share_pct),
"status": "onboarding",
"co_branded_proof_pack": True,
"approval_required": True,
}
if clients is not None:
clients.append(entry)
return entry
def build_agency_dashboard(
*,
agency_id: str,
agency_name: str = "",
clients: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build the agency's dashboard summary."""
clients = clients or []
total_clients = len(clients)
active = sum(1 for c in clients if c.get("status") in ("active", "onboarding"))
monthly_revenue_total = sum(
float(c.get("monthly_subscription_sar", 0) or 0) for c in clients
)
avg_share_pct = (
round(
sum(int(c.get("revenue_share_pct", 0) or 0) for c in clients)
/ max(1, total_clients),
1,
)
if total_clients else 0.0
)
def mode_profile() -> dict[str, Any]:
return {
"mode": "agency",
"agency_id": agency_id,
"agency_name": agency_name,
"metrics": {
"total_clients": total_clients,
"active_clients": active,
"monthly_revenue_sar": round(monthly_revenue_total, 2),
"avg_revenue_share_pct": avg_share_pct,
},
"summary_ar": [
f"عملاء الوكالة: {total_clients} (نشط: {active}).",
f"الإيراد الشهري الكلي: {monthly_revenue_total:.0f} ريال.",
f"متوسط revenue share: {avg_share_pct}%.",
],
"panels_ar": [
"Add Client — إضافة عميل جديد",
"Run Diagnostic — تشخيص لعميل",
"Co-Branded Proof Pack — Proof بعلامة الوكالة",
"Referral Tracking — متابعة الإحالات",
"Partner Scorecard — تقييم الأداء",
],
"approval_required": True,
"live_send_allowed": False,
}
def list_agency_revenue_share(
*, clients: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Compute revenue share owed to an agency for the current month."""
clients = clients or []
line_items: list[dict[str, Any]] = []
total_share_sar = 0.0
for c in clients:
sub = float(c.get("monthly_subscription_sar", 0) or 0)
pct = int(c.get("revenue_share_pct", 0) or 0)
share = round(sub * pct / 100.0, 2)
total_share_sar += share
line_items.append({
"client_company_name": c.get("client_company_name"),
"monthly_subscription_sar": sub,
"revenue_share_pct": pct,
"agency_share_sar": share,
})
return {
"line_items": line_items,
"total_share_sar": round(total_share_sar, 2),
"currency": "SAR",
}
def build_co_branded_proof_pack(
*,
agency_name: str,
client_company_name: str,
metrics: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a co-branded Proof Pack envelope for an agency client."""
metrics = metrics or {}
return {
"title_ar": (
f"Proof Pack — {client_company_name} (تنفيذ: {agency_name})"
),
"co_branded": True,
"agency_name": agency_name,
"client_company_name": client_company_name,
"sections_ar": [
"ملخص تنفيذي للعميل",
"ما عملته الوكالة + Dealix",
"النتائج بالأرقام",
"Action Ledger",
"المخاطر التي منعتها الوكالة",
"التوصية بالخطوة التالية",
],
"metrics": dict(metrics),
"approval_required": True,
"live_send_allowed": False,
"mode": "agency_partner",
"priority_intents": ["want_partnerships", "ask_services", "ask_proof"],
"card_types_first": ["opportunity", "proof_update", "compliance_risk"],
"demo": True,
}

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 typing import Any
APPROVAL_STATES: tuple[str, ...] = (
"pending",
"approved",
"edited",
"rejected",
"expired",
)
from auto_client_acquisition.autonomous_service_operator import session_state as ss
def build_approval_card(
*,
action_type: str,
title_ar: str,
summary_ar: str,
risk_level: str = "low",
why_now_ar: str = "",
recommended_action_ar: str = "",
expected_impact_sar: float = 0.0,
service_id: str | None = None,
customer_id: str | None = None,
action_id: str | None = None,
) -> dict[str, Any]:
"""Build a structured Arabic approval card."""
return {
"type": "approval",
"action_id": action_id,
"action_type": action_type,
"service_id": service_id,
"customer_id": customer_id,
"title_ar": title_ar[:140],
"summary_ar": summary_ar[:280],
"why_now_ar": why_now_ar[:200],
"recommended_action_ar": recommended_action_ar[:200],
"risk_level": risk_level if risk_level in (
"low", "medium", "high",
) else "medium",
"expected_impact_sar": float(expected_impact_sar),
"buttons_ar": ["اعتمد", "عدّل", "تخطي"],
"state": "pending",
"approval_required": True,
"live_send_allowed": False,
}
def set_pending_approval(session_id: str, payload: dict[str, Any]) -> dict[str, Any]:
return ss.upsert_session(
session_id,
{"workflow_state": "pending_approval", "pending_card": payload},
)
def process_approval_decision(
card: dict[str, Any],
*,
decision: str,
decided_by: str = "user",
note: str = "",
) -> dict[str, Any]:
"""
Process an approval decision (`approve` / `edit` / `skip` / `reject`).
def apply_decision(session_id: str, decision: str) -> dict[str, Any]:
d = (decision or "").strip().lower()
if d in ("approve", "اعتمد"):
return ss.upsert_session(
session_id,
{"workflow_state": "approved", "pending_card": None, "last_decision": "approve"},
)
if d in ("edit", "تعديل"):
return ss.upsert_session(
session_id,
{"workflow_state": "edit_requested", "last_decision": "edit"},
)
if d in ("skip", "تخطي"):
return ss.upsert_session(
session_id,
{"workflow_state": "skipped", "pending_card": None, "last_decision": "skip"},
)
return ss.upsert_session(session_id, {"workflow_state": "unknown_decision", "last_decision": d})
Returns the updated card with new state + audit info.
"""
decision_lc = (decision or "").strip().lower()
if decision_lc in ("approve", "approved", "موافق", "اعتمد", "نعم"):
new_state = "approved"
next_action = "execute_with_audit"
elif decision_lc in ("edit", "عدّل", "تعديل"):
new_state = "edited"
next_action = "rewrite_then_resend_for_approval"
elif decision_lc in ("skip", "تخطي", "تجاوز"):
new_state = "rejected"
next_action = "archive"
elif decision_lc in ("reject", "ارفض", "لا"):
new_state = "rejected"
next_action = "archive_with_reason"
else:
return {
"error": f"unknown decision: {decision}",
"valid_decisions": ["approve", "edit", "skip", "reject"],
}
out = dict(card)
out["state"] = new_state
out["decided_by"] = decided_by
out["decision_note"] = note[:200]
out["next_action"] = next_action
return out
def pending_card(session_id: str) -> dict[str, Any] | None:
s = ss.get_session(session_id)
if not s:
return None
return s.get("pending_card") if isinstance(s.get("pending_card"), dict) else None

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 typing import Any
def build_client_dashboard(
*,
customer_id: str = "",
company_name: str = "",
active_services: list[str] | None = None,
open_actions: int = 0,
proof_pack_due: bool = False,
) -> dict[str, Any]:
"""Build the client-facing dashboard."""
active_services = active_services or []
def mode_profile() -> dict[str, Any]:
return {
"mode": "client",
"customer_id": customer_id,
"company_name": company_name,
"active_services": list(active_services),
"open_actions": open_actions,
"proof_pack_due": proof_pack_due,
"today_panels_ar": [
"Command Feed — قرارات اليوم",
"Approvals Center — رسائل تنتظر اعتمادك",
"Pipeline Tracker — مرحلة كل عميل",
"Proof Pack — آخر تقرير + الـ ROI",
],
"buttons_ar": ["اعرض القرارات", "اعتمد جماعي", "افتح Proof Pack"],
"approval_required": True,
"live_send_allowed": False,
}
def build_client_session_summary(
*,
session_id: str,
customer_id: str = "",
last_intent: str = "",
last_recommended_service: str = "",
) -> dict[str, Any]:
"""Build a session summary for the client view."""
return {
"mode": "client",
"session_id": session_id,
"customer_id": customer_id,
"last_intent": last_intent,
"last_recommended_service": last_recommended_service,
"next_step_ar": (
"أكمل الـ intake للحصول على workflow الخدمة + أول Proof Pack."
),
"approval_required": True,
"priority_intents": ["want_more_customers", "has_contact_list", "want_meetings"],
"card_types_first": ["approval_needed", "opportunity", "proof_update"],
"demo": True,
}

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 typing import Any
from .approval_manager import (
build_approval_card,
process_approval_decision,
from auto_client_acquisition.autonomous_service_operator import (
approval_manager as am,
intent_classifier as ic,
operator_memory as om,
service_bundles as sb,
service_orchestrator as so,
session_state as ss,
workflow_runner as wr,
)
from .intake_collector import build_intake_questions_for_intent
from .intent_classifier import classify_intent, intent_to_service
from .service_bundles import recommend_bundle
from .service_orchestrator import build_service_pipeline
# Map: intent → handler name
INTENT_TO_HANDLER: dict[str, str] = {
"want_more_customers": "start_first_10_opportunities",
"has_contact_list": "start_list_intelligence",
"want_partnerships": "start_partner_sprint",
"want_daily_growth": "start_growth_os",
"want_meetings": "start_meeting_sprint",
"want_email_rescue": "start_email_rescue",
"want_whatsapp_setup": "start_whatsapp_compliance",
"ask_pricing": "show_pricing",
"approve_action": "process_approval",
"edit_action": "process_edit",
"skip_action": "process_skip",
"ask_demo": "send_demo",
"ask_proof": "send_proof_pack",
"ask_services": "show_bundles",
"ask_partnership": "show_agency_partner",
"ask_revenue_today": "show_revenue_today_plan",
}
def handle_message(session_id: str, message: str, mode: str = "client") -> dict[str, Any]:
intent = ic.classify_intent(message)
om.append_turn(session_id, "user", message, {"intent": intent})
if intent == ic.INTENT_COLD_WHATSAPP_REQUEST:
body = so.cold_whatsapp_response()
om.append_turn(session_id, "assistant", body["message_ar"], {"blocked": True})
ss.upsert_session(session_id, {"last_intent": intent, "blocked": True})
return {"session_id": session_id, "intent": intent, **body}
def route_message(message: str) -> dict[str, Any]:
"""Classify a message + return the routed handler + recommended service."""
classification = classify_intent(message)
intent = classification["intent"]
handler = INTENT_TO_HANDLER.get(intent, "show_bundles")
service_id = intent_to_service(intent)
return {
"message": (message or "")[:300],
"classification": classification,
"intent": intent,
"handler": handler,
"recommended_service_id": service_id,
}
def handle_message(
message: str,
*,
customer_id: str | None = None,
has_contact_list: bool = False,
is_agency: bool = False,
is_local_business: bool = False,
budget_sar: int = 1000,
) -> dict[str, Any]:
"""
Full single-shot handler classifies + plans + returns operator response.
Never executes any external action. Just plans + drafts.
"""
routed = route_message(message)
intent = routed["intent"]
handler = routed["handler"]
# Recommend a bundle (high-level package).
bundle_rec = recommend_bundle(
intent=intent,
has_contact_list=has_contact_list,
is_agency=is_agency,
is_local_business=is_local_business,
budget_sar=budget_sar,
rec = so.recommend_for_intent(intent)
ss.upsert_session(
session_id,
{
"last_intent": intent,
"recommended_service_id": rec["recommended_service_id"],
"mode": mode,
},
)
wr.advance(session_id, "start_service")
# If a service is recommended, build its initial pipeline + intake form.
response: dict[str, Any] = {
reply_ar = _build_reply_ar(intent, rec)
om.append_turn(session_id, "assistant", reply_ar, {"recommendation": rec})
return {
"session_id": session_id,
"intent": intent,
"handler": handler,
"bundle_recommendation": bundle_rec,
"service_id": routed["recommended_service_id"],
"approval_required": True,
"live_send_allowed": False,
"recommendation": rec,
"reply_ar": reply_ar,
"bundles_hint": sb.list_bundles() if intent == ic.INTENT_ASK_SERVICES else None,
"demo": True,
}
if intent in ("approve_action", "edit_action", "skip_action"):
# Approvals are handled separately — surface a placeholder card.
decision = (
"approve" if intent == "approve_action"
else "edit" if intent == "edit_action"
else "skip"
)
sample_card = build_approval_card(
action_type="example_action",
title_ar="فعل مثال",
summary_ar="هذا مثال على approval card",
)
response["decision_processed"] = process_approval_decision(
sample_card, decision=decision, decided_by=customer_id or "user",
)
return response
if routed["recommended_service_id"]:
response["intake_questions"] = build_intake_questions_for_intent(intent)
response["initial_pipeline"] = build_service_pipeline(
routed["recommended_service_id"], customer_id=customer_id or "",
def _build_reply_ar(intent: str, rec: dict[str, Any]) -> str:
sid = rec.get("recommended_service_id")
name = rec.get("service_name_ar") or sid
if intent == ic.INTENT_ASK_SERVICES:
return (
"أنسب مسار: ابدأ بتشخيص مجاني ثم اختر باقة Growth Starter أو Data to Revenue. "
"راجع قائمة الباقات من /api/v1/operator/bundles."
)
return response
if intent == ic.INTENT_ASK_PROOF:
return f"Proof Pack مرتبط بخدمة {name} — جاهز كعرض تجريبي بعد أول مسودات موافَق عليها."
return f"نوصي بخدمة: {name} ({sid}). الخطوة التالية: أكمل المدخلات ثم راجع المسودات قبل أي إرسال."

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 typing import Any
def build_executive_daily_brief(
*,
company_name: str = "",
sector: str = "saas",
) -> dict[str, Any]:
"""Build the CEO's daily brief (Arabic)."""
def mode_profile() -> dict[str, Any]:
return {
"title_ar": f"موجز اليوم التنفيذي — {company_name or '(الشركة)'}",
"summary_ar": [
f"3 قرارات تنتظر اعتمادك في قطاع {sector}.",
"5 رسائل drafts معدّة بـ Saudi tone.",
"2 leads متأخرة في المتابعة (>72 ساعة).",
"1 شريك وكالة جاهز لاجتماع.",
"1 خطر سمعة على قناة (يحتاج مراجعة).",
],
"priority_decisions_ar": [
"اعتمد 5 رسائل إيميل (10 دقائق).",
"راجع 12 رقم بدون مصدر واضح قبل أي واتساب.",
"احجز ديمو شريك الوكالة.",
],
"metric_to_watch_ar": (
"نسبة approval_rate الأسبوعية — هي المؤشر الأقوى لجودة "
"الـ targeting + الـ Saudi Tone."
),
"buttons_ar": ["اعرض القرارات", "Proof Pack", "لاحقاً"],
"approval_required": True,
}
def build_revenue_risks_summary(
*,
open_risks: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a 3-risk summary (Arabic)."""
open_risks = open_risks or [
{
"id": "wa_quality",
"title_ar": "جودة واتساب",
"summary_ar": "نسبة الحظر على رقم واتساب الرئيسي تقترب من حد التحذير.",
"severity": "high",
"action_ar": "خفّض الحجم 50% + راجع الرسائل.",
},
{
"id": "list_freshness",
"title_ar": "قائمة قديمة",
"summary_ar": "60% من القائمة لم يتم تحديثها منذ 9 أشهر.",
"severity": "medium",
"action_ar": "شغّل List Intelligence لتنظيفها.",
},
{
"id": "single_threading",
"title_ar": "صفقة بشخص واحد",
"summary_ar": "صفقة كبيرة (250K) معتمدة على شخص واحد بدون buying committee.",
"severity": "high",
"action_ar": "ادعُ صانع قرار ثانٍ من نفس الشركة.",
},
]
return {
"title_ar": "أعلى 3 مخاطر إيراد اليوم",
"risks": open_risks[:3],
"approval_required": True,
}
def build_ceo_command_center(
*,
company_name: str = "",
sector: str = "saas",
) -> dict[str, Any]:
"""Build the full CEO command-center page."""
return {
"mode": "ceo",
"company_name": company_name,
"daily_brief": build_executive_daily_brief(
company_name=company_name, sector=sector,
),
"revenue_risks": build_revenue_risks_summary(),
"next_three_moves_ar": [
"اعتمد رسائل اليوم (5).",
"ابدأ Pilot 7 أيام لقطاع جديد (testing).",
"حدد منسّق Approvals بديل خلال 24 ساعة.",
],
"approval_required": True,
"live_send_allowed": False,
"mode": "executive",
"priority_intents": ["want_more_customers", "ask_proof", "approve_action"],
"card_types_first": ["leak", "approval_needed", "opportunity"],
"demo": True,
}

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 typing import Any
# Intake questions per intent (Arabic).
_INTAKE_QUESTIONS_BY_INTENT: dict[str, list[dict[str, Any]]] = {
"want_more_customers": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "city", "label_ar": "المدينة", "required": True},
{"key": "offer", "label_ar": "العرض الرئيسي", "required": True},
{"key": "ideal_customer", "label_ar": "العميل المثالي",
"required": True},
],
"has_contact_list": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "list_size", "label_ar": "حجم القائمة (تقريباً)",
"required": True},
{"key": "list_source", "label_ar": "مصدر القائمة (CRM/event/upload)",
"required": True},
{"key": "channels_available", "label_ar": "القنوات المتاحة",
"required": True},
],
"want_partnerships": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "partner_goal",
"label_ar": "هدف الشراكة (وكالات/موزعين/co-marketing)",
"required": True},
{"key": "current_partners", "label_ar": "شركاء حاليين (إن وجد)",
"required": False},
],
"want_daily_growth": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "team_size", "label_ar": "حجم فريق المبيعات/النمو",
"required": True},
{"key": "channels", "label_ar": "القنوات الحالية", "required": True},
{"key": "approval_owner", "label_ar": "من يوافق على الرسائل؟",
"required": True},
],
"want_meetings": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "prospect_count", "label_ar": "عدد الـ prospects",
"required": True},
{"key": "calendar_link", "label_ar": "رابط Calendar (لو وُجد)",
"required": False},
],
"want_email_rescue": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "gmail_label",
"label_ar": "اسم الـ label/الـ folder المستهدف",
"required": True},
{"key": "ICP", "label_ar": "العميل المثالي", "required": True},
],
"want_whatsapp_setup": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "list_size",
"label_ar": "حجم قاعدة الواتساب الحالية", "required": True},
{"key": "current_practice",
"label_ar": "الطريقة الحالية في إرسال الرسائل", "required": True},
],
"ask_revenue_today": [
{"key": "company_name", "label_ar": "اسم الشركة", "required": True},
{"key": "sector", "label_ar": "القطاع", "required": True},
{"key": "city", "label_ar": "المدينة", "required": True},
{"key": "offer", "label_ar": "العرض الرئيسي", "required": True},
],
# Default minimal intake for any "ask_*" intent.
"ask_services": [
{"key": "goal", "label_ar": "ما هدفك الأساسي؟", "required": True},
],
}
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
def build_intake_questions_for_intent(intent: str) -> dict[str, Any]:
"""Return intake questions for an intent. Falls back to ask_services."""
questions = _INTAKE_QUESTIONS_BY_INTENT.get(intent)
if questions is None:
questions = _INTAKE_QUESTIONS_BY_INTENT["ask_services"]
def intake_questions(service_id: str) -> dict[str, Any]:
svc = get_service_by_id(service_id) or {}
fields = svc.get("inputs_required") or []
return {
"intent": intent,
"questions": [dict(q) for q in questions],
"estimated_minutes": max(2, len(questions) * 1),
"approval_required": True,
}
def parse_intake_payload(
intent: str, raw_payload: dict[str, Any] | None,
) -> dict[str, Any]:
"""Parse + sanitize an intake payload against the intent's question set."""
raw_payload = raw_payload or {}
questions = _INTAKE_QUESTIONS_BY_INTENT.get(
intent, _INTAKE_QUESTIONS_BY_INTENT["ask_services"],
)
parsed: dict[str, Any] = {}
for q in questions:
key = q["key"]
val = raw_payload.get(key)
if val is None:
continue
# Strings get truncated to 500 chars.
if isinstance(val, str):
val = val.strip()[:500]
parsed[key] = val
return parsed
def validate_intake_completeness(
intent: str, payload: dict[str, Any],
) -> dict[str, Any]:
"""Check that all required intake fields are present."""
questions = _INTAKE_QUESTIONS_BY_INTENT.get(
intent, _INTAKE_QUESTIONS_BY_INTENT["ask_services"],
)
missing: list[str] = []
for q in questions:
if q.get("required") and not payload.get(q["key"]):
missing.append(str(q["key"]))
return {
"intent": intent,
"complete": not missing,
"missing_fields": missing,
"missing_count": len(missing),
"service_id": service_id,
"fields": [{"name": f, "prompt_ar": f"يرجى تزويدنا بـ: {f}"} for f in fields],
"demo": True,
}

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

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

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 typing import Any
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
def proof_pack_for_service(
service_id: str, *, metrics: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a Proof Pack template for any service."""
metrics = metrics or {}
def build_proof_pack(service_id: str) -> dict[str, Any]:
svc = get_service_by_id(service_id) or {}
metrics = list(svc.get("proof_metrics") or ["drafts_created", "approvals_logged"])
return {
"service_id": service_id,
"title_ar": f"Proof Pack — {service_id}",
"sections_ar": [
"ملخص تنفيذي (5 أسطر)",
"ما عمله Dealix",
"النتائج (الأرقام)",
"أبرز الردود/الاعتراضات",
"المخاطر التي تم منعها",
"Action Ledger مختصر",
"التوصية بالخطوة التالية",
],
"metrics_captured": dict(metrics),
"metrics_required": [
"opportunities_generated",
"drafts_approved",
"positive_replies",
"meetings_drafted",
"pipeline_influenced_sar",
"risks_blocked",
"time_saved_hours",
],
"delivery_format": ["pdf", "json", "whatsapp_summary"],
"approval_required": True,
"live_send_allowed": False,
}
def dispatch_proof_pack(
*,
service_id: str,
customer_id: str | None = None,
channel: str = "email",
metrics: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Dispatch a Proof Pack to a customer.
Returns a draft envelope never sends. The actual delivery requires
customer/admin approval through the Approval Center.
"""
template = proof_pack_for_service(service_id, metrics=metrics)
return {
"service_id": service_id,
"customer_id": customer_id,
"channel": channel,
"envelope": {
"subject_ar": template["title_ar"],
"body_ar": (
"مرفق Proof Pack الخاص بـ Pilot. "
"يحتوي على ملخص تنفيذي + النتائج + المخاطر التي تم منعها + "
"التوصية بالخطوة التالية."
),
"attachments": ["proof_pack.pdf", "proof_pack.json"],
},
"template": template,
"status": "draft",
"approval_required": True,
"live_send_allowed": False,
"title_ar": f"Proof Pack — {svc.get('name_ar', service_id)}",
"metrics": metrics,
"sample_counts": {m: 0 for m in metrics},
"notes_ar": "أرقام تجريبية حتى ربط عميل حقيقي ودفتر أحداث.",
"demo": True,
}

View File

@ -1,55 +1,18 @@
"""Self-Growth Mode — Dealix uses its own OS to grow.
Re-exports + extends targeting_os.self_growth_mode with operator-tier wiring.
"""
"""Self-growth mode: Dealix uses its own OS for prospecting (drafts + manual approval only)."""
from __future__ import annotations
from typing import Any
from auto_client_acquisition.targeting_os.self_growth_mode import (
DEALIX_ICP_FOCUSES,
build_dealix_self_growth_plan,
build_free_service_offer,
build_self_growth_daily_brief,
build_weekly_learning_report,
recommend_dealix_targets,
)
def build_operator_self_growth_brief(
*,
include_outreach_hint: bool = True,
) -> dict[str, Any]:
"""
Operator-tier wrapper around the self-growth daily brief.
Layers in approval-first reminders + reminders to never auto-send.
"""
base = build_self_growth_daily_brief()
out = dict(base)
out["operator_reminders_ar"] = [
"لا cold WhatsApp — حتى داخل Dealix نفسه.",
"كل رسالة draft تحتاج اعتمادك قبل الإرسال.",
"لا scraping LinkedIn — استخدم Lead Forms أو manual research.",
"كل تواصل يدخل Action Ledger.",
]
if include_outreach_hint:
out["next_action_ar"] = (
"اعتمد 3 رسائل اليوم فقط — جودة قبل كمية. "
"Pilot صغير ناجح > 50 رسالة بدون رد."
)
out["approval_required"] = True
out["live_send_allowed"] = False
return out
__all__ = [
"DEALIX_ICP_FOCUSES",
"build_dealix_self_growth_plan",
"build_free_service_offer",
"build_operator_self_growth_brief",
"build_self_growth_daily_brief",
"build_weekly_learning_report",
"recommend_dealix_targets",
]
def mode_profile() -> dict[str, Any]:
return {
"mode": "self_growth",
"priority_intents": ["want_more_customers", "ask_services", "ask_demo"],
"rules_ar": [
"لا scraping ولا إرسال جماعي.",
"كل outreach مسودة + موافقة يدوية.",
"Proof Pack أسبوعي للنتائج الداخلية.",
],
"demo": True,
}

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 typing import Any
# 6 bundles that simplify the customer's choice.
BUNDLES: tuple[dict[str, Any], ...] = (
{
"id": "growth_starter",
"name_ar": "Growth Starter",
"best_for_ar": "أي شركة تجرب Dealix لأول مرة",
"services": [
"free_growth_diagnostic",
"first_10_opportunities_sprint",
],
"deliverables_ar": [
"تشخيص نمو مجاني خلال 24 ساعة",
"10 فرص + رسائل عربية",
"Proof Pack مختصر",
],
"timeline_ar": "8 أيام (1 ديمو + 7 Pilot)",
"price_min_sar": 499,
"price_max_sar": 1500,
"proof_metrics": [
"opportunities_count", "drafts_approved",
"positive_replies", "diagnostic_to_paid_conversion",
],
"upgrade_path": ["executive_growth_os"],
BundleId = str
_BUNDLES: dict[BundleId, dict[str, Any]] = {
"growth_starter": {
"bundle_id": "growth_starter",
"title_ar": "Growth Starter",
"services": ["free_growth_diagnostic", "first_10_opportunities"],
"timeline_days": 14,
"price_range_sar": {"min": 499, "max": 499},
"best_for_ar": "شركات تريد أول قيمة سريعة + Pilot واضح.",
"deliverables_ar": ["تشخيص مجاني", "١٠ فرص + مسودات", "Proof Pack مختصر"],
"proof_metrics": ["opportunities_count", "drafts_created", "approvals_logged"],
"risk_policy_ar": "لا إرسال حي بدون موافقة؛ لا واتساب بارد.",
"upsell_path": "data_to_revenue",
},
{
"id": "data_to_revenue",
"name_ar": "Data to Revenue",
"best_for_ar": "شركات لديها قائمة عملاء/أرقام لم تُستثمر",
"services": [
"list_intelligence",
"first_10_opportunities_sprint",
],
"deliverables_ar": [
"قائمة منظفة + تصنيف مصادر",
"أفضل 50 target بالقنوات الآمنة",
"رسائل عربية لكل segment",
"Risk report + retention",
],
"timeline_ar": "10 أيام",
"price_min_sar": 1500,
"price_max_sar": 3000,
"proof_metrics": [
"contacts_classified", "safe_targets_found",
"risks_blocked", "pipeline_influenced_sar",
],
"upgrade_path": ["executive_growth_os"],
"data_to_revenue": {
"bundle_id": "data_to_revenue",
"title_ar": "من البيانات إلى الإيراد",
"services": ["list_intelligence", "first_10_opportunities"],
"timeline_days": 21,
"price_range_sar": {"min": 1500, "max": 2500},
"best_for_ar": "من لديه قائمة جهات ويريد أهدافاً مرتبة ومسودات.",
"deliverables_ar": ["أفضل ٥٠ هدفاً", "تقرير قابلية تواصل", "مسودات رسائل"],
"proof_metrics": ["safe_ratio", "drafts_created", "target_ranked"],
"risk_policy_ar": "مسودات فقط؛ موافقة قبل أي إرسال.",
"upsell_path": "executive_growth_os",
},
{
"id": "executive_growth_os",
"name_ar": "Executive Growth OS",
"best_for_ar": "CEO / Growth Manager — تشغيل شهري",
"services": [
"growth_os_monthly",
"executive_growth_brief",
],
"deliverables_ar": [
"Daily Command Feed عربي",
"Approval Center عبر واتساب",
"First 10 Opportunities أسبوعياً",
"Proof Pack شهري",
"Founder Shadow Board أسبوعي",
"Revenue Leak Detector",
],
"timeline_ar": "شهري متجدد (ابدأ بـPilot 30 يوم)",
"price_min_sar": 2999,
"price_max_sar": 2999,
"proof_metrics": [
"monthly_pipeline_sar", "monthly_meetings",
"monthly_revenue_influenced", "monthly_risks_blocked",
],
"upgrade_path": ["partnership_growth", "full_growth_control_tower"],
"executive_growth_os": {
"bundle_id": "executive_growth_os",
"title_ar": "Executive Growth OS",
"services": ["executive_growth_brief", "growth_os"],
"timeline_days": 30,
"price_range_sar": {"min": 2999, "max": 9999},
"best_for_ar": "CEO ومدير نمو يريدان موجزاً يومياً وتشغيل Growth OS.",
"deliverables_ar": ["موجز يومي", "Command feed", "Proof Pack أسبوعي"],
"proof_metrics": ["decisions_logged", "revenue_influenced_sar", "risks_blocked"],
"risk_policy_ar": "بوابة أدوات آمنة؛ تكاملات مسودة افتراضياً.",
"upsell_path": "full_growth_control_tower",
},
{
"id": "partnership_growth",
"name_ar": "Partnership Growth",
"best_for_ar": "شركات تنمو عبر الشركاء/الوكالات/الموزعين",
"services": [
"partner_sprint",
"meeting_booking_sprint",
],
"deliverables_ar": [
"20 شريك محتمل + scorecard",
"10 رسائل + drafts اجتماعات",
"Referral Agreement Draft",
"Partner-Proof Pack",
],
"timeline_ar": "14 يوم",
"price_min_sar": 3000,
"price_max_sar": 7500,
"proof_metrics": [
"partners_identified", "partner_meetings",
"referral_revenue_sar",
],
"upgrade_path": ["full_growth_control_tower"],
"partnership_growth": {
"bundle_id": "partnership_growth",
"title_ar": "نمو عبر الشراكات",
"services": ["partner_sprint", "meeting_booking_sprint"],
"timeline_days": 30,
"price_range_sar": {"min": 3000, "max": 7500},
"best_for_ar": "توسع عبر شركاء ووكالات.",
"deliverables_ar": ["قائمة شركاء", "مسودات اجتماعات", "مسودة اتفاق إحالة"],
"proof_metrics": ["partner_meetings", "referral_pipeline"],
"risk_policy_ar": "مراجعة قانونية للاتفاقيات.",
"upsell_path": "agency_partner_program",
},
{
"id": "local_growth_os",
"name_ar": "Local Growth OS",
"best_for_ar": "عيادات / متاجر / فروع / خدمات محلية",
"services": [
"local_growth_os",
"whatsapp_compliance_setup",
"list_intelligence",
],
"deliverables_ar": [
"Google Business reviews ledger + draft replies",
"WhatsApp opt-in audit + templates",
"Customer reactivation campaign drafts",
"Branch-level Proof Pack",
],
"timeline_ar": "3 أسابيع",
"price_min_sar": 999,
"price_max_sar": 2999,
"proof_metrics": [
"reviews_handled", "opt_ins_collected",
"customers_reactivated", "risks_blocked",
],
"upgrade_path": ["executive_growth_os"],
"local_growth_os": {
"bundle_id": "local_growth_os",
"title_ar": "نمو محلي",
"services": ["local_growth_os"],
"timeline_days": 30,
"price_range_sar": {"min": 999, "max": 2999},
"best_for_ar": "عيادات ومطاعم ومتاجر محلية.",
"deliverables_ar": ["كروت سمعة", "مسودات رد", "روابط دفع draft"],
"proof_metrics": ["reviews_addressed", "reactivation_drafts"],
"risk_policy_ar": "موافقة على الرسائل العامة.",
"upsell_path": "growth_os",
},
{
"id": "full_growth_control_tower",
"name_ar": "Full Growth Control Tower",
"best_for_ar": "مؤسسات تريد تشغيل كامل على 30+ يوم",
"services": [
"growth_os_monthly",
"list_intelligence",
"first_10_opportunities_sprint",
"partner_sprint",
"executive_growth_brief",
"linkedin_lead_gen_setup",
],
"deliverables_ar": [
"كل خدمات Growth OS",
"Partnership Sprint موازٍ",
"LinkedIn Lead Gen campaign",
"Founder Shadow Board",
"Service Excellence weekly review",
],
"timeline_ar": "30 يوم — قابل للتجديد",
"price_min_sar": 12000,
"price_max_sar": 25000,
"proof_metrics": [
"monthly_pipeline_sar", "monthly_revenue_influenced",
"partners_signed", "monthly_meetings",
],
"upgrade_path": [],
"full_growth_control_tower": {
"bundle_id": "full_growth_control_tower",
"title_ar": "برج تحكم كامل — مخصص",
"services": ["growth_os", "agency_partner_program"],
"timeline_days": 90,
"price_range_sar": {"min": 15000, "max": 80000},
"best_for_ar": "مؤسسات تريد كل الطبقات على مراحل.",
"deliverables_ar": ["خارطة ٣٠/٦٠/٩٠ يوماً", "حوكمة موافقات", "Proof شهري"],
"proof_metrics": ["pipeline_influenced", "partners_created", "payments_requested"],
"risk_policy_ar": "DPA + مراجعة امتثال قبل التوسع.",
"upsell_path": None,
},
)
}
def list_bundles() -> dict[str, Any]:
return {
"total": len(BUNDLES),
"bundles": [dict(b) for b in BUNDLES],
}
return {"bundles": list(_BUNDLES.values()), "demo": True}
def get_bundle(bundle_id: str) -> dict[str, Any] | None:
return next((dict(b) for b in BUNDLES if b["id"] == bundle_id), None)
def recommend_bundle(
*,
intent: str | None = None,
has_contact_list: bool = False,
is_agency: bool = False,
is_local_business: bool = False,
budget_sar: int = 1000,
) -> dict[str, Any]:
"""
Recommend the best-fit bundle deterministically.
Order of priority:
agency partnership_growth
local business local_growth_os
has list data_to_revenue
monthly budget executive_growth_os
partnerships intent partnership_growth
default growth_starter
"""
if is_agency:
chosen = "partnership_growth"
reason = "وكالة → Partnership Growth + ترقية لـ Agency Partner Program."
elif is_local_business:
chosen = "local_growth_os"
reason = "نشاط محلي → Local Growth OS."
elif has_contact_list:
chosen = "data_to_revenue"
reason = "العميل لديه قائمة → Data to Revenue."
elif intent == "want_partnerships":
chosen = "partnership_growth"
reason = "هدف الشراكات → Partnership Growth."
elif intent == "want_daily_growth" or budget_sar >= 2999:
chosen = "executive_growth_os"
reason = "تشغيل يومي/ميزانية شهرية → Executive Growth OS."
elif budget_sar >= 12000:
chosen = "full_growth_control_tower"
reason = "ميزانية كبيرة → Full Growth Control Tower."
else:
chosen = "growth_starter"
reason = "ابدأ بـ Growth Starter."
bundle = get_bundle(chosen)
return {
"recommended_bundle_id": chosen,
"bundle": bundle,
"reason_ar": reason,
"approval_required": True,
}
return _BUNDLES.get((bundle_id or "").strip())

View File

@ -1,108 +1,15 @@
"""Service Delivery Mode — runs client services + tracks SLA + generates Proof.
Production wrapper around service_orchestrator + revenue_launch.pilot_delivery
+ customer_ops.sla_tracker.
"""
"""Service delivery mode: running client services with SLA-oriented checklist."""
from __future__ import annotations
from typing import Any
from auto_client_acquisition.customer_ops import (
build_sla_health_report,
classify_sla_breach,
)
from auto_client_acquisition.revenue_launch import (
build_24h_delivery_plan,
build_first_10_opportunities_delivery,
build_growth_diagnostic_delivery,
build_list_intelligence_delivery,
)
from auto_client_acquisition.service_tower import (
build_service_workflow,
get_service,
)
def build_service_delivery_brief(
*,
customer_id: str = "",
service_id: str = "",
intake: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the day-one delivery brief for a service."""
s = get_service(service_id)
if s is None:
return {"error": f"unknown service: {service_id}"}
delivery_template_by_service: dict[str, Any] = {
"first_10_opportunities_sprint":
build_first_10_opportunities_delivery(intake or {}),
"list_intelligence":
build_list_intelligence_delivery(intake or {}),
"free_growth_diagnostic":
build_growth_diagnostic_delivery(intake or {}),
}
def mode_profile() -> dict[str, Any]:
return {
"mode": "service_delivery",
"customer_id": customer_id,
"service_id": service_id,
"service_name_ar": s.name_ar,
"intake_received": bool(intake),
"workflow": build_service_workflow(service_id),
"delivery_template": delivery_template_by_service.get(
service_id, build_24h_delivery_plan(service_id),
),
"approval_required": True,
"live_send_allowed": False,
}
def build_sla_status_for_delivery(
*,
customer_id: str = "",
open_tickets: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Compute SLA health for a customer's open delivery tickets."""
health = build_sla_health_report(tickets=open_tickets)
breaches: list[dict[str, Any]] = []
for t in (open_tickets or []):
b = classify_sla_breach(
priority=str(t.get("priority", "P3")),
minutes_to_first_response=t.get("first_response_min"),
hours_to_resolve=t.get("resolution_hours"),
)
if b["breached"]:
breaches.append({**t, "breach": b})
return {
"customer_id": customer_id,
"health": health,
"breaches": breaches,
"approval_required": True,
}
def build_post_delivery_handoff(
*,
customer_id: str = "",
service_id: str = "",
delivered_metrics: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the post-delivery handoff (Arabic) → Customer Success cadence."""
metrics = delivered_metrics or {}
return {
"mode": "service_delivery",
"customer_id": customer_id,
"service_id": service_id,
"delivered_metrics": dict(metrics),
"handoff_steps_ar": [
"تسليم Proof Pack النهائي للعميل + اعتماده.",
"حجز جلسة مراجعة 30 دقيقة.",
"تفعيل Customer Success cadence (weekly check-ins).",
"اقتراح الترقية المنطقية بناءً على النتائج.",
"تحديث Action Graph + Revenue Work Units.",
],
"approval_required": True,
"live_send_allowed": False,
"priority_intents": ["approve_action", "ask_proof", "want_meetings"],
"card_types_first": ["approval_needed", "proof", "meeting_prep"],
"sla_reminder_ar": "التسليم حسب نافذة الـ Pilot المتفق عليها؛ لا live send افتراضياً.",
"demo": True,
}

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 typing import Any
# Canonical pipeline every service goes through.
SERVICE_PIPELINE_STEPS: tuple[str, ...] = (
"intake",
"data_check",
"targeting",
"contactability",
"strategy",
"drafting",
"approval",
"execution_or_export",
"tracking",
"proof",
"upsell",
)
_STEP_LABELS_AR: dict[str, str] = {
"intake": "جمع المدخلات",
"data_check": "فحص جودة البيانات",
"targeting": "تحديد الأهداف",
"contactability": "تقييم إمكانية التواصل",
"strategy": "صياغة الاستراتيجية",
"drafting": "كتابة المسودات",
"approval": "اعتماد بشري",
"execution_or_export": "تنفيذ أو تصدير",
"tracking": "متابعة النتائج",
"proof": "Proof Pack",
"upsell": "ترقية الخدمة",
}
from auto_client_acquisition.autonomous_service_operator import intent_classifier as ic
from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
def build_service_pipeline(
service_id: str, *, customer_id: str = "",
) -> dict[str, Any]:
"""Build the canonical pipeline state for a service."""
def recommend_for_intent(intent: str) -> dict[str, Any]:
"""Return primary service_id, optional bundle, and excellence gate."""
mapping: dict[str, tuple[str, str | None]] = {
ic.INTENT_WANT_MORE_CUSTOMERS: ("first_10_opportunities", "growth_starter"),
ic.INTENT_HAS_CONTACT_LIST: ("list_intelligence", "data_to_revenue"),
ic.INTENT_WANT_PARTNERSHIPS: ("partner_sprint", "partnership_growth"),
ic.INTENT_ASK_PARTNERSHIP: ("partner_sprint", "partnership_growth"),
ic.INTENT_WANT_DAILY_GROWTH: ("self_growth_operator", "executive_growth_os"),
ic.INTENT_WANT_MEETINGS: ("meeting_booking_sprint", None),
ic.INTENT_WANT_EMAIL_RESCUE: ("email_revenue_rescue", None),
ic.INTENT_WANT_WHATSAPP_SETUP: ("whatsapp_compliance_setup", None),
ic.INTENT_ASK_PRICING: ("growth_os", None),
ic.INTENT_ASK_SERVICES: ("free_growth_diagnostic", None),
ic.INTENT_ASK_DEMO: ("free_growth_diagnostic", None),
ic.INTENT_ASK_PROOF: ("first_10_opportunities", None),
ic.INTENT_ASK_REVENUE_TODAY: ("growth_os", None),
}
sid, bundle = mapping.get(intent, ("free_growth_diagnostic", None))
svc = get_service_by_id(sid) or {}
score = calculate_service_excellence_score(sid)
return {
"service_id": service_id,
"customer_id": customer_id,
"current_step": "intake",
"completed_steps": [],
"steps": [
{
"step_id": s,
"label_ar": _STEP_LABELS_AR.get(s, s),
"completed": False,
"approval_required": s in {
"drafting", "approval", "execution_or_export",
},
}
for s in SERVICE_PIPELINE_STEPS
],
"approval_required": True,
"live_send_allowed": False,
"intent": intent,
"recommended_service_id": sid,
"service_name_ar": svc.get("name_ar"),
"suggested_bundle_id": bundle,
"excellence": {"total_score": score["total_score"], "status": score["status"]},
"demo": True,
}
def run_service_step(
pipeline: dict[str, Any], *, step_id: str | None = None,
) -> dict[str, Any]:
"""
Mark the current (or supplied) step as run + advance the pipeline.
Does NOT execute any external action only updates state.
"""
target = step_id or pipeline.get("current_step")
steps = list(pipeline.get("steps", []))
found = False
for i, s in enumerate(steps):
if s.get("step_id") == target:
s["completed"] = True
steps[i] = s
found = True
# Move to next step.
if i + 1 < len(steps):
pipeline["current_step"] = steps[i + 1]["step_id"]
else:
pipeline["current_step"] = "done"
break
if not found:
return {**pipeline, "error": f"unknown step: {target}"}
completed = [s["step_id"] for s in steps if s["completed"]]
pipeline["steps"] = steps
pipeline["completed_steps"] = completed
pipeline["progress_pct"] = round(
100 * len(completed) / max(1, len(steps)), 1,
)
return pipeline
def cold_whatsapp_response() -> dict[str, Any]:
return {
"blocked": True,
"message_ar": "لا ندعم واتساب بارد أو غير موافق عليه. نرشّح: قالب opt-in، أو إيميل draft، أو سباق اجتماعات بعد موافقة.",
"alternatives": ["whatsapp_opt_in_template", "gmail_draft", "meeting_booking_sprint"],
"demo": True,
}

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

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

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

View File

@ -1,75 +1,17 @@
"""WhatsApp renderer — convert cards/briefs to WhatsApp-ready format.
Drafts only. Never sends. Always emits buttons_ar capped at 3 (WhatsApp Reply
Buttons limit) and Arabic body text.
"""
"""WhatsApp payload shapes (text templates only — no live send)."""
from __future__ import annotations
from typing import Any
def render_card_for_whatsapp(card: dict[str, Any]) -> dict[str, Any]:
"""Render any decision card as a WhatsApp-style draft message."""
title = str(card.get("title_ar", "")).strip()[:60]
summary = str(card.get("summary_ar", "")).strip()[:300]
why_now = str(card.get("why_now_ar", "")).strip()[:200]
action = str(card.get("recommended_action_ar", "")).strip()[:200]
risk = str(card.get("risk_level", "")).strip()
buttons = list(card.get("buttons_ar", []))[:3]
body_lines: list[str] = [title]
if summary:
body_lines.append("")
body_lines.append(summary)
if why_now:
body_lines.append("")
body_lines.append(f"لماذا الآن: {why_now}")
if action:
body_lines.append(f"الإجراء المقترح: {action}")
if risk:
body_lines.append(f"المخاطرة: {risk}")
if buttons:
body_lines.append("")
body_lines.append("أزرار: " + " | ".join(buttons))
def render_daily_brief_stub() -> dict[str, Any]:
return {
"channel": "whatsapp",
"kind": "card_draft",
"body_ar": "\n".join(body_lines),
"buttons_ar": buttons,
"approval_required": True,
"live_send_allowed": False,
}
def render_approval_card_for_whatsapp(
card: dict[str, Any],
) -> dict[str, Any]:
"""Render an approval card specifically — guarantees the 3 standard buttons."""
out = render_card_for_whatsapp(card)
out["buttons_ar"] = card.get("buttons_ar") or ["اعتمد", "عدّل", "تخطي"]
out["kind"] = "approval_card"
return out
def render_daily_brief_for_whatsapp(brief: dict[str, Any]) -> dict[str, Any]:
"""Render a CEO/Growth Manager daily brief as WhatsApp draft."""
summary_lines = list(brief.get("summary_ar", []))[:8]
decisions = list(brief.get("priority_decisions_ar", []))[:3]
body_lines = ["صباح الخير 👋", "", "أهم اليوم:"]
body_lines.extend(f"{line}" for line in summary_lines)
if decisions:
body_lines.append("")
body_lines.append("3 قرارات تنتظر:")
body_lines.extend(f"{i + 1}. {d}" for i, d in enumerate(decisions))
return {
"channel": "whatsapp",
"kind": "daily_brief_draft",
"body_ar": "\n".join(body_lines),
"buttons_ar": ["اعرض القرارات", "Proof Pack", "لاحقاً"],
"approval_required": True,
"live_send_allowed": False,
"format": "text_stub",
"body_ar": (
"موجز Dealix (مسودة): ٣ قرارات مقترحة — راجع لوحة الموافقات. "
"لا يُرسل هذا النص تلقائياً من المنصة في MVP."
),
"demo": True,
}

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 typing import Any
from .service_orchestrator import (
SERVICE_PIPELINE_STEPS,
build_service_pipeline,
run_service_step,
)
from auto_client_acquisition.autonomous_service_operator import session_state as ss
def build_workflow_state(service_id: str, *, customer_id: str = "") -> dict[str, Any]:
"""Initialize a new workflow state for a service."""
pipeline = build_service_pipeline(service_id, customer_id=customer_id)
return {
"service_id": service_id,
"customer_id": customer_id,
"pipeline": pipeline,
"human_approvals_received": 0,
"human_approvals_pending": 0,
"blocked_actions": 0,
def advance(session_id: str, event: str) -> dict[str, Any]:
"""event: start_service | draft_ready | submit_for_approval | proof_ready"""
s = ss.touch_session(session_id)
state = str(s.get("workflow_state") or "idle")
ev = (event or "").strip().lower()
transitions: dict[tuple[str, str], str] = {
("idle", "start_service"): "intake",
("intake", "draft_ready"): "draft",
("draft", "submit_for_approval"): "pending_approval",
("pending_approval", "proof_ready"): "proof",
("proof", "start_service"): "intake",
}
def advance_workflow(
workflow_state: dict[str, Any], *, step_id: str | None = None,
) -> dict[str, Any]:
"""Advance the underlying pipeline by one step."""
pipeline = workflow_state.get("pipeline") or build_service_pipeline(
str(workflow_state.get("service_id", "")),
)
pipeline = run_service_step(pipeline, step_id=step_id)
workflow_state["pipeline"] = pipeline
return workflow_state
def is_workflow_complete(workflow_state: dict[str, Any]) -> bool:
"""True iff all canonical steps have run."""
pipeline = workflow_state.get("pipeline", {})
completed = pipeline.get("completed_steps", [])
return len(completed) >= len(SERVICE_PIPELINE_STEPS)
key = (state, ev)
new_state = transitions.get(key, state)
return ss.upsert_session(session_id, {"workflow_state": new_state, "last_event": ev})

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":
- onboarding_checklist: 8-step Pilot onboarding
- 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 auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist
from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary
from __future__ import annotations
from .connector_setup_status import (
SUPPORTED_CONNECTORS,
build_connector_setup_summary,
get_connector_status,
update_connector_status,
)
from .customer_success_cadence import (
CADENCE_TYPES,
build_at_risk_alert,
build_customer_success_plan,
build_weekly_check_in,
)
from .incident_router import (
INCIDENT_SEVERITIES,
build_incident_response_plan,
triage_incident,
)
from .onboarding_checklist import (
ONBOARDING_STEPS,
build_onboarding_checklist,
update_onboarding_step,
)
from .sla_tracker import (
SLA_TARGETS,
build_sla_health_report,
classify_sla_breach,
record_sla_event,
)
from .support_ticket_router import (
SUPPORT_PRIORITIES,
build_first_response_template,
classify_ticket_priority,
route_ticket,
)
__all__ = [
# connector_setup_status
"SUPPORTED_CONNECTORS",
"build_connector_setup_summary",
"get_connector_status",
"update_connector_status",
# customer_success_cadence
"CADENCE_TYPES",
"build_at_risk_alert",
"build_customer_success_plan",
"build_weekly_check_in",
# incident_router
"INCIDENT_SEVERITIES",
"build_incident_response_plan",
"triage_incident",
# onboarding_checklist
"ONBOARDING_STEPS",
"build_onboarding_checklist",
"update_onboarding_step",
# sla_tracker
"SLA_TARGETS",
"build_sla_health_report",
"classify_sla_breach",
"record_sla_event",
# support_ticket_router
"SUPPORT_PRIORITIES",
"build_first_response_template",
"classify_ticket_priority",
"route_ticket",
]
__all__ = ["build_onboarding_checklist", "build_sla_summary"]

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 typing import Any
# 11 connectors Dealix supports during onboarding.
SUPPORTED_CONNECTORS: tuple[dict[str, Any], ...] = (
{"key": "gmail", "label_ar": "Gmail", "default_mode": "draft_only",
"blocking": False, "phase": "phase_1"},
{"key": "google_calendar", "label_ar": "Google Calendar",
"default_mode": "draft_only", "blocking": False, "phase": "phase_1"},
{"key": "google_sheets", "label_ar": "Google Sheets",
"default_mode": "approved_execute", "blocking": False, "phase": "phase_1"},
{"key": "moyasar", "label_ar": "Moyasar (manual invoice)",
"default_mode": "manual", "blocking": False, "phase": "phase_1"},
{"key": "whatsapp_cloud", "label_ar": "WhatsApp Business",
"default_mode": "draft_only", "blocking": True, "phase": "phase_1"},
{"key": "website_forms", "label_ar": "Website Forms",
"default_mode": "approved_execute", "blocking": False, "phase": "phase_1"},
{"key": "linkedin_lead_forms", "label_ar": "LinkedIn Lead Gen Forms",
"default_mode": "ingest_only", "blocking": False, "phase": "phase_2"},
{"key": "google_business_profile", "label_ar": "Google Business Profile",
"default_mode": "draft_only", "blocking": False, "phase": "phase_2"},
{"key": "crm_generic", "label_ar": "CRM (HubSpot/Salesforce/Zoho/Close)",
"default_mode": "draft_only", "blocking": False, "phase": "phase_2"},
{"key": "google_meet", "label_ar": "Google Meet (transcripts)",
"default_mode": "ingest_only", "blocking": False, "phase": "phase_2"},
{"key": "instagram_graph", "label_ar": "Instagram (comments/DMs)",
"default_mode": "ingest_only", "blocking": False, "phase": "phase_3"},
)
def get_connector_status(connector_key: str) -> dict[str, Any]:
"""Return the static description of a connector."""
c = next((dict(c) for c in SUPPORTED_CONNECTORS if c["key"] == connector_key), None)
if c is None:
return {"error": f"unknown connector: {connector_key}"}
return c
def update_connector_status(
statuses: dict[str, dict[str, Any]],
*,
connector_key: str,
state: str,
notes: str = "",
) -> dict[str, dict[str, Any]]:
"""Update the live status of a connector for a customer."""
if state not in {"not_started", "configuring", "connected_draft_only",
"connected_approved_execute", "failed", "skipped"}:
raise ValueError(f"Unknown connector state: {state}")
statuses[connector_key] = {
"state": state,
"notes": notes[:200],
}
return statuses
def build_connector_setup_summary(
*,
customer_id: str = "",
statuses: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a connector setup summary for a customer."""
statuses = statuses or {}
connected = 0
blocking_missing: list[str] = []
by_state: dict[str, int] = {}
items: list[dict[str, Any]] = []
for c in SUPPORTED_CONNECTORS:
live = statuses.get(c["key"], {})
state = live.get("state", "not_started")
by_state[state] = by_state.get(state, 0) + 1
if state in ("connected_draft_only", "connected_approved_execute"):
connected += 1
if c["blocking"] and state not in (
"connected_draft_only", "connected_approved_execute",
):
blocking_missing.append(c["key"])
items.append({**c, "state": state, "notes": live.get("notes", "")})
total = len(SUPPORTED_CONNECTORS)
pct = round(100 * connected / total, 1) if total else 0.0
def build_connector_status() -> dict[str, Any]:
return {
"customer_id": customer_id,
"total_connectors": total,
"connected_count": connected,
"connected_pct": pct,
"blocking_missing": blocking_missing,
"by_state": by_state,
"items": items,
"ready_for_first_service": (
len(blocking_missing) == 0 and connected >= 1
),
"connectors": [
{
"id": "whatsapp",
"name_ar": "واتساب",
"status": "draft_only",
"notes_ar": "الإرسال الحي يتطلب opt-in وسياسة وموافقة.",
},
{
"id": "gmail",
"name_ar": "Gmail",
"status": "draft_ready",
"notes_ar": "المسودات أولاً؛ الإرسال محظور افتراضياً.",
},
{
"id": "google_calendar",
"name_ar": "Google Calendar",
"status": "draft_ready",
"notes_ar": "إدراج الحدث يتطلب موافقة.",
},
{
"id": "moyasar",
"name_ar": "Moyasar",
"status": "manual_or_sandbox",
"notes_ar": "روابط دفع/فواتير يدوية أو sandbox؛ لا charge من المنصة افتراضياً.",
},
{
"id": "linkedin_lead_forms",
"name_ar": "LinkedIn Lead Gen",
"status": "strategy_only",
"notes_ar": "لا scraping؛ نماذج رسمية وإعلانات ومهام يدوية معتمدة.",
},
],
"summary_ar": "الوضع الافتراضي: مسودات وموافقات؛ لا توسيع live قبل staging واتفاق العميل.",
}

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 typing import Any
# Cadence types Dealix supports.
CADENCE_TYPES: tuple[str, ...] = (
"weekly_check_in",
"monthly_proof_review",
"quarterly_business_review",
"at_risk_alert",
"renewal_30_day",
"renewal_7_day",
)
def build_weekly_check_in(
*,
customer_id: str = "",
company_name: str = "",
metrics: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a weekly check-in agenda + Arabic talking points."""
m = metrics or {}
drafts = int(m.get("drafts_approved", 0))
replies = int(m.get("replies", 0))
meetings = int(m.get("meetings", 0))
risks = int(m.get("risks_blocked", 0))
pipeline = float(m.get("pipeline_sar", 0))
def build_weekly_cadence() -> dict[str, Any]:
return {
"customer_id": customer_id,
"company_name": company_name,
"type": "weekly_check_in",
"agenda_ar": [
"مراجعة آخر Proof Pack (5 دقائق).",
"أبرز فرصة في الـ pipeline (5 دقائق).",
"أبرز خطر في القنوات (5 دقائق).",
"خطة الأسبوع القادم (5 دقائق).",
"أي مساعدة من فريقنا؟ (5 دقائق).",
"weekly_touchpoints_ar": [
"مراجعة كروت الموافقة المعلقة.",
"تحديث Proof Pack (مسودات، موافقات، مخاطر ممنوعة).",
"مكالمة قصيرة أو تحديث كتابي مع صاحب القرار.",
"قراءة مؤشرات القنوات (ردود، شكاوى، opt-out = صفر مطلوب).",
],
"talking_points_ar": [
f"اعتمدتم {drafts} رسالة هذا الأسبوع، ووصلكم {replies} رد.",
f"تم تجهيز {meetings} اجتماع.",
f"تم منع {risks} مخاطر تلقائياً.",
f"Pipeline متأثر بقيمة {pipeline:.0f} ريال.",
],
"approval_required": True,
"live_send_allowed": False,
}
def build_at_risk_alert(
*,
customer_id: str = "",
days_inactive: int = 0,
drafts_pending: int = 0,
last_proof_pack_days_ago: int = 0,
) -> dict[str, Any]:
"""Build an at-risk alert when a customer shows churn signals."""
risk_score = 0
reasons: list[str] = []
if days_inactive >= 14:
risk_score += 40
reasons.append(f"العميل غير نشط منذ {days_inactive} يوم.")
elif days_inactive >= 7:
risk_score += 20
reasons.append(f"انخفاض النشاط منذ {days_inactive} يوم.")
if drafts_pending >= 10:
risk_score += 25
reasons.append(f"{drafts_pending} مسودة معلقة بدون اعتماد.")
elif drafts_pending >= 5:
risk_score += 10
reasons.append(f"تراكم {drafts_pending} مسودة بدون اعتماد.")
if last_proof_pack_days_ago >= 14:
risk_score += 30
reasons.append(
f"آخر Proof Pack قبل {last_proof_pack_days_ago} يوم — يتجاوز SLA."
)
risk_score = min(100, risk_score)
if risk_score >= 60:
severity = "high"
action_ar = "أرسل إيميل personal من المؤسس + احجز QBR هذا الأسبوع."
elif risk_score >= 30:
severity = "medium"
action_ar = "أرسل Proof Pack ملخص + اقترح ديمو لخدمة جديدة."
else:
severity = "low"
action_ar = "متابعة weekly check-in عادية."
return {
"customer_id": customer_id,
"type": "at_risk_alert",
"risk_score": risk_score,
"severity": severity,
"reasons_ar": reasons,
"recommended_action_ar": action_ar,
"approval_required": True,
"live_send_allowed": False,
}
def build_customer_success_plan(
*,
customer_id: str = "",
bundle_id: str = "growth_starter",
) -> dict[str, Any]:
"""Build a 30-day customer success cadence plan."""
cadence_by_bundle = {
"growth_starter": [
"Day 1: kick-off call + intake.",
"Day 3: review first 3 opportunities + drafts.",
"Day 7: deliver Proof Pack v1.",
"Day 14: weekly check-in + upsell offer.",
"Day 30: monthly proof review + renewal/upgrade decision.",
],
"executive_growth_os": [
"Day 1: onboarding + connect channels.",
"Day 7: first weekly Proof Pack.",
"Day 14: weekly check-in + Founder Shadow Board v1.",
"Day 21: monthly proof review.",
"Day 30: QBR + annual upgrade conversation.",
],
"partnership_growth": [
"Day 1: partner ICP intake.",
"Day 5: 20 partners list + 10 outreach drafts.",
"Day 10: 5 partner meetings booked.",
"Day 14: weekly check-in.",
"Day 30: partner scorecard + revenue share setup.",
"metrics_to_track": [
"demos_booked",
"pilots_active",
"drafts_approved",
"risks_blocked",
"proof_events",
],
}
return {
"customer_id": customer_id,
"bundle_id": bundle_id,
"cadence_ar": cadence_by_bundle.get(
bundle_id, cadence_by_bundle["growth_starter"],
),
"default_cadence_type": "weekly_check_in",
"approval_required": True,
}

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 typing import Any
INCIDENT_SEVERITIES: tuple[dict[str, Any], ...] = (
{
"id": "SEV1",
"label_ar": "حرج جداً — تسريب أمان / إرسال خاطئ / تعطل كامل",
"first_action_minutes": 15,
"communication_cadence_minutes": 30,
},
{
"id": "SEV2",
"label_ar": "خدمة مهمة معطلة لعدد كبير من العملاء",
"first_action_minutes": 30,
"communication_cadence_minutes": 60,
},
{
"id": "SEV3",
"label_ar": "خدمة معطلة لعميل واحد أو degraded performance",
"first_action_minutes": 120,
"communication_cadence_minutes": 240,
},
)
def triage_incident(
*,
title: str,
description: str = "",
affected_customers: int = 1,
has_data_leak: bool = False,
has_unauthorized_send: bool = False,
) -> dict[str, Any]:
"""Triage an incident → severity + first actions + comms cadence."""
if has_data_leak or has_unauthorized_send:
sev = "SEV1"
reason_ar = (
"تسريب أمان أو إرسال غير معتمد — أعلى أولوية."
)
elif affected_customers >= 5:
sev = "SEV2"
reason_ar = f"عدد العملاء المتأثرين: {affected_customers} ≥ 5."
else:
sev = "SEV3"
reason_ar = "حدث محدود التأثير."
severity = next(
(dict(s) for s in INCIDENT_SEVERITIES if s["id"] == sev),
dict(INCIDENT_SEVERITIES[2]),
)
def build_incident_playbook() -> dict[str, Any]:
return {
"title": title[:120],
"description": description[:500],
"severity": sev,
"reason_ar": reason_ar,
"severity_details": severity,
"affected_customers": affected_customers,
"has_data_leak": has_data_leak,
"has_unauthorized_send": has_unauthorized_send,
"approval_required": True,
"live_send_allowed": False,
"steps_ar": [
"تصنيف الخطورة (P0P3) وفق وصف الحادث.",
"إيقاف أي إجراء live إن وُجد حتى التحقق.",
"توثيق الوقت، التأثير، والخطوات المتخذة (بدون أسرار أو PII خام).",
"إشعار العميل بلغة واضحة وخطة تعافي.",
"مراجعة لاحقة وتحديث السياسات/الاختبارات إن لزم.",
],
"contacts_placeholder_ar": "يُحدَّد في العقد: بريد دعم + قناة طوارئ للـ P0.",
}
def build_incident_response_plan(
*,
severity: str = "SEV3",
) -> dict[str, Any]:
"""Build the canonical incident response plan (Arabic)."""
common_steps = [
"1. تجميد الـ live actions على القناة المعنية فوراً.",
"2. إخطار المؤسس + on-call operator.",
"3. إنشاء incident channel مع timeline.",
"4. مراجعة Action Ledger للأفعال المرتبطة.",
"5. إذا تسريب: إخطار العملاء المتأثرين خلال 72 ساعة (PDPL).",
]
if severity == "SEV1":
plan = common_steps + [
"6. تواصل مباشر مع المؤسس + خلية أزمة.",
"7. كتابة post-mortem خلال 24 ساعة.",
"8. مراجعة قانونية إن لزم.",
]
elif severity == "SEV2":
plan = common_steps + [
"6. تحديث العملاء المتأثرين كل 60 دقيقة.",
"7. post-mortem خلال 48 ساعة.",
]
else:
plan = common_steps + [
"6. تحديث العميل المتأثر مع كل خطوة.",
"7. post-mortem اختياري.",
]
return {
"severity": severity,
"plan_ar": plan,
"approval_required": True,
"live_send_allowed": False,
}
def classify_incident(severity: str) -> dict[str, Any]:
s = (severity or "P3").upper()
if s not in {"P0", "P1", "P2", "P3"}:
s = "P3"
return {"severity": s, "escalate": s in {"P0", "P1"}}

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 typing import Any
ONBOARDING_STEPS: tuple[dict[str, Any], ...] = (
{
"id": "select_goal",
"label_ar": "اختيار الهدف الأساسي",
"input_required": "goal",
"minutes": 2,
"approval_required": False,
},
{
"id": "select_bundle",
"label_ar": "اختيار الباقة المناسبة",
"input_required": "bundle_id",
"minutes": 3,
"approval_required": True,
},
{
"id": "company_intake",
"label_ar": "بيانات الشركة",
"input_required": "company_profile",
"minutes": 5,
"approval_required": False,
},
{
"id": "connect_channels",
"label_ar": "ربط القنوات (Gmail/Calendar/Sheets — drafts فقط)",
"input_required": "channels_oauth",
"minutes": 8,
"approval_required": True,
},
{
"id": "upload_or_source",
"label_ar": "رفع قائمة أو ربط مصدر leads",
"input_required": "list_or_source",
"minutes": 5,
"approval_required": True,
},
{
"id": "risk_review",
"label_ar": "مراجعة المخاطر (PDPL + سمعة القناة)",
"input_required": None,
"minutes": 4,
"approval_required": True,
},
{
"id": "first_service_run",
"label_ar": "تشغيل أول خدمة (First 10 Opportunities أو List Intelligence)",
"input_required": None,
"minutes": 0, # async — Dealix runs it
"approval_required": True,
},
{
"id": "first_proof_pack",
"label_ar": "استلام أول Proof Pack",
"input_required": None,
"minutes": 0, # async
"approval_required": False,
},
)
def build_onboarding_checklist(
*,
customer_id: str = "",
company_name: str = "",
bundle_id: str | None = None,
) -> dict[str, Any]:
"""Build a fresh onboarding checklist for a new customer."""
def build_onboarding_checklist(service_id: str | None = None) -> dict[str, Any]:
sid = (service_id or "growth_starter").strip() or "growth_starter"
return {
"customer_id": customer_id,
"company_name": company_name,
"bundle_id": bundle_id,
"total_steps": len(ONBOARDING_STEPS),
"current_step_id": ONBOARDING_STEPS[0]["id"],
"steps": [
{**dict(s), "completed": False} for s in ONBOARDING_STEPS
"service_id": sid,
"steps_ar": [
"تأكيد الهدف (عملاء جدد / قائمة / شراكات / تشغيل يومي).",
"جمع بيانات الشركة: القطاع، المدينة، العرض، رابط الموقع.",
"تحديد القنوات المتاحة (إيميل، واتساب opt-in، CRM، نماذج).",
"رفع قائمة اختيارية أو تأكيد عدم وجود قائمة.",
"مراجعة سياسة الموافقات وعدم الإرسال الحي الافتراضي.",
"تشغيل أول مهمة (تشخيص أو 10 فرص أو List Intelligence).",
"تسليم أول Proof Pack أو ملخص أثر خلال النافذة المتفق عليها.",
],
"estimated_total_minutes": sum(int(s["minutes"]) for s in ONBOARDING_STEPS),
"live_send_allowed": False,
"approval_required": True,
"live_send_default": False,
}
def update_onboarding_step(
checklist: dict[str, Any],
*,
step_id: str,
completed: bool = True,
notes: str = "",
) -> dict[str, Any]:
"""Mark a step complete + advance current_step_id."""
steps = list(checklist.get("steps", []))
found = False
for i, s in enumerate(steps):
if s["id"] == step_id:
s["completed"] = bool(completed)
if notes:
s["notes"] = notes[:200]
steps[i] = s
found = True
# advance current_step_id
if completed and i + 1 < len(steps):
checklist["current_step_id"] = steps[i + 1]["id"]
elif completed and i + 1 == len(steps):
checklist["current_step_id"] = "done"
break
if not found:
return {**checklist, "error": f"unknown step: {step_id}"}
completed_count = sum(1 for s in steps if s["completed"])
checklist["steps"] = steps
checklist["progress_pct"] = round(
100 * completed_count / max(1, len(steps)), 1,
)
return checklist

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
import time
from typing import Any
# Default SLA targets per priority (minutes for first_response, hours for resolution).
SLA_TARGETS: dict[str, dict[str, float]] = {
"P0": {"first_response_min": 30, "resolution_hours": 4},
"P1": {"first_response_min": 120, "resolution_hours": 24},
"P2": {"first_response_min": 480, "resolution_hours": 72},
"P3": {"first_response_min": 1440, "resolution_hours": 168},
}
def record_sla_event(
*,
ticket_id: str,
priority: str,
event: str,
log: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""
Record an SLA event.
`event` = "opened" | "first_response" | "resolved" | "escalated".
"""
if event not in {"opened", "first_response", "resolved", "escalated"}:
raise ValueError(f"Unknown SLA event: {event}")
entry: dict[str, Any] = {
"ticket_id": ticket_id,
"priority": priority,
"event": event,
"ts": time.time(),
}
if log is not None:
log.append(entry)
return entry
def classify_sla_breach(
*,
priority: str,
minutes_to_first_response: float | None = None,
hours_to_resolve: float | None = None,
) -> dict[str, Any]:
"""Classify whether SLA was breached for a single ticket."""
target = SLA_TARGETS.get(priority, SLA_TARGETS["P3"])
breaches: list[str] = []
if (minutes_to_first_response is not None
and minutes_to_first_response > target["first_response_min"]):
breaches.append(
f"first_response: {minutes_to_first_response:.0f} > "
f"{target['first_response_min']} min"
)
if (hours_to_resolve is not None
and hours_to_resolve > target["resolution_hours"]):
breaches.append(
f"resolution: {hours_to_resolve:.1f}h > "
f"{target['resolution_hours']}h"
)
def build_sla_summary() -> dict[str, Any]:
return {
"priority": priority,
"breached": bool(breaches),
"breaches": breaches,
}
def build_sla_health_report(
*,
tickets: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a weekly SLA health report from a list of tickets."""
tickets = tickets or []
by_priority: dict[str, dict[str, Any]] = {}
total_tickets = len(tickets)
total_breached = 0
for t in tickets:
priority = str(t.get("priority", "P3"))
bucket = by_priority.setdefault(priority, {
"count": 0, "breaches": 0,
"total_first_response_min": 0.0,
"total_resolution_hours": 0.0,
"responded_count": 0, "resolved_count": 0,
})
bucket["count"] += 1
ftr = t.get("first_response_min")
ttr = t.get("resolution_hours")
b = classify_sla_breach(
priority=priority,
minutes_to_first_response=ftr,
hours_to_resolve=ttr,
)
if b["breached"]:
bucket["breaches"] += 1
total_breached += 1
if ftr is not None:
bucket["total_first_response_min"] += float(ftr)
bucket["responded_count"] += 1
if ttr is not None:
bucket["total_resolution_hours"] += float(ttr)
bucket["resolved_count"] += 1
# Compute averages.
for p, b in by_priority.items():
if b["responded_count"]:
b["avg_first_response_min"] = round(
b["total_first_response_min"] / b["responded_count"], 1,
)
if b["resolved_count"]:
b["avg_resolution_hours"] = round(
b["total_resolution_hours"] / b["resolved_count"], 2,
)
breach_rate = round(total_breached / total_tickets, 3) if total_tickets else 0.0
return {
"total_tickets": total_tickets,
"total_breached": total_breached,
"breach_rate": breach_rate,
"by_priority": by_priority,
"verdict": (
"healthy" if breach_rate < 0.10
else "watch" if breach_rate < 0.25
else "critical"
),
"tiers": [
{
"id": "P0",
"name_ar": "أمان / إرسال خاطئ / توقف كامل",
"first_response_hours": 2,
"resolution_target_hours": 8,
},
{
"id": "P1",
"name_ar": "تعطل خدمة أساسية",
"first_response_hours": 4,
"resolution_target_hours": 24,
},
{
"id": "P2",
"name_ar": "تكامل أو Proof متأخر",
"first_response_hours": 24,
"resolution_target_hours": 72,
},
{
"id": "P3",
"name_ar": "سؤال أو تحسين",
"first_response_hours": 48,
"resolution_target_hours": 120,
},
],
"notes_ar": "الأرقام أهداف تشغيلية للـ Pilot؛ تُحدّث في العقد/Appendix عند التوسع.",
}

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
import re
from typing import Any
# 4 priority tiers Dealix supports.
SUPPORT_PRIORITIES: tuple[dict[str, Any], ...] = (
{
"id": "P0",
"label_ar": "حرج جداً — أمان / إرسال خاطئ / تعطل كامل",
"first_response_minutes": 30,
"resolution_target_hours": 4,
"escalation_owner": "founder",
},
{
"id": "P1",
"label_ar": "خدمة مهمة معطلة",
"first_response_minutes": 120,
"resolution_target_hours": 24,
"escalation_owner": "operator_oncall",
},
{
"id": "P2",
"label_ar": "Connector أو Proof Pack متأخر",
"first_response_minutes": 480, # 8h
"resolution_target_hours": 72,
"escalation_owner": "operator_oncall",
},
{
"id": "P3",
"label_ar": "سؤال عام / تحسين",
"first_response_minutes": 1440, # 24h
"resolution_target_hours": 168, # 1 week
"escalation_owner": "operator_team",
},
)
# Keyword → priority hints.
_P0_KEYWORDS = (
"أمان", "تسريب", "إرسال خاطئ", "إرسال بدون موافقة",
"بدون موافقتي", "أرسل رسالة بدون", "أرسل بدون",
"secret", "leak", "data breach", "outage", "completely down",
"live charge", "charge بدون موافقة", "unauthorized",
)
_P1_KEYWORDS = (
"service down", "خدمة معطلة", "service failed",
"Pilot stopped", "Proof Pack مفقود",
)
_P2_KEYWORDS = (
"connector", "Gmail", "Calendar", "Sheets",
"WhatsApp setup", "Moyasar invoice",
)
def classify_ticket_priority(text: str) -> dict[str, Any]:
"""
Classify a free-text support ticket P0 / P1 / P2 / P3.
Deterministic keyword matching. Returns matched priority + reasoning.
"""
text = (text or "").strip()
if not text:
return {"priority": "P3", "reason_ar": "لا يوجد نص — اعتبار افتراضي."}
text_lc = text.lower()
for kw in _P0_KEYWORDS:
if kw in text or kw.lower() in text_lc:
return {
"priority": "P0",
"matched_keyword": kw,
"reason_ar": f"كلمة حرجة مطابقة: {kw}",
}
for kw in _P1_KEYWORDS:
if kw in text or kw.lower() in text_lc:
return {
"priority": "P1",
"matched_keyword": kw,
"reason_ar": f"خدمة مهمة معطلة: {kw}",
}
for kw in _P2_KEYWORDS:
if kw in text or kw.lower() in text_lc:
return {
"priority": "P2",
"matched_keyword": kw,
"reason_ar": f"connector أو Proof Pack: {kw}",
}
return {"priority": "P3", "reason_ar": "افتراضي — سؤال أو تحسين."}
def route_ticket(
*,
text: str,
customer_id: str = "",
contact_email: str = "",
) -> dict[str, Any]:
"""Classify + route a ticket to the right SLA + owner."""
classification = classify_ticket_priority(text)
priority = classification["priority"]
sla = next(
(dict(p) for p in SUPPORT_PRIORITIES if p["id"] == priority),
dict(SUPPORT_PRIORITIES[3]),
)
return {
"customer_id": customer_id,
"contact_email": contact_email,
"priority": priority,
"classification": classification,
"sla": sla,
"first_response_template": build_first_response_template(priority),
"approval_required": True,
"live_send_allowed": False,
}
def build_first_response_template(priority: str) -> dict[str, Any]:
"""Build an Arabic first-response template per priority."""
if priority == "P0":
body = (
"وصلني بلاغك الآن. نتعامل معه كأولوية حرجة. "
"سأرد عليك خلال 30 دقيقة بتفاصيل ما حدث + الإجراءات المتخذة. "
"إذا اكتشفت أي إرسال غير معتمد أو تسريب بيانات، سأتواصل معك مباشرة."
)
elif priority == "P1":
body = (
"وصلني بلاغك. نتعامل معه كأولوية عالية. "
"سأرد بتفاصيل خلال ساعتين كحد أقصى."
)
elif priority == "P2":
body = (
"وصلني سؤالك حول الـ connector / Proof Pack. "
"سأتابع خلال 8 ساعات عمل وأرسل لك حل أو خطوات تالية."
)
else:
body = (
"شاكر لك على ملاحظتك. سأرد عليك خلال 24 ساعة عمل. "
"إذا الأمر عاجل، اكتب 'حرج' في رسالة جديدة وأرفعها للأولوية."
)
return {
"priority": priority,
"body_ar": body,
"approval_required": True,
"live_send_allowed": False,
}
def route_ticket(issue_ar: str) -> dict[str, Any]:
t = (issue_ar or "").lower()
if any(k in t for k in ("أرسل", "إرسال", "send", "live", "خرق", "سر")):
return {"priority": "P0", "queue_ar": "أمان وتشغيل", "sla_first_response_hours": 2}
if any(k in t for k in ("تعطل", "502", "500", "error", "خطأ")):
return {"priority": "P1", "queue_ar": "تشغيل", "sla_first_response_hours": 4}
if any(k in t for k in ("connector", "ربط", "تكامل", "proof", "تقرير")):
return {"priority": "P2", "queue_ar": "تكامل ونجاح عميل", "sla_first_response_hours": 24}
return {"priority": "P3", "queue_ar": "عام", "sla_first_response_hours": 48}

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

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 typing import Any
from .message_curator import detect_duplicates, grade_message
from .mission_curator import score_mission
from .playbook_curator import (
merge_similar_playbooks,
recommend_next_playbook,
score_playbook,
)
def build_weekly_curator_report(
*,
messages: list[dict[str, Any]] | None = None,
playbooks: list[dict[str, Any]] | None = None,
missions: list[dict[str, Any]] | None = None,
sector: str | None = None,
) -> dict[str, Any]:
"""
Build a weekly Arabic curator report.
Inputs are all optional the report degrades gracefully with empty data.
"""
messages = messages or []
playbooks = playbooks or []
missions = missions or []
# 1. Grade messages.
graded_messages: list[dict[str, Any]] = []
for m in messages:
text = str(m.get("text", "") or "")
grade = grade_message(text, sector=sector)
graded_messages.append({
"id": m.get("id"),
"text": text,
"grade": grade.to_dict(),
})
archived_messages = [g for g in graded_messages if g["grade"]["verdict"] == "reject"]
needs_edit = [g for g in graded_messages if g["grade"]["verdict"] == "needs_edit"]
# 2. Detect duplicate messages.
dup_pairs = detect_duplicates([str(m.get("text", "") or "") for m in messages])
# 3. Score playbooks.
scored_playbooks = []
for pb in playbooks:
s = score_playbook(pb)
scored_playbooks.append({**pb, **s})
merge_suggestions = merge_similar_playbooks(playbooks)
# 4. Score missions.
scored_missions = []
for mn in missions:
s = score_mission(mn)
scored_missions.append({**mn, **s})
# 5. Recommend next playbook.
next_pb = recommend_next_playbook(scored_playbooks, sector=sector)
# 6. Build human summary.
summary_ar: list[str] = []
summary_ar.append(
f"تمت مراجعة {len(messages)} رسالة، "
f"{len(playbooks)} playbook، و{len(missions)} مهمة هذا الأسبوع."
)
if archived_messages:
summary_ar.append(
f"تم اقتراح أرشفة {len(archived_messages)} رسالة ضعيفة الجودة."
)
if needs_edit:
summary_ar.append(f"{len(needs_edit)} رسالة تحتاج تعديلاً قبل النشر.")
if dup_pairs:
summary_ar.append(
f"تم اكتشاف {len(dup_pairs)} زوج رسائل متشابهة (للدمج)."
)
if merge_suggestions:
summary_ar.append(
f"تم اقتراح دمج {len(merge_suggestions)} مجموعة من الـ playbooks."
)
next_action_ar = next_pb.get("title_ar", "تواصل دافئ مع 10 جهات مختارة")
def build_weekly_curator_report(context: dict[str, Any] | None = None) -> dict[str, Any]:
ctx = context or {}
return {
"summary_ar": summary_ar,
"messages": {
"total": len(messages),
"publishable": sum(1 for g in graded_messages if g["grade"]["verdict"] == "publish"),
"needs_edit": len(needs_edit),
"to_archive": len(archived_messages),
"duplicate_pairs": len(dup_pairs),
},
"playbooks": {
"total": len(playbooks),
"winners": sum(1 for p in scored_playbooks if p.get("tier") == "winner"),
"promising": sum(1 for p in scored_playbooks if p.get("tier") == "promising"),
"to_merge_groups": len(merge_suggestions),
},
"missions": {
"total": len(missions),
"ship_it_widely": sum(1 for m in scored_missions if m.get("verdict") == "ship_it_widely"),
"iterate": sum(1 for m in scored_missions if m.get("verdict") == "iterate"),
"rework_or_retire": sum(1 for m in scored_missions if m.get("verdict") == "rework_or_retire"),
},
"next_playbook": next_pb,
"recommended_next_action_ar": next_action_ar,
"graded_messages": graded_messages,
"scored_playbooks": scored_playbooks,
"scored_missions": scored_missions,
"merge_suggestions": merge_suggestions,
"week_label_ar": str(ctx.get("week_label_ar") or "أسبوع تجريبي"),
"summary_ar": "تمت مراجعة رسائل المسودات: أرشفة ٣ نسخ ضعيفة، دمج تشابه في عنوانين، تحسين CTA في ٤ رسائل.",
"actions_ar": [
"حافظ على سؤال واحد لكل رسالة واتساب.",
"قلل الوعود المطلقة في البريد.",
"فعّل متابعة ٤٨ ساعة بعد الاجتماع فقط بعد الموافقة.",
],
"demo": True,
}

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
import re
from dataclasses import dataclass, field
from difflib import SequenceMatcher
# Risky/forbidden Arabic phrases — heavy promises, urgency manipulation.
RISKY_PHRASES_AR: tuple[str, ...] = (
"ضمان 100%",
"نتائج مضمونة",
"آخر فرصة",
"العرض ينتهي اليوم",
"خصم محدود جداً",
"لن تجد مثله",
"صفقة العمر",
"اضغط الآن",
)
# Required signals for a "Saudi natural tone" message.
REQUIRED_SIGNALS_AR: tuple[str, ...] = (
# Greeting
"هلا|أهلاً|السلام عليكم|مرحبا|مساء الخير|صباح الخير",
# Reason for contacting
"لاحظت|شفت|رأيت|متابع|قرأت|تابعت|اطلعت",
# Soft CTA
"يناسبك|تحب|ممكن|إذا فيه وقت|تفتح|تجربة|تواصل|نتقابل",
)
from typing import Any
@dataclass(frozen=True)
class MessageGrade:
"""Result of grading a single Arabic message."""
score: int # 0..100
verdict: str # "publish" | "needs_edit" | "reject"
reasons_ar: list[str] = field(default_factory=list)
suggestions_ar: list[str] = field(default_factory=list)
risky_phrases: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, object]:
return {
"score": self.score,
"verdict": self.verdict,
"reasons_ar": self.reasons_ar,
"suggestions_ar": self.suggestions_ar,
"risky_phrases": self.risky_phrases,
}
def _has_arabic(text: str) -> bool:
return any("؀" <= ch <= "ۿ" for ch in text)
def _word_count(text: str) -> int:
return len([w for w in re.split(r"\s+", text.strip()) if w])
def _matches_signal(text: str, alternatives: str) -> bool:
pat = "|".join(re.escape(a) for a in alternatives.split("|"))
return re.search(pat, text) is not None
def grade_message(
message: str,
*,
sector: str | None = None,
channel: str = "whatsapp",
) -> MessageGrade:
"""
Grade a single Arabic message.
Returns MessageGrade with score 0..100 and a verdict.
"""
reasons: list[str] = []
suggestions: list[str] = []
risky: list[str] = [p for p in RISKY_PHRASES_AR if p in message]
score = 100
# 1. Must contain Arabic.
if not _has_arabic(message):
score -= 60
reasons.append("الرسالة لا تحتوي محتوى عربي.")
suggestions.append("أعد صياغة الرسالة بالعربي بأسلوب طبيعي سعودي.")
# 2. Length sanity.
wc = _word_count(message)
if wc < 12:
def grade_message(message_ar: str, *, sector: str = "", channel: str = "whatsapp") -> dict[str, Any]:
text = (message_ar or "").strip()
score = 70
notes: list[str] = []
if len(text) < 40:
score -= 15
reasons.append("الرسالة قصيرة جداً ولا توضح السبب أو القيمة.")
suggestions.append("أضف سبب التواصل + سؤال مفتوح قصير.")
elif wc > 80:
score -= 15
reasons.append("الرسالة طويلة جداً للعرض الأول.")
suggestions.append("اختصر إلى 4-6 أسطر.")
# 3. Risky phrases.
if risky:
score -= 25 * min(len(risky), 2)
reasons.append(f"عبارات عالية المخاطرة: {', '.join(risky)}")
suggestions.append("استبدل العبارات المضللة بأمثلة محددة وأرقام واقعية.")
# 4. Saudi tone signals (greeting + reason + soft CTA).
missing_signals = []
for sig in REQUIRED_SIGNALS_AR:
if not _matches_signal(message, sig):
missing_signals.append(sig.split("|")[0])
if missing_signals:
score -= 8 * len(missing_signals)
reasons.append(
f"تنقصها إشارات أسلوب طبيعي: {', '.join(missing_signals)}"
)
suggestions.append("ابدأ بتحية + لاحظت/شفت + سؤال يناسبك.")
# 5. WhatsApp-specific: avoid bulk markers.
if channel == "whatsapp" and re.search(r"\bعميل عزيز\b|\bلجميع العملاء\b", message):
notes.append("قصير جداً — أضف سياقاً ولماذا الآن.")
if len(text) > 900:
score -= 10
reasons.append("الرسالة بنبرة جماعية لا تناسب واتساب الشخصي.")
suggestions.append("استخدم اسم الشخص أو شركته بدل النداء العام.")
# 6. Sector hook — soft bonus if sector is mentioned.
if sector and sector.lower() in message.lower():
score = min(100, score + 5)
notes.append("طويل — قصّر للواتساب/المتابعة السريعة.")
if "ضمان" in text or "مضمون" in text or "100%" in text:
score -= 20
notes.append("تجنب وعود مطلقة — خطر امتثال.")
if "؟" not in text and "?" not in text:
score -= 5
notes.append("أضف سؤالاً واحداً واضحاً لزيادة الرد.")
if channel == "email" and "السلام" not in text and "عليكم" not in text:
notes.append("افتتح بتحية مهنية للبريد.")
score = max(0, min(100, score))
if score >= 75 and not risky:
verdict = "publish"
elif score >= 50:
verdict = "needs_edit"
else:
verdict = "reject"
return MessageGrade(
score=score, verdict=verdict,
reasons_ar=reasons, suggestions_ar=suggestions,
risky_phrases=risky,
)
def detect_duplicates(messages: list[str], *, threshold: float = 0.85) -> list[tuple[int, int, float]]:
"""
Return pairs (i, j, ratio) of near-duplicate messages.
Uses SequenceMatcher; deterministic, no external deps.
"""
pairs: list[tuple[int, int, float]] = []
n = len(messages)
for i in range(n):
for j in range(i + 1, n):
ratio = SequenceMatcher(None, messages[i], messages[j]).ratio()
if ratio >= threshold:
pairs.append((i, j, round(ratio, 3)))
return pairs
def suggest_improvement(message: str, *, sector: str | None = None) -> dict[str, object]:
"""Return a structured improvement suggestion (deterministic, no LLM)."""
grade = grade_message(message, sector=sector)
template = (
"هلا [الاسم]، لاحظت [إشارة محددة عن شركتك/قطاعك]. "
"أعمل على [وصف العرض في جملة واحدة]. "
"يناسبك أعرض لك مثال خفيف 10 دقائق هذا الأسبوع؟"
)
return {
"current": message,
"grade": grade.to_dict(),
"suggested_skeleton_ar": template,
}
def archive_low_quality(
messages: list[dict[str, object]],
*,
score_field: str = "score",
threshold: int = 50,
) -> dict[str, list[dict[str, object]]]:
"""
Split a list of {message, score} into (active, archived) by threshold.
"""
active: list[dict[str, object]] = []
archived: list[dict[str, object]] = []
for m in messages:
score = int(m.get(score_field, 0) or 0)
if score < threshold:
archived.append(m)
else:
active.append(m)
return {"active": active, "archived": archived}
band = "strong" if score >= 80 else "ok" if score >= 60 else "weak"
return {"score": score, "band": band, "notes_ar": notes, "sector": sector, "channel": channel, "demo": True}

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
def score_mission(mission: dict[str, object]) -> dict[str, object]:
"""
Score a completed mission run.
Inputs:
opportunities_generated, drafts_approved, meetings_booked,
revenue_influenced_sar, time_to_value_minutes, risks_blocked
"""
opps = int(mission.get("opportunities_generated", 0) or 0)
approved = int(mission.get("drafts_approved", 0) or 0)
meetings = int(mission.get("meetings_booked", 0) or 0)
revenue = float(mission.get("revenue_influenced_sar", 0) or 0)
risks_blocked = int(mission.get("risks_blocked", 0) or 0)
ttv = float(mission.get("time_to_value_minutes", 9_999) or 9_999)
score = 0
score += min(20, opps * 2)
score += min(20, approved * 4)
score += min(20, meetings * 5)
score += min(20, int(revenue / 5_000))
score += min(10, risks_blocked * 5)
if ttv <= 10:
score += 10
elif ttv <= 60:
score += 5
score = max(0, min(100, score))
if score >= 70:
verdict = "ship_it_widely"
elif score >= 40:
verdict = "iterate"
else:
verdict = "rework_or_retire"
return {"score": score, "verdict": verdict, "ttv_minutes": ttv}
from typing import Any
def recommend_next_mission(
mission_history: list[dict[str, object]] | None = None,
*,
growth_brain: dict[str, object] | None = None,
) -> dict[str, object]:
"""
Pick the next mission to run given history and brain context.
Defaults to the kill feature `first_10_opportunities` for early-stage
customers (low signal count).
"""
if not mission_history:
return {
"recommended_mission_id": "first_10_opportunities",
"reason_ar": "لا يوجد تاريخ مهمات — نبدأ بالـ Kill Feature.",
}
# If the kill feature has not yet shipped, ship it first.
ran_ids = {m.get("mission_id") for m in mission_history}
if "first_10_opportunities" not in ran_ids:
return {
"recommended_mission_id": "first_10_opportunities",
"reason_ar": "Kill Feature لم يُشغّل بعد — ابدأ به.",
}
# Otherwise, pick the next mission by sector/priority.
priorities = []
if growth_brain:
priorities = list(growth_brain.get("growth_priorities", []) or [])
if "fill_pipeline" in priorities:
return {
"recommended_mission_id": "meeting_booking_sprint",
"reason_ar": "الأولوية ملء الـ pipeline — سبرنت حجز الاجتماعات.",
}
if "rescue_lost_revenue" in priorities:
return {
"recommended_mission_id": "revenue_leak_rescue",
"reason_ar": "الأولوية استرجاع الإيراد — تشغيل ميشن التسريب.",
}
if "expand_partners" in priorities:
return {
"recommended_mission_id": "partnership_sprint",
"reason_ar": "الأولوية توسيع الشركاء — ميشن الشراكات.",
}
# Default deterministic next.
def curate_missions_weekly() -> dict[str, Any]:
return {
"recommended_mission_id": "customer_reactivation",
"reason_ar": "الافتراضي: إعادة تنشيط العملاء الخاملين.",
"merged_pairs_ar": ["book_three_meetings + followup_sequence → دمج عنوان الخطوات"],
"archived_ids": ["deprecated_template_v1"],
"next_week_focus_ar": "زيادة Pilot 7 أيام في قطاع التدريب",
"demo": True,
}
def score_mission_popularity(mission_id: str) -> dict[str, Any]:
return {"mission_id": mission_id, "popularity_score": 81 if "10" in mission_id else 55, "demo": True}

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 difflib import SequenceMatcher
from typing import Any
def score_playbook(playbook: dict[str, object]) -> dict[str, object]:
"""
Score a playbook on outcome quality.
Inputs (all optional, defaults are conservative):
used_count, accept_count, replied_count, meeting_count, deal_count
"""
used = int(playbook.get("used_count", 0) or 0)
accepted = int(playbook.get("accept_count", 0) or 0)
replied = int(playbook.get("replied_count", 0) or 0)
meetings = int(playbook.get("meeting_count", 0) or 0)
deals = int(playbook.get("deal_count", 0) or 0)
if used <= 0:
return {
"score": 0, "tier": "unproven",
"accept_rate": 0.0, "reply_rate": 0.0,
"meeting_rate": 0.0, "deal_rate": 0.0,
}
accept_rate = accepted / used if used else 0.0
reply_rate = replied / used if used else 0.0
meeting_rate = meetings / used if used else 0.0
deal_rate = deals / used if used else 0.0
# Weighted score; deals matter most.
score = int(round(
100 * (
0.10 * accept_rate
+ 0.20 * reply_rate
+ 0.30 * meeting_rate
+ 0.40 * deal_rate
)
))
score = max(0, min(100, score))
if score >= 70:
tier = "winner"
elif score >= 40:
tier = "promising"
elif score >= 20:
tier = "needs_work"
else:
tier = "candidate_archive"
return {
"score": score, "tier": tier,
"accept_rate": round(accept_rate, 3),
"reply_rate": round(reply_rate, 3),
"meeting_rate": round(meeting_rate, 3),
"deal_rate": round(deal_rate, 3),
}
def merge_similar_playbooks(
playbooks: list[dict[str, object]],
*,
field: str = "title",
threshold: float = 0.80,
) -> list[dict[str, object]]:
"""
Group near-identical playbooks (by title similarity) and return
a list of merge suggestions:
[{"keep_index", "merge_indices", "merged_title", "similarity"}]
"""
suggestions: list[dict[str, object]] = []
used: set[int] = set()
n = len(playbooks)
for i in range(n):
if i in used:
continue
merge_indices: list[int] = []
title_i = str(playbooks[i].get(field, "") or "")
for j in range(i + 1, n):
if j in used:
continue
title_j = str(playbooks[j].get(field, "") or "")
if not title_i or not title_j:
continue
ratio = SequenceMatcher(None, title_i, title_j).ratio()
if ratio >= threshold:
merge_indices.append(j)
used.add(j)
if merge_indices:
used.add(i)
suggestions.append({
"keep_index": i,
"merge_indices": merge_indices,
"merged_title": title_i,
"similarity_threshold": threshold,
})
return suggestions
def recommend_next_playbook(
scored_playbooks: list[dict[str, object]],
*,
sector: str | None = None,
) -> dict[str, object]:
"""
Pick the next playbook to run given scored history.
Strategy: prefer "promising" over "winner" (winners are saturated).
If sector is given, prefer playbooks tagged with that sector.
Falls back to deterministic default.
"""
if not scored_playbooks:
return {
"recommended_id": "default_warm_outreach",
"title_ar": "تواصل دافئ مع 10 جهات مختارة",
"reason_ar": "لا يوجد تاريخ بعد — ابدأ بالـ playbook الافتراضي.",
}
candidates = list(scored_playbooks)
if sector:
sector_filtered = [
p for p in candidates
if sector.lower() in str(p.get("sectors", "")).lower()
]
if sector_filtered:
candidates = sector_filtered
# Promote "promising" first, then "winner", then by score.
tier_priority = {"promising": 0, "winner": 1, "needs_work": 2,
"candidate_archive": 3, "unproven": 4}
candidates.sort(key=lambda p: (
tier_priority.get(str(p.get("tier", "unproven")), 9),
-int(p.get("score", 0) or 0),
))
chosen = candidates[0]
return {
"recommended_id": chosen.get("id"),
"title_ar": chosen.get("title", "?"),
"reason_ar": (
f"الـ tier: {chosen.get('tier')}, الـ score: {chosen.get('score')}."
),
}
def suggest_playbook_merge(playbooks: list[dict[str, Any]]) -> dict[str, Any]:
"""If two titles share same first word, suggest merge (demo)."""
if len(playbooks) < 2:
return {"merge_groups": [], "demo": True}
titles = [str(p.get("title_ar") or p.get("title") or "") for p in playbooks]
merge_groups: list[list[int]] = []
for i, a in enumerate(titles):
for j in range(i + 1, len(titles)):
if a and titles[j] and a.split()[:1] == titles[j].split()[:1] and a.split()[:1]:
merge_groups.append([i, j])
return {"merge_groups": merge_groups[:3], "demo": True}

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
# Curated, deterministic inventory of skills across the layers.
SKILL_INVENTORY: tuple[dict[str, object], ...] = (
# platform_services
{"id": "tool_gateway", "layer": "platform_services",
"label_ar": "بوابة الأدوات الآمنة", "tier": "core"},
{"id": "action_policy", "layer": "platform_services",
"label_ar": "محرك سياسة الأفعال", "tier": "core"},
{"id": "channel_registry", "layer": "platform_services",
"label_ar": "سجل القنوات", "tier": "core"},
{"id": "unified_inbox", "layer": "platform_services",
"label_ar": "صندوق البريد الموحد", "tier": "core"},
{"id": "action_ledger", "layer": "platform_services",
"label_ar": "سجل الأفعال", "tier": "core"},
{"id": "proof_ledger", "layer": "platform_services",
"label_ar": "سجل الأثر", "tier": "core"},
{"id": "service_catalog", "layer": "platform_services",
"label_ar": "كتالوج الخدمات", "tier": "core"},
{"id": "identity_resolution", "layer": "platform_services",
"label_ar": "حل الهوية المتقاطع", "tier": "core"},
# intelligence_layer
{"id": "growth_brain", "layer": "intelligence_layer",
"label_ar": "عقل النمو", "tier": "core"},
{"id": "command_feed", "layer": "intelligence_layer",
"label_ar": "بطاقات القرار اليومية", "tier": "core"},
{"id": "mission_engine", "layer": "intelligence_layer",
"label_ar": "محرك المهمات", "tier": "core"},
{"id": "trust_score", "layer": "intelligence_layer",
"label_ar": "Trust Score", "tier": "core"},
{"id": "revenue_dna", "layer": "intelligence_layer",
"label_ar": "DNA الإيرادات", "tier": "core"},
{"id": "opportunity_simulator", "layer": "intelligence_layer",
"label_ar": "محاكي الفرص", "tier": "core"},
{"id": "competitive_moves", "layer": "intelligence_layer",
"label_ar": "كاشف حركات المنافسين", "tier": "core"},
{"id": "board_brief", "layer": "intelligence_layer",
"label_ar": "موجز Founder Shadow Board", "tier": "core"},
{"id": "decision_memory", "layer": "intelligence_layer",
"label_ar": "ذاكرة القرارات", "tier": "core"},
{"id": "action_graph", "layer": "intelligence_layer",
"label_ar": "Action Graph", "tier": "core"},
# growth_operator (existing)
{"id": "first_10_opportunities", "layer": "growth_operator",
"label_ar": "10 فرص في 10 دقائق", "tier": "kill_feature"},
# security_curator
{"id": "secret_redactor", "layer": "security_curator",
"label_ar": "إخفاء الأسرار", "tier": "core"},
{"id": "patch_firewall", "layer": "security_curator",
"label_ar": "جدار الـ patches", "tier": "core"},
# growth_curator
{"id": "message_curator", "layer": "growth_curator",
"label_ar": "مدقق الرسائل", "tier": "core"},
{"id": "playbook_curator", "layer": "growth_curator",
"label_ar": "مدقق الـ playbooks", "tier": "core"},
)
from typing import Any
def inventory_skills() -> dict[str, object]:
"""Return the full skill inventory grouped by layer."""
by_layer: dict[str, list[dict[str, object]]] = {}
for s in SKILL_INVENTORY:
layer = str(s["layer"])
by_layer.setdefault(layer, []).append(dict(s))
return {
"total": len(SKILL_INVENTORY),
"layers": sorted(by_layer.keys()),
"by_layer": by_layer,
"kill_features": [
dict(s) for s in SKILL_INVENTORY if s.get("tier") == "kill_feature"
],
}
def list_skill_inventory() -> dict[str, Any]:
skills: list[dict[str, Any]] = [
{"id": "saudi_short_pitch", "score": 88, "usage_count_demo": 42, "status": "active"},
{"id": "objection_timing", "score": 72, "usage_count_demo": 18, "status": "active"},
{"id": "cold_whatsapp_template", "score": 12, "usage_count_demo": 3, "status": "archived", "reason_ar": "مخالف سياسة القناة"},
]
return {"skills": skills, "recommendation_ar": "أرشف القوالب منخفضة الدرجة وادمج المتشابه.", "demo": True}
def score_skill(skill_id: str) -> dict[str, Any]:
inv = list_skill_inventory()
for s in inv["skills"]:
if s["id"] == skill_id:
return {"skill": s, "demo": True}
return {"error": "not_found", "demo": True}

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 the decision brain on top of platform_services.
"""Intelligence layer — deterministic JSON, optional bridge to innovation."""
Turns Dealix from "channels + actions" into a **Growth Neural Network**:
the system understands the customer fully, watches the market, decides,
executes (with approval), and learns from every outcome.
Modules:
- growth_brain : per-customer brain (context + preferences + priorities)
- command_feed : Arabic decision cards (what to do now)
- action_graph : 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.action_graph import build_action_graph_trace
from auto_client_acquisition.intelligence_layer.board_brief import build_board_brief
from auto_client_acquisition.intelligence_layer.command_feed import (
INTEL_CARD_TYPES,
build_command_feed_demo,
)
from auto_client_acquisition.intelligence_layer.competitive_moves import (
analyze_competitive_move,
)
from auto_client_acquisition.intelligence_layer.decision_memory import (
DecisionMemory,
learn_from_decision,
)
from auto_client_acquisition.intelligence_layer.growth_brain import (
GrowthBrain,
build_growth_brain,
)
from auto_client_acquisition.intelligence_layer.mission_engine import (
INTEL_MISSIONS,
list_intel_missions,
recommend_missions,
)
from auto_client_acquisition.intelligence_layer.opportunity_simulator import (
simulate_opportunity,
)
from auto_client_acquisition.intelligence_layer.revenue_dna import (
build_revenue_dna_demo,
extract_revenue_dna,
)
from auto_client_acquisition.intelligence_layer.competitive_moves import build_competitive_moves
from auto_client_acquisition.intelligence_layer.decision_memory import list_decisions, record_decision
from auto_client_acquisition.intelligence_layer.growth_brain import build_growth_profile
from auto_client_acquisition.intelligence_layer.intel_command_feed import build_intel_command_feed
from auto_client_acquisition.intelligence_layer.mission_engine import get_mission, list_mission_catalog
from auto_client_acquisition.intelligence_layer.opportunity_simulator import simulate_opportunities
from auto_client_acquisition.intelligence_layer.revenue_dna import build_revenue_dna
from auto_client_acquisition.intelligence_layer.trust_score import compute_trust_score
__all__ = [
"GrowthBrain", "build_growth_brain",
"INTEL_CARD_TYPES", "build_command_feed_demo",
"ActionGraph", "ActionEdge", "EDGE_TYPES",
"INTEL_MISSIONS", "list_intel_missions", "recommend_missions",
"DecisionMemory", "learn_from_decision",
"compute_trust_score",
"extract_revenue_dna", "build_revenue_dna_demo",
"simulate_opportunity",
"analyze_competitive_move",
"build_action_graph_trace",
"build_board_brief",
"build_competitive_moves",
"build_growth_profile",
"build_intel_command_feed",
"build_revenue_dna",
"compute_trust_score",
"get_mission",
"list_decisions",
"list_mission_catalog",
"record_decision",
"simulate_opportunities",
]

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

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

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

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 collections import Counter
from dataclasses import dataclass, field
from typing import Any
VALID_DECISIONS: tuple[str, ...] = ("accept", "skip", "edit", "block")
_STORE: list[dict[str, Any]] = []
@dataclass
class DecisionMemory:
"""Per-customer Accept/Skip/Edit history and aggregates."""
customer_id: str
raw_decisions: list[dict[str, Any]] = field(default_factory=list)
def append(
self,
*,
decision: str,
action_type: str,
channel: str,
sector: str | None = None,
tone: str | None = None,
objection_id: str | None = None,
) -> None:
if decision not in VALID_DECISIONS:
raise ValueError(f"unknown decision: {decision}")
self.raw_decisions.append({
"decision": decision,
"action_type": action_type,
"channel": channel,
"sector": sector,
"tone": tone,
"objection_id": objection_id,
})
def preferences(self) -> dict[str, Any]:
if not self.raw_decisions:
return {
"samples": 0,
"preferred_channels": [],
"preferred_tones": [],
"preferred_sectors": [],
"rejected_action_types": [],
"accept_rate": 0.0,
}
ch_counter: Counter[str] = Counter()
tone_counter: Counter[str] = Counter()
sector_counter: Counter[str] = Counter()
rejected: Counter[str] = Counter()
accepts = 0
for d in self.raw_decisions:
if d["decision"] == "accept":
accepts += 1
ch_counter[d.get("channel", "")] += 1
if d.get("tone"):
tone_counter[d["tone"]] += 1
if d.get("sector"):
sector_counter[d["sector"]] += 1
elif d["decision"] in ("skip", "block"):
rejected[d.get("action_type", "")] += 1
return {
"samples": len(self.raw_decisions),
"preferred_channels": [c for c, _ in ch_counter.most_common(3)],
"preferred_tones": [t for t, _ in tone_counter.most_common(2)],
"preferred_sectors": [s for s, _ in sector_counter.most_common(3)],
"rejected_action_types": [a for a, _ in rejected.most_common(3) if a],
"accept_rate": round(accepts / len(self.raw_decisions), 4),
}
def learn_from_decision(
*,
memory: DecisionMemory,
decision: str,
action_type: str,
channel: str,
sector: str | None = None,
tone: str | None = None,
objection_id: str | None = None,
) -> dict[str, Any]:
"""Record a decision + return updated preferences."""
memory.append(
decision=decision, action_type=action_type, channel=channel,
sector=sector, tone=tone, objection_id=objection_id,
)
return {
"customer_id": memory.customer_id,
"added": True,
"preferences": memory.preferences(),
def record_decision(entry: dict[str, Any]) -> dict[str, Any]:
e = {
"id": f"dec_{len(_STORE)+1}",
**entry,
}
_STORE.append(e)
return {"ok": True, "entry": e, "demo": True}
def list_decisions(*, limit: int = 20) -> dict[str, Any]:
return {"decisions": list(reversed(_STORE[-limit:])), "count": len(_STORE), "demo": True}
def reset_demo_memory() -> None:
_STORE.clear()

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

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

View File

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

View File

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

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

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:
- private_beta: today's offer, gates, FAQ
- demo_flow: 12-min demo script consolidator
- outreach_messages: first-20 plan + per-segment messages
- go_no_go: deterministic launch readiness gate
- launch_scorecard: daily ops metrics
"""
from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer
from __future__ import annotations
from .demo_flow import (
build_12_min_demo_flow,
build_close_script,
build_discovery_questions,
build_objection_responses,
)
from .go_no_go import build_launch_readiness, decide_go_no_go
from .launch_scorecard import (
build_daily_launch_scorecard,
build_weekly_launch_scorecard,
record_launch_event,
)
from .outreach_messages import (
build_first_20_segments,
build_followup_message,
build_outreach_message,
build_reply_handlers,
)
from .private_beta import (
PRIVATE_BETA_OFFER,
build_private_beta_offer,
build_private_beta_safety_notes,
private_beta_faq,
)
__all__ = [
# private_beta
"PRIVATE_BETA_OFFER",
"build_private_beta_offer",
"build_private_beta_safety_notes",
"private_beta_faq",
# demo_flow
"build_12_min_demo_flow",
"build_close_script",
"build_discovery_questions",
"build_objection_responses",
# outreach_messages
"build_first_20_segments",
"build_followup_message",
"build_outreach_message",
"build_reply_handlers",
# go_no_go
"build_launch_readiness",
"decide_go_no_go",
# launch_scorecard
"build_daily_launch_scorecard",
"build_weekly_launch_scorecard",
"record_launch_event",
]
__all__ = ["build_private_beta_offer"]

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 typing import Any
def build_12_min_demo_flow() -> dict[str, Any]:
"""The canonical 12-minute Arabic demo plan."""
def build_demo_script() -> dict[str, Any]:
return {
"duration_minutes": 12,
"minute_by_minute_ar": [
"02: الفكرة الكبرى — Dealix ليس CRM ولا أداة واتساب.",
"24: Daily Brief / Command Feed — 3 قرارات + 3 فرص + 3 مخاطر.",
"46: 10 فرص في 10 دقائق — مثال حي.",
"68: Trust Score + Simulator + Approval Card.",
"810: الأمان والتكاملات — security_curator + connector_catalog.",
"1012: العرض والـ CTA — Pilot 7 أيام / 499 ريال.",
],
"demo_endpoints": [
"/api/v1/personal-operator/daily-brief",
"/api/v1/intelligence/command-feed/demo",
"/api/v1/intelligence/missions",
"/api/v1/targeting/free-diagnostic",
"/api/v1/services/catalog",
"/api/v1/launch/private-beta/offer",
],
"do_not_do_in_demo_ar": [
"لا تكشف API keys على الشاشة.",
"لا تشغّل live WhatsApp أو Gmail send.",
"لا تعد بأرقام لم تُحقَّق.",
"sections": [
{
"minute_range": "0-2",
"title_ar": "المشكلة والوعد",
"talking_points_ar": [
"Dealix ليس CRM ولا بوت واتساب فقط.",
"نحوّل الإشارات إلى قرار يومي عربي + موافقة + Proof.",
],
},
{
"minute_range": "2-4",
"title_ar": "Daily Brief",
"api_hint": "GET /api/v1/personal-operator/daily-brief",
"talking_points_ar": ["٣ قرارات", "مخاطر", "جاهزية"],
},
{
"minute_range": "4-6",
"title_ar": "Growth Operator / ١٠ فرص",
"api_hint": "GET /api/v1/growth-operator/missions",
"talking_points_ar": ["لماذا الآن", "Accept/Skip", "لا cold WhatsApp"],
},
{
"minute_range": "6-8",
"title_ar": "Inbox ومنصة",
"api_hint": "GET /api/v1/platform/inbox/feed",
"talking_points_ar": ["كروت عربية", "موافقة قبل الإرسال"],
},
{
"minute_range": "8-10",
"title_ar": "برج الخدمات",
"api_hint": "GET /api/v1/services/catalog",
"talking_points_ar": ["تشخيص", "قوائم", "Growth OS", "أسعار تقديرية"],
},
{
"minute_range": "10-12",
"title_ar": "Pilot وProof",
"talking_points_ar": ["٧ أيام أو ٣٠ يوم", "Proof Pack", "الخطوة التالية"],
},
],
}
def build_discovery_questions() -> list[dict[str, str]]:
"""5 discovery questions to ask in the demo's first 4 minutes."""
return [
{"key": "challenge",
"q_ar": "وش أكبر تحدي نمو لديكم اليوم؟"},
{"key": "current_targeting",
"q_ar": "كيف تستهدفون اليوم؟ ما الذي يعمل؟ ما الذي لا يعمل؟"},
{"key": "time_drain",
"q_ar": "ما الذي يأخذ وقتاً يومياً ولا يثبت قيمة؟"},
{"key": "old_list",
"q_ar": "هل عندكم قائمة عملاء قدامى لم تتم متابعتهم؟"},
{"key": "approval_owner",
"q_ar": "من يوافق على الرسائل قبل الإرسال؟"},
]
def build_objection_responses() -> dict[str, str]:
"""Standard Arabic objection-handling responses."""
return {
"price": (
"نقدم Free Diagnostic أولاً — تشوفون عينة قبل الدفع. "
"Pilot 499 ريال أرخص من ساعة عمل في وكالة."
),
"timing": (
"Pilot 7 أيام لا يحتاج التزام طويل. "
"نسلّم خلال أسبوع، تقررون بعدها."
),
"trust": (
"Approval-first: لا نرسل أي شيء بدون موافقتكم. "
"Audit ledger يسجل كل فعل."
),
"complexity": (
"Pilot لا يحتاج تكاملات. "
"نستلم intake في 30 دقيقة ونسلم خلال 24 ساعة."
),
"data_privacy": (
"PDPL-aware من اليوم الأول. "
"DPA draft جاهز للتوقيع. "
"بياناتكم تُخزّن في Supabase KSA-region حسب الإمكان."
),
"results_uncertainty": (
"لا نضمن أرقاماً، نضمن طريقة تشغيل + Proof Pack مفصّل. "
"إذا ما اقتنعتم بعد 7 أيام، تأخذون Proof Pack مجاناً وتمشون."
),
}
def build_close_script() -> dict[str, Any]:
"""The closing script — used in minute 11-12 of the demo."""
return {
"close_sequence_ar": [
"هل الفكرة منطقية؟",
"هل عندك أسئلة محددة قبل ما نبدأ؟",
"أحدد لكم Pilot يبدأ يوم الأحد القادم — موافق؟",
"أرسل لكم intake form + invoice خلال ساعة من نهاية المكالمة.",
],
"close_template_ar": (
"تمام، نبدأ Pilot 7 أيام بـ499 ريال. "
"أرسل لك خلال ساعة:\n"
"1. نموذج intake.\n"
"2. Moyasar invoice.\n"
"3. تأكيد موعد الكيك-أوف.\n\n"
"بعد الدفع، Pilot يبدأ يوم الأحد."
),
"if_hesitant_ar": (
"إذا تحبون عينة قبل الالتزام، أرسل لكم Free Growth Diagnostic "
"خلال 24 ساعة — 3 فرص + رسالة + توصية، بدون التزام."
),
"closing_line_ar": "لا نعد نتائج مضمونة — نعد مسودات وموافقات وتقارير قياس.",
"demo": True,
}

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 typing import Any
# All 10 gates Dealix Launch Control Room checks before approving sale.
LAUNCH_GATES: tuple[dict[str, str], ...] = (
{"id": "tests_passed", "label_ar": "اختبارات pytest خضراء"},
{"id": "routes_check", "label_ar": "scripts/print_routes.py لا يكشف تكرار"},
{"id": "no_secrets", "label_ar": "scan الأسرار نظيف"},
{"id": "staging_health", "label_ar": "/health على staging يرجع 200"},
{"id": "supabase_staging", "label_ar": "Supabase staging مهيأ"},
{"id": "service_catalog", "label_ar": "/services/catalog يعمل ويعرض ≥4 خدمات"},
{"id": "private_beta_page", "label_ar": "landing/private-beta.html جاهزة"},
{"id": "first_20_ready", "label_ar": "أول 20 prospect معرّفون"},
{"id": "live_sends_disabled", "label_ar": "WHATSAPP/GMAIL/CALENDAR/MOYASAR live=false"},
{"id": "payment_manual_ready", "label_ar": "Moyasar invoice/payment link جاهز يدوياً"},
)
def build_launch_readiness(
*, statuses: dict[str, bool] | None = None,
) -> dict[str, Any]:
"""
Build the launch-readiness checklist with current statuses.
Pass `statuses` as a dict of gate_id bool. Unknown gates default to False.
"""
statuses = statuses or {}
items: list[dict[str, Any]] = []
passed = 0
blockers: list[str] = []
for gate in LAUNCH_GATES:
ok = bool(statuses.get(gate["id"], False))
items.append({
**gate,
"passed": ok,
"status": "" if ok else "🔴",
})
if ok:
passed += 1
else:
blockers.append(gate["label_ar"])
total = len(LAUNCH_GATES)
pct = round(100.0 * passed / total, 1) if total else 0.0
return {
"total_gates": total,
"passed_gates": passed,
"passed_pct": pct,
"items": items,
"blockers_ar": blockers,
"ready_threshold_min_pct": 70.0,
def evaluate_go_no_go(flags: dict[str, Any] | None = None) -> dict[str, Any]:
"""flags: optional overrides for tests (e.g. tests_pass=False)."""
f = flags or {}
checks = {
"tests_pass": bool(f.get("tests_pass", True)),
"routes_ok": bool(f.get("routes_ok", True)),
"staging_health_ok": bool(f.get("staging_health_ok", False)),
"no_secrets_in_repo_scan": bool(f.get("no_secrets_in_repo_scan", True)),
"whatsapp_live_send_disabled": bool(f.get("whatsapp_live_send_disabled", True)),
"service_catalog_ok": bool(f.get("service_catalog_ok", True)),
"landing_ready": bool(f.get("landing_ready", True)),
}
def decide_go_no_go(
*, statuses: dict[str, bool] | None = None,
) -> dict[str, Any]:
"""
Decide whether Dealix can sell today.
Rules:
- All "critical" gates must pass: no_secrets, live_sends_disabled, staging_health.
- At least 7 of 10 gates must pass overall.
"""
readiness = build_launch_readiness(statuses=statuses)
passed_pct = readiness["passed_pct"]
items = {it["id"]: it for it in readiness["items"]}
critical = ("no_secrets", "live_sends_disabled", "staging_health")
critical_failed = [c for c in critical if not items.get(c, {}).get("passed")]
if critical_failed:
verdict = "no_go"
reason_ar = (
f"بوابات حرجة فشلت: {', '.join(critical_failed)}. "
"لا تبيع اليوم."
)
elif passed_pct >= 70:
verdict = "go"
reason_ar = (
f"الجاهزية {passed_pct}%. "
"ابدأ Private Beta — لا Public Launch."
)
else:
verdict = "fix_then_go"
reason_ar = (
f"الجاهزية {passed_pct}% — أقل من 70%. "
"ابدأ بإصلاح: " + ", ".join(readiness["blockers_ar"][:3])
)
critical = [
"tests_pass",
"routes_ok",
"no_secrets_in_repo_scan",
"whatsapp_live_send_disabled",
"service_catalog_ok",
]
blockers = [k for k in critical if not checks[k]]
if not checks["landing_ready"]:
blockers.append("landing_ready")
go = len(blockers) == 0
warnings_ar: list[str] = []
if not checks["staging_health_ok"]:
warnings_ar.append("Staging غير مؤكد — يُنصح بتشغيل /health على بيئة staging قبل أول عميل.")
return {
"verdict": verdict,
"reason_ar": reason_ar,
"readiness": readiness,
"next_actions_ar": _next_actions(readiness),
"go": go,
"checks": checks,
"blockers": blockers,
"warnings_ar": warnings_ar,
"verdict_ar": "جاهز للبيتا الخاصة (كود وعمليات أساسية)" if go else "موقوف — راجع قائمة الـ blockers.",
"demo": True,
}
def _next_actions(readiness: dict[str, Any]) -> list[str]:
"""Build concrete next-actions for any failing gates."""
by_id = {it["id"]: it for it in readiness["items"]}
actions: list[str] = []
if not by_id["tests_passed"]["passed"]:
actions.append("شغّل: pytest -q")
if not by_id["routes_check"]["passed"]:
actions.append("شغّل: python scripts/print_routes.py")
if not by_id["no_secrets"]["passed"]:
actions.append("شغّل grep scan + ألغِ أي مفتاح ظهر.")
if not by_id["staging_health"]["passed"]:
actions.append("انشر على Railway: railway up + curl /health.")
if not by_id["supabase_staging"]["passed"]:
actions.append("شغّل: supabase db push --dry-run ثم db push.")
if not by_id["service_catalog"]["passed"]:
actions.append("افحص: curl /api/v1/services/catalog.")
if not by_id["private_beta_page"]["passed"]:
actions.append("افتح landing/private-beta.html وتحقق من CTA.")
if not by_id["first_20_ready"]["passed"]:
actions.append("جهز Sheet 'Dealix First 20 Pipeline' بالعمدة.")
if not by_id["live_sends_disabled"]["passed"]:
actions.append(
"تأكد: WHATSAPP_ALLOW_LIVE_SEND=false (وما يماثلها)."
)
if not by_id["payment_manual_ready"]["passed"]:
actions.append("افتح Moyasar dashboard وجهّز invoice template.")
return actions

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 collections import defaultdict
from typing import Any
# Valid event types the launch scorecard accepts.
VALID_LAUNCH_EVENTS: tuple[str, ...] = (
"outreach_sent",
"reply_received",
"demo_booked",
"demo_held",
"diagnostic_delivered",
"pilot_offered",
"pilot_paid",
"pilot_committed",
"pilot_lost",
"case_study_published",
"blocked_action",
)
# Daily targets per the launch plan.
DAILY_TARGETS: dict[str, int] = {
"outreach_sent": 20,
"reply_received": 5,
"demo_booked": 3,
"pilot_paid": 1,
}
# Weekly targets (7-day plan).
WEEKLY_TARGETS: dict[str, int] = {
"outreach_sent": 100,
"reply_received": 20,
"demo_booked": 10,
"pilot_paid": 2,
}
from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go
def record_launch_event(
*,
event_type: str,
customer_id: str | None = None,
notes: str | None = None,
event_log: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""
Record a launch event into an in-memory log.
Returns the appended entry (validated). Raises ValueError on unknown type.
"""
if event_type not in VALID_LAUNCH_EVENTS:
raise ValueError(
f"Unknown launch event: {event_type}. "
f"Valid: {', '.join(VALID_LAUNCH_EVENTS)}"
)
entry: dict[str, Any] = {
"event_type": event_type,
"customer_id": customer_id,
"notes": (notes or "")[:300],
}
if event_log is not None:
event_log.append(entry)
return entry
def _aggregate(events: list[dict[str, Any]]) -> dict[str, int]:
counts: dict[str, int] = defaultdict(int)
for e in events or []:
et = str(e.get("event_type", ""))
counts[et] += 1
return dict(counts)
def build_daily_launch_scorecard(
*, events: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build today's Arabic launch scorecard from event log."""
counts = _aggregate(events or [])
metrics = {k: counts.get(k, 0) for k in VALID_LAUNCH_EVENTS}
progress: dict[str, dict[str, int | float]] = {}
for k, target in DAILY_TARGETS.items():
actual = metrics.get(k, 0)
pct = round(100 * actual / target, 1) if target else 0.0
progress[k] = {"actual": actual, "target": target, "pct": pct}
summary_lines = [
f"تواصل اليوم: {metrics['outreach_sent']} / {DAILY_TARGETS['outreach_sent']}",
f"ردود: {metrics['reply_received']} / {DAILY_TARGETS['reply_received']}",
f"ديموهات: {metrics['demo_booked']} / {DAILY_TARGETS['demo_booked']}",
f"Pilots مدفوعة: {metrics['pilot_paid']} / {DAILY_TARGETS['pilot_paid']}",
f"مخاطر منعت: {metrics.get('blocked_action', 0)}",
]
def build_launch_scorecard(extra: dict[str, Any] | None = None) -> dict[str, Any]:
ex = extra or {}
g = evaluate_go_no_go(ex)
score = 100
if not g["checks"].get("tests_pass"):
score -= 30
if not g["checks"].get("routes_ok"):
score -= 15
if not g["checks"].get("staging_health_ok"):
score -= 10
if not g["checks"].get("no_secrets_in_repo_scan"):
score -= 40
if not g["checks"].get("whatsapp_live_send_disabled"):
score -= 25
if not g["checks"].get("service_catalog_ok"):
score -= 10
if not g["checks"].get("landing_ready"):
score -= 5
score = max(0, min(100, score))
status = "ready" if score >= 75 and g["go"] else "needs_work"
return {
"metrics": metrics,
"targets": DAILY_TARGETS,
"progress": progress,
"summary_ar": summary_lines,
}
def build_weekly_launch_scorecard(
*, events: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build the 7-day Arabic launch scorecard."""
counts = _aggregate(events or [])
metrics = {k: counts.get(k, 0) for k in VALID_LAUNCH_EVENTS}
progress = {}
for k, target in WEEKLY_TARGETS.items():
actual = metrics.get(k, 0)
pct = round(100 * actual / target, 1) if target else 0.0
progress[k] = {"actual": actual, "target": target, "pct": pct}
summary_lines = [
f"تواصل الأسبوع: {metrics['outreach_sent']} / {WEEKLY_TARGETS['outreach_sent']}",
f"ردود: {metrics['reply_received']} / {WEEKLY_TARGETS['reply_received']}",
f"ديموهات منعقدة: {metrics.get('demo_held', 0)}",
f"Pilots مدفوعة: {metrics['pilot_paid']} / {WEEKLY_TARGETS['pilot_paid']}",
f"Pilots commitments: {metrics.get('pilot_committed', 0)}",
f"Pilots خسرت: {metrics.get('pilot_lost', 0)}",
f"مخاطر منعت: {metrics.get('blocked_action', 0)}",
]
if metrics["pilot_paid"] >= WEEKLY_TARGETS["pilot_paid"]:
verdict = "on_track"
elif metrics["demo_booked"] >= 5:
verdict = "promising"
else:
verdict = "needs_focus"
return {
"metrics": metrics,
"targets": WEEKLY_TARGETS,
"progress": progress,
"summary_ar": summary_lines,
"verdict": verdict,
"readiness_score": score,
"status": status,
"go_no_go": g,
"summary_ar": f"درجة الجاهزية {score}/١٠٠{status}.",
"demo": True,
}

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 typing import Any
def build_first_20_segments() -> dict[str, Any]:
"""The deterministic first-20 plan — 4 segments × 5 prospects each."""
def build_first_twenty_outreach() -> dict[str, Any]:
return {
"total_targets": 20,
"segments": [
"count": 20,
"disclaimer_ar": "هذه قوالب للنسخ اليدوي — لا يُرسل من Dealix تلقائياً.",
"messages": [
{
"id": "agency_b2b",
"label_ar": "وكالات تسويق B2B",
"count": 5,
"best_offer_id": "agency_partner_program",
"fallback_offer_id": "partner_sprint",
"primary_channel": "email",
"id": 1,
"audience_ar": "مؤسس B2B",
"subject_ar": "تجربة بيتا — ١٠ فرص خلال أسبوع",
"body_ar": (
"هلا [الاسم]، نجرب Dealix كمدير نمو عربي: نطلع لك فرصاً مناسبة، "
"نكتب رسائل عربية، وأنت توافق قبل أي تواصل. مهتم نعرض لك عينة قصيرة؟"
),
},
{
"id": "training_consulting",
"label_ar": "شركات تدريب واستشارات",
"count": 5,
"best_offer_id": "first_10_opportunities_sprint",
"fallback_offer_id": "free_growth_diagnostic",
"primary_channel": "email",
"id": 2,
"audience_ar": "وكالة تسويق",
"subject_ar": "Pilot مشترك مع وكالة",
"body_ar": (
"هلا [الاسم]، نبحث وكالة واحدة لتجربة Dealix على عميل حقيقي: "
"فرص + مسودات + Proof Pack. يناسبكم ديمو ١٥ دقيقة؟"
),
},
{
"id": "saas_tech_small",
"label_ar": "SaaS / تقنية صغيرة",
"count": 5,
"best_offer_id": "first_10_opportunities_sprint",
"fallback_offer_id": "growth_os_monthly",
"primary_channel": "linkedin_lead_form",
"id": 3,
"audience_ar": "شركة تدريب",
"subject_ar": "فرص شركات في قطاعكم",
"body_ar": (
"هلا [الاسم]، Dealix يساعد فرق التدريب تطلع فرص B2B مع سبب «لماذا الآن» "
"ومسودات عربية. نقدر نرسل لكم تشخيصاً مجانياً مختصراً؟"
),
},
{
"id": "services_with_whatsapp",
"label_ar": "شركات خدمات لديها واتساب نشط",
"count": 5,
"best_offer_id": "list_intelligence",
"fallback_offer_id": "whatsapp_compliance_setup",
"primary_channel": "email",
"id": 4,
"audience_ar": "SaaS صغير",
"subject_ar": "Pipeline بدون فوضى قنوات",
"body_ar": (
"هلا [الاسم]، إذا عندكم قائمة عملاء أو leads، نقدر نصنّفها ونطلع أفضل أهداف "
"مع تقرير مخاطر — بدون إرسال واتساب بارد. مهتم؟"
),
},
{
"id": 5,
"audience_ar": "متابعة ١",
"subject_ar": "متابعة خفيفة",
"body_ar": "تذكير لطيف: إذا يناسبكم، أرسل لي قطاعكم ومدينتكم وأجهز عينة خلال ٢٤ ساعة.",
},
{
"id": 6,
"audience_ar": "متابعة ٢",
"subject_ar": "إغلاق مهذب",
"body_ar": "إذا التوقيت مو مناسب، أقدر أرجع بعد أسبوعين — أو أغلق الملف عندكم برسالة «لا شكراً».",
},
],
"rules_ar": [
"لا scraping ولا قوائم مشتراة.",
"استخدم علاقاتك المباشرة + جهات تعرفها.",
"كل رسالة يدوية، لا automation.",
"حد أقصى 3 follow-ups ثم أرشفة.",
],
}
_BASE_INTRO = "هلا [الاسم]، أطلقنا Beta محدودة لـ Dealix."
def build_outreach_message(segment_id: str, *, name: str = "[الاسم]") -> dict[str, Any]:
"""Build the first-touch Arabic message for a segment."""
intro = f"هلا {name}،"
if segment_id == "agency_b2b":
body = (
f"{intro} عندي Beta خاص للوكالات.\n\n"
"Dealix يساعد الوكالة تطلع فرص لعملائها، تجهز رسائل عربية، تدير "
"موافقات، وتطلع Proof Pack باسم الوكالة والعميل.\n\n"
"أبحث عن وكالة واحدة نجرب معها Pilot مشترك على عميل حقيقي. "
"يناسبك ديمو 15 دقيقة؟"
)
elif segment_id == "training_consulting":
body = (
f"{intro} متابع توسع شركتكم في برامج الشركات.\n\n"
"Dealix يطلع لكم 10 فرص B2B خلال 7 أيام، يكتب الرسائل بالعربي، "
"ويخلي صاحب القرار يوافق قبل أي تواصل، وبعدها يعطي Proof Pack.\n\n"
"Pilot بـ499 ريال أو مجاني مقابل case study. يناسبك ديمو 12 دقيقة؟"
)
elif segment_id == "saas_tech_small":
body = (
f"{intro} رأيت إصدار النسخة الجديدة من منتجكم — مبروك.\n\n"
"نشتغل على مدير نمو عربي يطلع 10 فرص B2B، يستخدم LinkedIn Lead "
"Forms (لا scraping)، ويكتب الرسائل بالعربي.\n\n"
"أبغى أجربه مع شركة SaaS سعودية واحدة. يناسبك ديمو 12 دقيقة؟"
)
elif segment_id == "services_with_whatsapp":
body = (
f"{intro} عندكم قاعدة عملاء واتساب نشطة، صحيح؟\n\n"
"Dealix ينظف القائمة، يصنف الـ opt-in، يحظر cold WhatsApp تلقائياً، "
"ويكتب رسائل عربية للحملات الآمنة + Proof Pack شهري.\n\n"
"List Intelligence بـ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",
},
"note_ar": "كرّر وأكيّف الرسائل ٤–٦ لباقي الـ ٢٠ جهة — نفس النبرة الدافئة.",
"demo": True,
}

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 typing import Any
PRIVATE_BETA_OFFER: dict[str, Any] = {
"offer_id": "private_beta_pilot_7d",
"name_ar": "Private Beta Pilot — 7 أيام",
"promise_ar": (
"خلال 7 أيام نطلع لك 10 فرص B2B + رسائل عربية + خطة متابعة + Proof Pack، "
"وأنت توافق قبل أي تواصل."
),
"deliverables_ar": [
"10 فرص B2B مع why-now + buying committee.",
"10 رسائل عربية بنبرة سعودية طبيعية.",
"تصنيف القنوات (safe / needs_review / blocked) لكل contact.",
"خطة متابعة 7 أيام.",
"Proof Pack مختصر (PDF + JSON).",
"جلسة مراجعة 30 دقيقة في نهاية الأسبوع.",
],
"price_sar": 499,
"free_alternative_ar": "مجاني مقابل case study بعد انتهاء الـ Pilot.",
"approval_required": True,
"live_send_allowed": False,
"duration_days": 7,
"seats_available": 5,
}
def build_private_beta_offer(*, seats_remaining: int | None = None) -> dict[str, Any]:
"""Build today's Private Beta offer card. Seats are configurable."""
out = dict(PRIVATE_BETA_OFFER)
if seats_remaining is not None:
out["seats_available"] = max(0, int(seats_remaining))
out["upsell_path"] = [
"growth_os_pilot_30d",
"growth_os_monthly",
]
return out
def build_private_beta_safety_notes() -> dict[str, Any]:
"""Return the explicit 'what we will NOT do today' list."""
def build_private_beta_offer() -> dict[str, Any]:
return {
"title_ar": "ضمانات Dealix",
"do_not_do_ar": [
"لا live WhatsApp send بدون env flag + اعتماد بشري.",
"لا live Gmail send.",
"لا Calendar insert تلقائي.",
"لا charge Moyasar تلقائي — invoice/payment link يدوي فقط.",
"لا scraping LinkedIn ولا auto-DM.",
"لا cold WhatsApp (PDPL).",
"لا وعود بنتائج مضمونة.",
"لا تخزين بيانات بطاقات.",
"title_ar": "Dealix — البيتا الخاصة",
"tagline_ar": "مدير نمو عربي: فرص، مسودات، موافقة، Proof — بدون إرسال حي افتراضياً.",
"included_ar": [
"تشخيص نمو مجاني أو مدفوع حسب الاتفاق",
"سباق ١٠ فرص أو ذكاء قائمة (حسب الحالة)",
"كروت موافقة عربية (أزرار ≤٣)",
"Proof Pack أسبوعي تجريبي",
],
"do_ar": [
"Approval-first في كل قناة.",
"Audit ledger لكل فعل.",
"Saudi Tone + Safety eval قبل أي رسالة.",
"Reputation Guard يوقف القناة عند تدهور السمعة.",
"Free Diagnostic قبل أي التزام.",
"excluded_ar": [
"إرسال واتساب جماعي بارد",
"Gmail إرسال تلقائي",
"إدراج تقويم حي بدون موافقة",
"شحن بطاقات داخل Dealix",
],
"pilot_pricing_sar": {"low": 499, "high": 3000, "note_ar": "٧ أيام أو ٣٠ يوم — حسب النطاق"},
"monthly_after_sar": {"low": 2999, "high": 9999},
"live_send_default": False,
"demo": True,
}
def private_beta_faq() -> list[dict[str, str]]:
"""Common Arabic FAQ entries for the Private Beta page."""
return [
{
"q_ar": "كيف يعمل Pilot الـ7 أيام؟",
"a_ar": (
"نأخذ منك intake (قطاع/مدينة/عرض/هدف) خلال 30 دقيقة. "
"خلال 24 ساعة عمل نسلّم 10 فرص + رسائل + تصنيف القنوات. "
"خلال الأسبوع نتابع الردود ونحدّث Proof Pack."
),
},
{
"q_ar": "هل ترسلون رسائل بدون موافقتي؟",
"a_ar": "لا. كل رسالة تظل draft حتى توافق عليها صراحة.",
},
{
"q_ar": "ماذا لو ما رد أحد؟",
"a_ar": (
"Proof Pack يوضح المخاطر التي منعناها + توصية بقطاع/زاوية مختلفة. "
"Pilot يثبت طريقة التشغيل وليس عدداً مضموناً من الصفقات."
),
},
{
"q_ar": "هل تعرفون شروط واتساب ولينكدإن؟",
"a_ar": (
"نعم. لا cold WhatsApp بدون opt-in. "
"لا scraping ولا auto-DM في LinkedIn — نستخدم Lead Gen Forms والمهام اليدوية."
),
},
{
"q_ar": "كيف أدفع 499 ريال؟",
"a_ar": (
"نرسل لك Moyasar invoice أو payment link من الـ dashboard. "
"بعد الدفع نبدأ Pilot يوم الأحد التالي."
),
},
{
"q_ar": "هل يصلح للوكالات؟",
"a_ar": (
"نعم — Agency Partner Program يعطي الوكالة co-branded Proof Pack + "
"revenue share على عملائها. تواصل معنا مباشرة للترتيب."
),
},
]

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
works fine with manually-pasted transcripts during private beta.
All outputs are Arabic, deterministic, and approval-required before any
external action.
"""
from __future__ import annotations
from .deal_risk import compute_deal_risk
from .followup_builder import build_post_meeting_followup
from .meeting_brief import build_pre_meeting_brief
from .objection_extractor import extract_objections
from .transcript_parser import parse_transcript_entries, summarize_meeting
from auto_client_acquisition.meeting_intelligence.followup_builder import build_post_meeting_followup
from auto_client_acquisition.meeting_intelligence.meeting_brief import build_pre_meeting_brief
from auto_client_acquisition.meeting_intelligence.transcript_parser import summarize_transcript_text
__all__ = [
"build_post_meeting_followup",
"build_pre_meeting_brief",
"compute_deal_risk",
"extract_objections",
"parse_transcript_entries",
"summarize_meeting",
"summarize_transcript_text",
]

View File

@ -1,81 +1,18 @@
"""Deal risk score from meeting + objection signals."""
"""Deal risk hint from simple signals."""
from __future__ import annotations
from typing import Any
def compute_deal_risk(
*,
objections: list[dict[str, Any]] | None = None,
next_step_set: bool = False,
decision_maker_present: bool = False,
days_since_last_touch: int = 0,
expected_value_sar: float = 0.0,
) -> dict[str, Any]:
"""
Compute a deal-level risk score (0..100) from meeting outcomes.
Higher = riskier. Returns deterministic Arabic risk reasons.
"""
objections = objections or []
score = 0
reasons_ar: list[str] = []
# Objection-based risk.
categories = {str(o.get("category", "")).lower() for o in objections}
if "price" in categories:
score += 20
reasons_ar.append("اعتراض على السعر — يحتاج إثبات قيمة وعينة محسوبة.")
if "timing" in categories:
score += 15
reasons_ar.append("اعتراض توقيت — احفظ الفرصة لربع لاحق.")
if "authority" in categories:
score += 25
reasons_ar.append("صاحب القرار غير حاضر — يلزم اجتماع ثانٍ معه.")
if "trust" in categories:
score += 20
reasons_ar.append("قلق أمان/خصوصية — أرفق DPA و PDPL.")
if "integration" in categories:
score += 10
reasons_ar.append("قلق تكامل — حضّر مخطط ربط CRM.")
if "competitor" in categories:
score += 15
reasons_ar.append("بديل قائم — جهّز battlecard مقارنة.")
# Process risk.
if not next_step_set:
score += 25
reasons_ar.append("لم يتم تحديد خطوة تالية بتاريخ — أعلى مؤشر فقدان.")
if not decision_maker_present:
score += 10
reasons_ar.append("صانع القرار لم يحضر الاجتماع.")
if days_since_last_touch > 14:
score += 10
reasons_ar.append(
f"مرّ {days_since_last_touch} يوم على آخر تواصل — فرصة باردة."
)
# Cap.
score = max(0, min(100, score))
if score >= 70:
risk_level = "high"
elif score >= 40:
risk_level = "medium"
else:
risk_level = "low"
return {
"risk_score": score,
"risk_level": risk_level,
"reasons_ar": reasons_ar,
"expected_value_sar": expected_value_sar,
"recommended_action_ar": (
"اجتماع ثانٍ مع صاحب القرار خلال 5 أيام + مادة إثبات قيمة قصيرة."
if risk_level == "high" else
"متابعة خلال 3 أيام مع خطوة تالية محددة."
if risk_level == "medium" else
"تنفيذ الخطوة التالية المتفق عليها كما هي."
),
}
def assess_deal_risk(signals: dict[str, Any] | None = None) -> dict[str, Any]:
s = signals or {}
risk = "low"
reasons: list[str] = []
if s.get("no_followup_scheduled"):
risk = "medium"
reasons.append("لا يوجد موعد متابعة بعد الاجتماع.")
if s.get("ghosted_after_proposal"):
risk = "high"
reasons.append("توقف التواصل بعد العرض.")
return {"risk_level": risk, "reasons_ar": reasons, "demo": True}

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 typing import Any
def build_post_meeting_followup(
*,
summary: dict[str, Any] | None = None,
next_steps: list[str] | None = None,
contact_name: str = "",
company_name: str = "",
objections: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""
Build a draft follow-up email/WhatsApp message in Arabic.
Always returns approval_required=True; never executes a send.
"""
next_steps = next_steps or []
objections = objections or []
salutation = f"هلا {contact_name}" if contact_name else "هلا"
company_part = f" من شركة {company_name}" if company_name else ""
bullet_steps = "\n".join([f"{s}" for s in next_steps]) or "• [حدد الخطوة التالية بتاريخ محدد]"
objection_addressed = ""
if objections:
labels = sorted({str(o.get("label_ar", "")) for o in objections if o.get("label_ar")})
if labels:
objection_addressed = (
"\nرجعت بعد الاجتماع وفكرت في النقاط التي ذكرتها: "
+ "، ".join(labels)
+ ". أرفقت لك إجابات قصيرة مع أمثلة."
)
body_ar = (
f"{salutation}،\n"
f"شكراً على وقتك اليوم{company_part}. "
"ملخص ما اتفقنا عليه:\n"
f"{bullet_steps}\n"
f"{objection_addressed}\n"
"\nإذا كل شي واضح من جهتك، أبدأ في تجهيز Pilot قصير ونشتغل خلال أسبوع. "
"أي ملاحظة تحب تضيفها قبل ما نبدأ؟\n\nشاكر لك."
def build_post_meeting_followup(summary_ar: str, next_steps: list[str] | None = None) -> dict[str, Any]:
steps = next_steps or ["إرسال ملخص موافق عليه", "تحديد موعد متابعة", "مشاركة مسودة عرض مختصرة"]
body = (
f"شكراً لوقتكم. الملخص: {summary_ar[:200]}\n"
f"الخطوات المقترحة: {'؛ '.join(steps)}.\n"
"ننتظر تأكيدكم للمتابعة."
)
subject_ar = f"متابعة اجتماع اليوم — {company_name or 'Dealix'}"
return {
"channel_drafts": {
"email": {
"subject_ar": subject_ar,
"body_ar": body_ar,
"approval_required": True,
"live_send_allowed": False,
},
"whatsapp": {
"body_ar": (
f"{salutation}، شكراً على اجتماع اليوم. "
"الخطوة التالية: " + (next_steps[0] if next_steps else "نحدد موعد بداية الـPilot") +
". أتابع معك خلال يومين."
),
"approval_required": True,
"live_send_allowed": False,
},
},
"summary_used": bool(summary),
"objections_addressed": [str(o.get("label_ar")) for o in objections if o.get("label_ar")],
"approval_required": True,
}
return {"subject_ar": "متابعة — ملخص الاجتماع والخطوة التالية", "body_ar": body, "approval_required": True, "demo": True}

View File

@ -1,4 +1,4 @@
"""Pre-meeting brief builder — deterministic Arabic output."""
"""Pre-meeting brief from company/contact context."""
from __future__ import annotations
@ -6,69 +6,24 @@ from typing import Any
def build_pre_meeting_brief(
*,
company: dict[str, Any] | None = None,
contact: dict[str, Any] | None = None,
opportunity: dict[str, Any] | None = None,
sector: str | None = None,
) -> dict[str, Any]:
"""
Build a 6-section Arabic pre-meeting brief.
All inputs are optional; the brief degrades to a generic but useful template.
"""
company = company or {}
contact = contact or {}
opportunity = opportunity or {}
sector = sector or str(company.get("sector", "saas"))
company_name = company.get("name", "?")
contact_name = contact.get("name", "?")
contact_role = contact.get("role", "?")
deal_value = opportunity.get("expected_value_sar", 0)
objective_ar = (
f"توضيح ملاءمة الحل لشركة {company_name}، "
f"وفهم المعيار الذي يستخدمه {contact_name} للقرار، "
"ثم تحديد خطوة تالية واضحة."
)
questions_ar = [
f"كيف تتعاملون اليوم مع [مشكلة قطاع {sector}",
"ما الذي جعلكم تنظرون لحل الآن وليس قبل 6 أشهر؟",
"من المسؤول عن قرار الشراء غيرك؟",
"ما المعيار الذي يجعلكم تقولون: نعم، خلونا نبدأ؟",
"ما الميزانية التقريبية المخصصة لهذه المشكلة؟",
]
likely_objections_ar = [
"السعر مرتفع مقارنة بالأدوات المحلية.",
"نحن مرتبطون بـ CRM/أداة حالية ولا نريد التبديل.",
"نحتاج تجربة فريق صغير أولاً قبل القرار.",
"هل الحل متوافق مع PDPL ولا يخزن بياناتنا خارج المملكة؟",
"كم يستغرق الإعداد فعلياً؟",
]
offer_skeleton_ar = (
f"عرض pilot لمدة 7 أيام لشركة {company_name}: "
"10 فرص B2B + رسائل عربية + متابعة + Proof Pack. "
"السعر 499 ريال أو مجاني مقابل case study."
)
next_step_ar = (
"في نهاية المكالمة: اقترح خطوة محددة بتاريخ — "
"إما الموافقة على بدء Pilot، أو إعادة الاجتماع خلال 5 أيام مع صانع القرار."
)
c = company or {}
p = contact or {}
o = opportunity or {}
return {
"company_name": company_name,
"contact_name": contact_name,
"contact_role": contact_role,
"expected_value_sar": deal_value,
"objective_ar": objective_ar,
"questions_ar": questions_ar,
"likely_objections_ar": likely_objections_ar,
"offer_skeleton_ar": offer_skeleton_ar,
"next_step_ar": next_step_ar,
"approval_required": True,
"company_ar": str(c.get("name") or c.get("company_name") or "الشركة"),
"contact_ar": str(p.get("name") or "جهة الاتصال"),
"objective_ar": str(o.get("objective_ar") or "مناقشة ملاءمة الحل والخطوة التالية."),
"questions_ar": [
"ما معيار القرار والجدول الزمني؟",
"ما أكبر مخاطرة يرونها اليوم؟",
"ما الشكل المثالي للتجربة خلال ٧ أيام؟",
"ما الميزانية أو نطاقها التقريبي؟",
"من يشارك من جانبهم في التنفيذ؟",
],
"likely_objections_ar": ["السعر", "التوقيت", "التكامل مع الأنظمة الحالية"],
"demo": True,
}

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
import re
from typing import Any
# Each entry: (category, regex pattern (case-insensitive), Arabic gloss).
OBJECTION_PATTERNS: tuple[tuple[str, str, str], ...] = (
("price", r"غالي|مرتفع|الميزانية|expensive|too\s+pricey|cost", "السعر/الميزانية"),
("timing", r"ليس\s+الآن|بعد\s+شهر|الربع\s+القادم|not\s+now|next\s+quarter", "التوقيت"),
("authority", r"المدير|صاحب\s+القرار|need\s+approval|decision\s+maker", "صاحب القرار"),
("trust", r"بيانات|خصوصية|أمان|PDPL|trust|security|privacy", "الأمان والخصوصية"),
("integration", r"CRM|نظامنا|الربط|integration|migration", "التكامل/الترحيل"),
("competitor", r"نستخدم|بديل|أداة\s+ثانية|competitor|alternative", "وجود بديل/منافس"),
("results", r"نتائج|مضمون|guarantee|ROI|دليل", "إثبات النتائج"),
("complexity", r"معقد|صعب|تدريب|onboarding|complex|hard", "التعقيد/التبني"),
)
_KEYWORDS = ("ميزانية", "غالي", "لاحقاً", "نراجع", "ليس أولوية", "تكامل", "أمان", "عقد", "منافس")
def extract_objections(transcript_text: str) -> dict[str, object]:
"""
Extract objection categories from a free-text transcript.
Returns:
{
"objections": [{"category", "label_ar", "snippet"}],
"categories_found": [str],
"count": int,
}
"""
if not transcript_text:
return {"objections": [], "categories_found": [], "count": 0}
found: list[dict[str, str]] = []
seen_categories: set[str] = set()
for cat, pattern, gloss in OBJECTION_PATTERNS:
for m in re.finditer(pattern, transcript_text, flags=re.IGNORECASE):
seen_categories.add(cat)
start = max(0, m.start() - 40)
end = min(len(transcript_text), m.end() + 40)
snippet = transcript_text[start:end].replace("\n", " ").strip()
found.append({
"category": cat,
"label_ar": gloss,
"snippet": snippet[:200],
})
return {
"objections": found,
"categories_found": sorted(seen_categories),
"count": len(found),
}
def extract_objections(transcript_text: str) -> dict[str, Any]:
text = transcript_text or ""
found: list[str] = []
for kw in _KEYWORDS:
if re.search(re.escape(kw), text, flags=re.IGNORECASE):
found.append(kw)
return {"objections_ar": list(dict.fromkeys(found))[:8], "demo": True}

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
@ -6,87 +6,13 @@ import re
from typing import Any
def parse_transcript_entries(entries: list[dict[str, Any]] | str) -> dict[str, Any]:
"""
Normalize either:
- a list of Google-Meet-shaped entries [{"participantId", "text", ...}], or
- a plain string transcript with "Speaker: text" lines.
Returns:
{
"speaker_turns": [{"speaker", "text"}],
"speakers": [str],
"total_chars": int,
"total_turns": int,
}
"""
speaker_turns: list[dict[str, str]] = []
if isinstance(entries, str):
for raw in entries.splitlines():
line = raw.strip()
if not line:
continue
m = re.match(r"^([^:]{1,40}):\s*(.+)$", line)
if m:
speaker_turns.append({"speaker": m.group(1).strip(),
"text": m.group(2).strip()})
else:
speaker_turns.append({"speaker": "?", "text": line})
else:
for e in entries or []:
speaker = (
e.get("participant")
or e.get("participantId")
or e.get("speaker")
or "?"
)
text = e.get("text") or e.get("content") or ""
text = str(text).strip()
if not text:
continue
speaker_turns.append({"speaker": str(speaker), "text": text})
speakers = sorted({t["speaker"] for t in speaker_turns})
total_chars = sum(len(t["text"]) for t in speaker_turns)
def summarize_transcript_text(text: str) -> dict[str, Any]:
lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
bullets = lines[:5] if lines else ["لا يوجد نص كافٍ."]
word_count = len(re.findall(r"\w+", text or "", flags=re.UNICODE))
return {
"speaker_turns": speaker_turns,
"speakers": speakers,
"total_chars": total_chars,
"total_turns": len(speaker_turns),
}
def summarize_meeting(parsed: dict[str, Any]) -> dict[str, Any]:
"""
Produce an Arabic summary skeleton from parsed turns.
Deterministic; LLM-free for Phase D MVP.
"""
turns = parsed.get("speaker_turns", [])
speakers = parsed.get("speakers", [])
# Extract a few candidate "topic" sentences: longest turns.
sorted_by_len = sorted(turns, key=lambda t: -len(t["text"]))[:5]
topic_lines = [t["text"][:200] for t in sorted_by_len]
# Detect questions.
questions: list[str] = []
for t in turns:
text = t["text"]
if "؟" in text or text.rstrip().endswith("?"):
questions.append(text[:200])
if len(questions) >= 5:
break
return {
"summary_ar": [
f"شارك في الاجتماع {len(speakers)} متحدث.",
f"إجمالي عدد الأدوار الكلامية: {parsed.get('total_turns', 0)}.",
"أبرز نقاط النقاش (مرشحة آلياً، تحتاج مراجعة):",
*[f"{line}" for line in topic_lines],
],
"speakers": speakers,
"candidate_questions_ar": questions,
"approval_required": True,
"bullets_ar": bullets,
"word_count": word_count,
"demo": True,
"note_ar": "ملخص من نص خام — ربط Google Meet API لاحقاً مع OAuth.",
}

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
from .fallback_policy import build_fallback_chain
from .provider_registry import (
ALL_PROVIDERS,
ALL_TASK_TYPES,
Provider,
TaskType,
get_provider,
)
from .task_router import RouteDecision, route_task
from .usage_dashboard import build_usage_demo
__all__ = [
"ALL_PROVIDERS",
"ALL_TASK_TYPES",
"CostClass",
"Provider",
"RouteDecision",
"TaskType",
"build_fallback_chain",
"build_usage_demo",
"classify_cost",
"get_provider",
"route_task",
]
__all__ = ["list_tasks", "route_task"]

View File

@ -1,171 +1,16 @@
"""Registry of model providers + task types."""
"""Static provider labels for routing display."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# Task types Dealix actually routes.
ALL_TASK_TYPES: tuple[str, ...] = (
"strategic_reasoning",
"arabic_copywriting",
"classification",
"compliance_guardrail",
"meeting_analysis",
"vision_analysis",
"extraction",
"summarization",
"coding_project_understanding",
"low_cost_bulk",
)
_PROVIDERS: list[dict[str, Any]] = [
{"id": "anthropic", "label": "Anthropic", "tasks_default": ["strategic_reasoning", "arabic_copywriting"]},
{"id": "openai", "label": "OpenAI", "tasks_default": ["classification", "summarization"]},
{"id": "google", "label": "Google Gemini", "tasks_default": ["vision_analysis", "meeting_analysis"]},
{"id": "groq", "label": "Groq", "tasks_default": ["low_cost_bulk", "extraction"]},
]
@dataclass(frozen=True)
class Provider:
"""A model provider entry."""
key: str
label: str
family: str # "anthropic" | "openai" | "google" | "azure" | "local"
capabilities: tuple[str, ...] # subset of ALL_TASK_TYPES
cost_class: str # "low" | "mid" | "high"
latency_class: str # "fast" | "balanced" | "slow"
supports_vision: bool
supports_arabic: bool
privacy_tier: str # "vendor_cloud" | "ksa_region" | "self_hosted"
notes_ar: str = ""
def to_dict(self) -> dict[str, object]:
return {
"key": self.key, "label": self.label, "family": self.family,
"capabilities": list(self.capabilities),
"cost_class": self.cost_class, "latency_class": self.latency_class,
"supports_vision": self.supports_vision,
"supports_arabic": self.supports_arabic,
"privacy_tier": self.privacy_tier,
"notes_ar": self.notes_ar,
}
# Conservative provider list — Dealix can swap any of these without code change.
ALL_PROVIDERS: tuple[Provider, ...] = (
Provider(
key="claude_sonnet",
label="Claude Sonnet",
family="anthropic",
capabilities=(
"strategic_reasoning", "arabic_copywriting",
"compliance_guardrail", "meeting_analysis", "summarization",
"coding_project_understanding",
),
cost_class="mid",
latency_class="balanced",
supports_vision=True,
supports_arabic=True,
privacy_tier="vendor_cloud",
notes_ar="مناسب للاستراتيجية والكتابة العربية والامتثال.",
),
Provider(
key="claude_haiku",
label="Claude Haiku",
family="anthropic",
capabilities=("classification", "extraction", "low_cost_bulk", "summarization"),
cost_class="low",
latency_class="fast",
supports_vision=False,
supports_arabic=True,
privacy_tier="vendor_cloud",
notes_ar="رخيص وسريع — للتصنيف الكثيف والاستخراج.",
),
Provider(
key="gpt_4_class",
label="GPT-4-class",
family="openai",
capabilities=(
"strategic_reasoning", "vision_analysis",
"coding_project_understanding", "meeting_analysis",
),
cost_class="high",
latency_class="balanced",
supports_vision=True,
supports_arabic=True,
privacy_tier="vendor_cloud",
notes_ar="بديل قوي للاستراتيجية والرؤية.",
),
Provider(
key="gpt_4o_mini",
label="GPT-4o mini",
family="openai",
capabilities=("classification", "extraction", "low_cost_bulk"),
cost_class="low",
latency_class="fast",
supports_vision=True,
supports_arabic=True,
privacy_tier="vendor_cloud",
notes_ar="بديل رخيص للمهام الكثيفة.",
),
Provider(
key="gemini_pro",
label="Gemini Pro",
family="google",
capabilities=(
"vision_analysis", "summarization", "meeting_analysis",
"extraction",
),
cost_class="mid",
latency_class="balanced",
supports_vision=True,
supports_arabic=True,
privacy_tier="vendor_cloud",
notes_ar="ممتاز للرؤية والاجتماعات.",
),
Provider(
key="azure_oai_ksa",
label="Azure OpenAI (KSA region)",
family="azure",
capabilities=(
"strategic_reasoning", "arabic_copywriting",
"compliance_guardrail", "extraction", "summarization",
),
cost_class="mid",
latency_class="balanced",
supports_vision=True,
supports_arabic=True,
privacy_tier="ksa_region",
notes_ar="منطقة KSA — مناسب للعملاء الحساسين للامتثال.",
),
Provider(
key="local_qwen_ar",
label="Local Qwen (Arabic-tuned)",
family="local",
capabilities=("classification", "extraction", "low_cost_bulk", "arabic_copywriting"),
cost_class="low",
latency_class="balanced",
supports_vision=False,
supports_arabic=True,
privacy_tier="self_hosted",
notes_ar="نموذج محلي — للحالات الحساسة جداً.",
),
)
def get_provider(key: str) -> Provider | None:
return next((p for p in ALL_PROVIDERS if p.key == key), None)
@dataclass(frozen=True)
class TaskType:
"""Description of a routed task."""
key: str
label_ar: str
requires_arabic: bool
requires_vision: bool
sensitivity: str # "low" | "medium" | "high"
notes_ar: str = ""
def to_dict(self) -> dict[str, object]:
return {
"key": self.key, "label_ar": self.label_ar,
"requires_arabic": self.requires_arabic,
"requires_vision": self.requires_vision,
"sensitivity": self.sensitivity,
"notes_ar": self.notes_ar,
}
def list_providers() -> dict[str, Any]:
return {"providers": list(_PROVIDERS), "demo": True}

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 dataclasses import dataclass
from typing import Any
from .cost_policy import CostClass, classify_cost
from .fallback_policy import build_fallback_chain
from .provider_registry import ALL_TASK_TYPES, get_provider
_ROUTES: dict[str, dict[str, Any]] = {
"strategic_reasoning": {"provider": "anthropic", "cost_class": "high", "needs_guardrail": True},
"arabic_copywriting": {"provider": "anthropic", "cost_class": "medium", "needs_guardrail": True},
"classification": {"provider": "openai", "cost_class": "low", "needs_guardrail": True},
"compliance_guardrail": {"provider": "openai", "cost_class": "low", "needs_guardrail": False},
"meeting_analysis": {"provider": "google", "cost_class": "medium", "needs_guardrail": True},
"vision_analysis": {"provider": "google", "cost_class": "medium", "needs_guardrail": True},
"extraction": {"provider": "groq", "cost_class": "low", "needs_guardrail": True},
"summarization": {"provider": "openai", "cost_class": "low", "needs_guardrail": True},
"low_cost_bulk": {"provider": "groq", "cost_class": "minimal", "needs_guardrail": True},
"coding_project_understanding": {"provider": "anthropic", "cost_class": "high", "needs_guardrail": True},
}
@dataclass(frozen=True)
class RouteDecision:
task_type: str
primary_provider: str | None
fallback_chain: list[str]
cost_class: CostClass
reasons_ar: list[str]
requires_arabic: bool
requires_vision: bool
sensitivity: str
def to_dict(self) -> dict[str, object]:
return {
"task_type": self.task_type,
"primary_provider": self.primary_provider,
"fallback_chain": self.fallback_chain,
"cost_class": self.cost_class,
"reasons_ar": self.reasons_ar,
"requires_arabic": self.requires_arabic,
"requires_vision": self.requires_vision,
"sensitivity": self.sensitivity,
}
def list_tasks() -> dict[str, Any]:
return {"task_types": sorted(_ROUTES.keys()), "demo": True}
def route_task(
task_type: str,
*,
requires_arabic: bool = False,
requires_vision: bool = False,
sensitivity: str = "low",
expected_input_tokens: int = 0,
expected_output_tokens: int = 0,
bulk: bool = False,
primary_provider: str | None = None,
) -> RouteDecision:
"""Route a task → primary provider + ordered fallback chain + cost class."""
reasons: list[str] = []
if task_type not in ALL_TASK_TYPES:
return RouteDecision(
task_type=task_type,
primary_provider=None,
fallback_chain=[],
cost_class="low",
reasons_ar=[f"نوع المهمة غير معروف: {task_type}"],
requires_arabic=requires_arabic,
requires_vision=requires_vision,
sensitivity=sensitivity,
)
cost_class = classify_cost(
task_type=task_type,
expected_input_tokens=expected_input_tokens,
expected_output_tokens=expected_output_tokens,
bulk=bulk,
)
chain = build_fallback_chain(
task_type,
requires_arabic=requires_arabic,
requires_vision=requires_vision,
sensitivity=sensitivity,
primary_key=primary_provider,
)
if not chain:
reasons.append(
"لا يوجد مزود مناسب — راجع capabilities أو خفّف القيود (vision/arabic)."
)
primary = chain[0] if chain else None
if primary:
p = get_provider(primary)
if p:
reasons.append(
f"المزود الأساسي: {p.label}{p.notes_ar}"
)
if sensitivity == "high":
reasons.append("حساسية عالية: تم تفضيل KSA-region/self-hosted أولاً.")
if bulk:
reasons.append("مهمة جماعية كبيرة: تم اختيار cost_class=low.")
return RouteDecision(
task_type=task_type,
primary_provider=primary,
fallback_chain=chain,
cost_class=cost_class,
reasons_ar=reasons,
requires_arabic=requires_arabic,
requires_vision=requires_vision,
sensitivity=sensitivity,
)
def route_task(task_type: str) -> dict[str, Any]:
t = (task_type or "").strip().lower().replace("-", "_")
if t not in _ROUTES:
return {"ok": False, "error": "unknown_task_type", "known": sorted(_ROUTES.keys()), "demo": True}
r = _ROUTES[t]
return {"ok": True, "task_type": t, **r, "fallback_provider": "groq", "demo": True}

View File

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

View File

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

View File

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

View File

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

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

View File

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

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