mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-20 08:19:34 +00:00
feat(dealix): sync full Dealix package to repo
- API routers, ACA modules, integrations (draft operators) - Docs, landing pages, scripts (launch readiness, scorecard) - Tests and CI workflow updates for Dealix Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4f02f54019
commit
b13cb389cc
81
.github/workflows/dealix-api-ci.yml
vendored
81
.github/workflows/dealix-api-ci.yml
vendored
@ -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,52 @@ 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
|
||||
|
||||
- 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
|
||||
|
||||
35
.github/workflows/dealix-staging-smoke.yml
vendored
35
.github/workflows/dealix-staging-smoke.yml
vendored
@ -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"
|
||||
|
||||
@ -15,11 +15,15 @@ from fastapi.responses import JSONResponse
|
||||
from api.middleware import RequestIDMiddleware
|
||||
from api.routers import (
|
||||
admin,
|
||||
agent_observability,
|
||||
agents,
|
||||
autonomous_service_operator,
|
||||
automation,
|
||||
autonomous,
|
||||
business,
|
||||
command_center,
|
||||
connector_router,
|
||||
customer_ops,
|
||||
customer_success,
|
||||
data,
|
||||
dominance,
|
||||
@ -27,18 +31,30 @@ from api.routers import (
|
||||
ecosystem,
|
||||
email_send,
|
||||
full_os,
|
||||
growth_curator,
|
||||
growth_operator,
|
||||
health,
|
||||
innovation,
|
||||
intelligence_layer,
|
||||
launch_ops,
|
||||
leads,
|
||||
meeting_intelligence,
|
||||
model_router,
|
||||
outreach,
|
||||
personal_operator,
|
||||
platform_services,
|
||||
pricing,
|
||||
prospect,
|
||||
public,
|
||||
revenue_launch,
|
||||
revenue,
|
||||
revenue_os,
|
||||
sales,
|
||||
sectors,
|
||||
security_curator,
|
||||
service_excellence,
|
||||
service_tower,
|
||||
targeting_os,
|
||||
v3,
|
||||
webhooks,
|
||||
)
|
||||
@ -130,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)
|
||||
@ -139,11 +156,26 @@ 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(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_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(business.router)
|
||||
app.include_router(personal_operator.router)
|
||||
app.include_router(public.router)
|
||||
|
||||
41
dealix/api/routers/agent_observability.py
Normal file
41
dealix/api/routers/agent_observability.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Agent observability demo endpoints — evals and trace shapes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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.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(default_factory=dict)) -> dict[str, Any]:
|
||||
return build_trace_event(
|
||||
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 {},
|
||||
)
|
||||
137
dealix/api/routers/autonomous_service_operator.py
Normal file
137
dealix/api/routers/autonomous_service_operator.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Autonomous Service Operator — /api/v1/operator (deterministic MVP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
|
||||
from auto_client_acquisition.autonomous_service_operator import (
|
||||
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"])
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@router.post("/chat/message")
|
||||
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 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.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}
|
||||
|
||||
|
||||
@router.get("/cards/pending")
|
||||
async def operator_cards_pending() -> dict[str, Any]:
|
||||
return {"pending": ss.list_sessions_with_pending(), "demo": True}
|
||||
|
||||
|
||||
@router.post("/service/start")
|
||||
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("/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 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)
|
||||
16
dealix/api/routers/connector_router.py
Normal file
16
dealix/api/routers/connector_router.py
Normal 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()
|
||||
50
dealix/api/routers/customer_ops.py
Normal file
50
dealix/api/routers/customer_ops.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Customer ops API — onboarding, SLA, connectors (deterministic)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
@router.get("/onboarding/checklist")
|
||||
async def onboarding_checklist(service_id: str | None = None) -> dict[str, object]:
|
||||
return build_onboarding_checklist(service_id)
|
||||
|
||||
|
||||
@router.get("/support/sla")
|
||||
async def support_sla() -> dict[str, object]:
|
||||
return build_sla_summary()
|
||||
|
||||
|
||||
@router.get("/connectors/status")
|
||||
async def connectors_status() -> dict[str, object]:
|
||||
return build_connector_status()
|
||||
|
||||
|
||||
@router.get("/success/cadence")
|
||||
async def success_cadence() -> dict[str, object]:
|
||||
return build_weekly_cadence()
|
||||
|
||||
|
||||
@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, object] = Body(default_factory=dict)) -> dict[str, object]:
|
||||
issue = str(payload.get("issue_ar") or "")
|
||||
return route_ticket(issue)
|
||||
|
||||
|
||||
@router.get("/incidents/classify")
|
||||
async def incidents_classify(severity: str = "P3") -> dict[str, object]:
|
||||
return classify_incident(severity)
|
||||
38
dealix/api/routers/growth_curator.py
Normal file
38
dealix/api/routers/growth_curator.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Growth curator API — grading and weekly report."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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("/report/demo")
|
||||
async def report_demo() -> dict[str, Any]:
|
||||
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()
|
||||
38
dealix/api/routers/growth_operator.py
Normal file
38
dealix/api/routers/growth_operator.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""
|
||||
Growth Operator — thin product-facing aliases over innovation + business.
|
||||
|
||||
لا يكرر منطق ten-in-ten؛ يعرّف مسارات متوقعة في وثائق الـ beta والـ smoke.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
@router.get("/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.get("/proof-pack/demo")
|
||||
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"}
|
||||
122
dealix/api/routers/intelligence_layer.py
Normal file
122
dealix/api/routers/intelligence_layer.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Intelligence layer API — deterministic JSON; optional ten-in-ten bridge."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
@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 {})
|
||||
|
||||
|
||||
@router.get("/command-feed")
|
||||
async def intel_command_feed() -> dict[str, Any]:
|
||||
return build_intel_command_feed()
|
||||
|
||||
|
||||
@router.get("/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()
|
||||
|
||||
|
||||
@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.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 {})
|
||||
|
||||
|
||||
@router.post("/trust-score")
|
||||
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(context: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return build_revenue_dna(context or {})
|
||||
|
||||
|
||||
@router.post("/opportunity-simulator")
|
||||
async def opportunity_simulator(inputs: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return simulate_opportunities(inputs or {})
|
||||
|
||||
|
||||
@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 {})
|
||||
|
||||
|
||||
@router.get("/competitive-moves")
|
||||
async def competitive_moves(sector: str | None = None) -> dict[str, Any]:
|
||||
return build_competitive_moves(sector)
|
||||
|
||||
|
||||
@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
|
||||
45
dealix/api/routers/launch_ops.py
Normal file
45
dealix/api/routers/launch_ops.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Launch ops API — private beta, demo, outreach, go/no-go."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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.get("/private-beta/offer")
|
||||
async def launch_private_beta_offer() -> dict[str, Any]:
|
||||
return build_private_beta_offer()
|
||||
|
||||
|
||||
@router.get("/demo-script")
|
||||
async def launch_demo_script() -> dict[str, Any]:
|
||||
return build_demo_script()
|
||||
|
||||
|
||||
@router.get("/outreach/first-20")
|
||||
async def launch_outreach_first_20() -> dict[str, Any]:
|
||||
return build_first_twenty_outreach()
|
||||
|
||||
|
||||
@router.get("/go-no-go")
|
||||
async def launch_go_no_go_get() -> dict[str, Any]:
|
||||
return evaluate_go_no_go(None)
|
||||
|
||||
|
||||
@router.post("/go-no-go")
|
||||
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("/scorecard")
|
||||
async def launch_scorecard() -> dict[str, Any]:
|
||||
return build_launch_scorecard()
|
||||
37
dealix/api/routers/meeting_intelligence.py
Normal file
37
dealix/api/routers/meeting_intelligence.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Meeting intelligence API — text in, Arabic briefs out (no Calendar insert)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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("/transcript/summarize")
|
||||
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(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("/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)
|
||||
27
dealix/api/routers/model_router.py
Normal file
27
dealix/api/routers/model_router.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Model routing API — configuration hints only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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("/tasks")
|
||||
async def tasks() -> dict[str, Any]:
|
||||
return list_tasks()
|
||||
|
||||
|
||||
@router.post("/route")
|
||||
async def route(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return route_task(str(payload.get("task_type") or ""))
|
||||
|
||||
|
||||
@router.get("/providers")
|
||||
async def providers() -> dict[str, Any]:
|
||||
return list_providers()
|
||||
185
dealix/api/routers/platform_services.py
Normal file
185
dealix/api/routers/platform_services.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""Platform Services API — Growth Control Tower (no live external sends)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from auto_client_acquisition.platform_services import (
|
||||
build_proof_summary,
|
||||
evaluate_action,
|
||||
event_to_inbox_card,
|
||||
execute_tool,
|
||||
get_action_ledger,
|
||||
get_service_catalog,
|
||||
list_channels,
|
||||
validate_event,
|
||||
)
|
||||
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"])
|
||||
|
||||
|
||||
@router.get("/service-catalog")
|
||||
async def service_catalog() -> dict[str, Any]:
|
||||
return get_service_catalog()
|
||||
|
||||
|
||||
@router.get("/services/catalog")
|
||||
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 list_channels()
|
||||
|
||||
|
||||
@router.post("/events/validate")
|
||||
async def events_validate(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return validate_event(payload or {})
|
||||
|
||||
|
||||
@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(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 {
|
||||
"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]:
|
||||
return build_inbox_feed()
|
||||
|
||||
|
||||
@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("/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("/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 {})
|
||||
|
||||
|
||||
# --- 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 {})
|
||||
97
dealix/api/routers/revenue_launch.py
Normal file
97
dealix/api/routers/revenue_launch.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""Revenue Today — offers, outreach templates, pilot delivery, manual payment (no live charge)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
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_payment_confirmation_checklist,
|
||||
build_payment_link_message,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/revenue-launch", tags=["revenue_launch"])
|
||||
|
||||
|
||||
@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.get("/outreach/first-20")
|
||||
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.get("/demo-flow")
|
||||
async def revenue_launch_demo_flow() -> dict[str, Any]:
|
||||
return {
|
||||
"flow": build_12_min_demo_flow(),
|
||||
"discovery": build_discovery_questions(),
|
||||
"close": build_close_script(),
|
||||
"objections": build_objection_responses(),
|
||||
"demo": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/pipeline/schema")
|
||||
async def revenue_launch_pipeline_schema() -> dict[str, Any]:
|
||||
return build_pipeline_schema()
|
||||
|
||||
|
||||
@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,
|
||||
}
|
||||
|
||||
|
||||
@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.get("/proof-pack/template")
|
||||
async def revenue_launch_proof_pack_template() -> dict[str, Any]:
|
||||
return build_private_beta_proof_pack()
|
||||
@ -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
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
37
dealix/api/routers/security_curator.py
Normal file
37
dealix/api/routers/security_curator.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Security curator API — redact and inspect diffs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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.get("/demo")
|
||||
async def demo() -> dict[str, Any]:
|
||||
return {"ok": True, "message_ar": "طبقة أمان للوكلاء — redaction وفحص فرق قبل التطبيق.", "demo": True}
|
||||
|
||||
|
||||
@router.post("/redact")
|
||||
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_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("/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}
|
||||
103
dealix/api/routers/service_excellence.py
Normal file
103
dealix/api/routers/service_excellence.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Service Excellence OS API — scoring, matrices, launch packages (deterministic)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
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_landing_page_outline,
|
||||
build_onboarding_checklist,
|
||||
build_sales_script,
|
||||
build_service_launch_package,
|
||||
)
|
||||
from auto_client_acquisition.service_excellence.proof_metrics import (
|
||||
build_proof_pack_template,
|
||||
required_proof_metrics,
|
||||
summarize_proof_ar,
|
||||
)
|
||||
from auto_client_acquisition.service_excellence.quality_review import review_all_services, review_service_before_launch
|
||||
from auto_client_acquisition.service_excellence.research_lab import build_service_research_brief
|
||||
from auto_client_acquisition.service_excellence.service_improvement_backlog import build_backlog, prioritize_backlog_items
|
||||
from auto_client_acquisition.service_excellence.service_scoring import calculate_service_excellence_score
|
||||
from auto_client_acquisition.service_excellence.workflow_builder import (
|
||||
build_approval_steps,
|
||||
build_day_by_day_execution_plan,
|
||||
build_workflow,
|
||||
validate_workflow,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/service-excellence", tags=["service_excellence"])
|
||||
|
||||
|
||||
@router.get("/review/all")
|
||||
async def review_all() -> dict[str, Any]:
|
||||
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
|
||||
|
||||
|
||||
@router.get("/{service_id}/proof-metrics")
|
||||
async def proof_metrics(service_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"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.get("/{service_id}/gap-analysis")
|
||||
async def gap_analysis(service_id: str) -> dict[str, Any]:
|
||||
return compare_against_categories(service_id)
|
||||
|
||||
|
||||
@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}/review")
|
||||
async def review_one(service_id: str) -> dict[str, Any]:
|
||||
return review_service_before_launch(service_id)
|
||||
173
dealix/api/routers/service_tower.py
Normal file
173
dealix/api/routers/service_tower.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""Service Tower API — sellable services wizard (no live send)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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_internal_operator_checklist,
|
||||
build_proof_pack_template,
|
||||
)
|
||||
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,
|
||||
quote_service,
|
||||
recommend_plan_after_service,
|
||||
)
|
||||
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id, list_tower_services
|
||||
from auto_client_acquisition.service_tower.service_scorecard import build_service_scorecard
|
||||
from auto_client_acquisition.service_tower.service_wizard import (
|
||||
build_intake_questions,
|
||||
recommend_service,
|
||||
start_service,
|
||||
summarize_recommendation_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.get("/catalog")
|
||||
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 services_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
p = payload or {}
|
||||
rec = recommend_service(
|
||||
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
|
||||
|
||||
|
||||
@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.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")
|
||||
async def service_workflow(service_id: str) -> dict[str, Any]:
|
||||
return build_service_workflow(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]:
|
||||
p = payload or {}
|
||||
q = quote_service(
|
||||
service_id,
|
||||
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}/intake-questions")
|
||||
async def intake_questions(service_id: str) -> dict[str, Any]:
|
||||
return build_intake_questions(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.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")
|
||||
async def service_upgrade(service_id: str) -> dict[str, Any]:
|
||||
return recommend_upgrade(service_id, {})
|
||||
133
dealix/api/routers/targeting_os.py
Normal file
133
dealix/api/routers/targeting_os.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""Targeting & Acquisition OS API — planning and evaluation only, no live send."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
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,
|
||||
recommend_linkedin_strategy,
|
||||
)
|
||||
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.post("/accounts/recommend")
|
||||
async def accounts_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return recommend_accounts(
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/buying-committee/map")
|
||||
async def buying_committee_map(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
|
||||
return map_buying_committee(
|
||||
str(payload.get("sector") or ""),
|
||||
payload.get("company_size"),
|
||||
payload.get("goal"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/contacts/evaluate")
|
||||
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")
|
||||
return evaluate_contactability(contact, str(desired) if desired else None)
|
||||
|
||||
|
||||
@router.post("/uploaded-list/analyze")
|
||||
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 {})
|
||||
|
||||
|
||||
@router.post("/outreach/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)
|
||||
|
||||
|
||||
@router.get("/daily-autopilot/demo")
|
||||
async def daily_autopilot_demo() -> dict[str, Any]:
|
||||
return build_daily_targeting_brief({"sector": "training", "city": "الرياض", "offer": "Growth OS", "goal": "meetings"})
|
||||
|
||||
|
||||
@router.get("/self-growth/demo")
|
||||
async def self_growth_demo() -> dict[str, Any]:
|
||||
return build_self_growth_daily_brief()
|
||||
|
||||
|
||||
@router.get("/reputation/status")
|
||||
async def reputation_status() -> dict[str, Any]:
|
||||
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("/linkedin/strategy")
|
||||
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 base
|
||||
|
||||
|
||||
@router.get("/services")
|
||||
async def targeting_services() -> dict[str, Any]:
|
||||
return list_targeting_services()
|
||||
|
||||
|
||||
@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 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)
|
||||
@ -0,0 +1,7 @@
|
||||
"""Agent observability stubs — trace shape + eval scores (no Langfuse SDK required)."""
|
||||
|
||||
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
|
||||
|
||||
__all__ = ["build_trace_event", "evaluate_safety", "evaluate_saudi_tone"]
|
||||
@ -0,0 +1,5 @@
|
||||
"""Named eval cases for CI — references only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
EVAL_CASES: tuple[str, ...] = ("no_guarantee_language", "no_payment_collect", "has_single_cta")
|
||||
@ -0,0 +1,19 @@
|
||||
"""Tripwire checks on Arabic marketing copy — deterministic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_BAD = ("ضمان كامل", "مضمون 100%", "ارسل لي رقم البطاقة", "كلمة المرور", "حسابك معلق")
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,19 @@
|
||||
"""Lightweight Saudi-Arabic tone score — heuristic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
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))
|
||||
return {"tone_score": score, "demo": True}
|
||||
@ -0,0 +1,35 @@
|
||||
"""Structured trace event for dashboards (PII-redacted strings)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.security_curator.trace_redactor import redact_trace_payload
|
||||
|
||||
|
||||
def build_trace_event(
|
||||
*,
|
||||
workflow_name: str,
|
||||
agent_name: str,
|
||||
action_type: str,
|
||||
policy_result: str,
|
||||
tool_called: str | None = None,
|
||||
outcome: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
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,
|
||||
"action_type": action_type,
|
||||
"policy_result": policy_result,
|
||||
"tool_called": tool_called,
|
||||
"outcome": outcome,
|
||||
"metadata": safe_meta,
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
"""Autonomous Service Operator — intent to service, approval-first, deterministic MVP."""
|
||||
|
||||
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
|
||||
|
||||
__all__ = ["handle_message", "list_bundles", "get_bundle"]
|
||||
@ -0,0 +1,14 @@
|
||||
"""Agency partner prioritization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def mode_profile() -> dict[str, Any]:
|
||||
return {
|
||||
"mode": "agency_partner",
|
||||
"priority_intents": ["want_partnerships", "ask_services", "ask_proof"],
|
||||
"card_types_first": ["opportunity", "proof_update", "compliance_risk"],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
"""Human-in-the-loop gates for operator workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.autonomous_service_operator import session_state as ss
|
||||
|
||||
|
||||
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 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})
|
||||
|
||||
|
||||
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
|
||||
@ -0,0 +1,14 @@
|
||||
"""End-customer (growth manager) prioritization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def mode_profile() -> dict[str, Any]:
|
||||
return {
|
||||
"mode": "client",
|
||||
"priority_intents": ["want_more_customers", "has_contact_list", "want_meetings"],
|
||||
"card_types_first": ["approval_needed", "opportunity", "proof_update"],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
"""Single entry: user message → intent → recommendation + session updates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
|
||||
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")
|
||||
|
||||
reply_ar = _build_reply_ar(intent, rec)
|
||||
om.append_turn(session_id, "assistant", reply_ar, {"recommendation": rec})
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"intent": intent,
|
||||
"recommendation": rec,
|
||||
"reply_ar": reply_ar,
|
||||
"bundles_hint": sb.list_bundles() if intent == ic.INTENT_ASK_SERVICES else None,
|
||||
"demo": True,
|
||||
}
|
||||
|
||||
|
||||
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."
|
||||
)
|
||||
if intent == ic.INTENT_ASK_PROOF:
|
||||
return f"Proof Pack مرتبط بخدمة {name} — جاهز كعرض تجريبي بعد أول مسودات موافَق عليها."
|
||||
return f"نوصي بخدمة: {name} ({sid}). الخطوة التالية: أكمل المدخلات ثم راجع المسودات قبل أي إرسال."
|
||||
@ -0,0 +1,14 @@
|
||||
"""CEO-style prioritization hints for cards and intents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def mode_profile() -> dict[str, Any]:
|
||||
return {
|
||||
"mode": "executive",
|
||||
"priority_intents": ["want_more_customers", "ask_proof", "approve_action"],
|
||||
"card_types_first": ["leak", "approval_needed", "opportunity"],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
"""Required intake fields per service_id — from Service Tower catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.service_tower.service_catalog import get_service_by_id
|
||||
|
||||
|
||||
def intake_questions(service_id: str) -> dict[str, Any]:
|
||||
svc = get_service_by_id(service_id) or {}
|
||||
fields = svc.get("inputs_required") or []
|
||||
return {
|
||||
"service_id": service_id,
|
||||
"fields": [{"name": f, "prompt_ar": f"يرجى تزويدنا بـ: {f}"} for f in fields],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
"""Rule-based intent classification (AR/EN) — no LLM required for MVP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Final
|
||||
|
||||
# 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 normalize_user_text(text: str) -> str:
|
||||
t = (text or "").strip().lower()
|
||||
t = re.sub(r"\s+", " ", t)
|
||||
return t
|
||||
|
||||
|
||||
def classify_intent(text: str) -> str:
|
||||
"""Return intent id from free-form user message."""
|
||||
t = normalize_user_text(text)
|
||||
|
||||
if not t:
|
||||
return INTENT_UNKNOWN
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@ -0,0 +1,19 @@
|
||||
"""Append-only conversation turns per session (in-memory MVP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.autonomous_service_operator import session_state as ss
|
||||
|
||||
|
||||
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 list_turns(session_id: str) -> list[dict[str, Any]]:
|
||||
s = ss.get_session(session_id) or {}
|
||||
return list(s.get("turns") or [])
|
||||
@ -0,0 +1,20 @@
|
||||
"""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 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 — {svc.get('name_ar', service_id)}",
|
||||
"metrics": metrics,
|
||||
"sample_counts": {m: 0 for m in metrics},
|
||||
"notes_ar": "أرقام تجريبية حتى ربط عميل حقيقي ودفتر أحداث.",
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Self-growth mode: Dealix uses its own OS for prospecting (drafts + manual approval only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
"""Productized service bundles — SAR ranges and catalog service_ids."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
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",
|
||||
},
|
||||
"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",
|
||||
},
|
||||
"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",
|
||||
},
|
||||
"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",
|
||||
},
|
||||
"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",
|
||||
},
|
||||
"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 {"bundles": list(_BUNDLES.values()), "demo": True}
|
||||
|
||||
|
||||
def get_bundle(bundle_id: str) -> dict[str, Any] | None:
|
||||
return _BUNDLES.get((bundle_id or "").strip())
|
||||
@ -0,0 +1,15 @@
|
||||
"""Service delivery mode: running client services with SLA-oriented checklist."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def mode_profile() -> dict[str, Any]:
|
||||
return {
|
||||
"mode": "service_delivery",
|
||||
"priority_intents": ["approve_action", "ask_proof", "want_meetings"],
|
||||
"card_types_first": ["approval_needed", "proof", "meeting_prep"],
|
||||
"sla_reminder_ar": "التسليم حسب نافذة الـ Pilot المتفق عليها؛ لا live send افتراضياً.",
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
"""Map intents to recommended service_ids and bundles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
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 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 {
|
||||
"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 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,
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
"""In-memory operator sessions — MVP; replace with DB or revenue_memory later."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
_SESSIONS: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def new_session_id() -> str:
|
||||
return f"op_{uuid.uuid4().hex[:16]}"
|
||||
|
||||
|
||||
def get_session(session_id: str) -> dict[str, Any] | None:
|
||||
return _SESSIONS.get(session_id)
|
||||
|
||||
|
||||
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
|
||||
@ -0,0 +1,40 @@
|
||||
"""Safe Tool Gateway matrix — execution modes per tool (deterministic)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
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"
|
||||
|
||||
# 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 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 list_tool_matrix() -> dict[str, Any]:
|
||||
return {"tools": [{**{"tool_id": k}, **v} for k, v in _TOOL_MATRIX.items()], "demo": True}
|
||||
@ -0,0 +1,25 @@
|
||||
"""Suggest next service / bundle from catalog upgrade_path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
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 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 {
|
||||
"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,
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
"""WhatsApp payload shapes (text templates only — no live send)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def render_daily_brief_stub() -> dict[str, Any]:
|
||||
return {
|
||||
"channel": "whatsapp",
|
||||
"format": "text_stub",
|
||||
"body_ar": (
|
||||
"موجز Dealix (مسودة): ٣ قرارات مقترحة — راجع لوحة الموافقات. "
|
||||
"لا يُرسل هذا النص تلقائياً من المنصة في MVP."
|
||||
),
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
"""Simple workflow state machine: intake → draft → pending_approval → proof."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.autonomous_service_operator import session_state as ss
|
||||
|
||||
|
||||
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",
|
||||
}
|
||||
key = (state, ev)
|
||||
new_state = transitions.get(key, state)
|
||||
return ss.upsert_session(session_id, {"workflow_state": new_state, "last_event": ev})
|
||||
@ -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"]
|
||||
|
||||
@ -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}
|
||||
6
dealix/auto_client_acquisition/customer_ops/__init__.py
Normal file
6
dealix/auto_client_acquisition/customer_ops/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Customer operations: onboarding, SLA, connectors, incidents (deterministic stubs)."""
|
||||
|
||||
from auto_client_acquisition.customer_ops.onboarding_checklist import build_onboarding_checklist
|
||||
from auto_client_acquisition.customer_ops.sla_tracker import build_sla_summary
|
||||
|
||||
__all__ = ["build_onboarding_checklist", "build_sla_summary"]
|
||||
@ -0,0 +1,43 @@
|
||||
"""Connector readiness matrix (demo / staging oriented)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_connector_status() -> dict[str, Any]:
|
||||
return {
|
||||
"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 واتفاق العميل.",
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
"""Weekly cadence for pilots (deterministic)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_weekly_cadence() -> dict[str, Any]:
|
||||
return {
|
||||
"weekly_touchpoints_ar": [
|
||||
"مراجعة كروت الموافقة المعلقة.",
|
||||
"تحديث Proof Pack (مسودات، موافقات، مخاطر ممنوعة).",
|
||||
"مكالمة قصيرة أو تحديث كتابي مع صاحب القرار.",
|
||||
"قراءة مؤشرات القنوات (ردود، شكاوى، opt-out = صفر مطلوب).",
|
||||
],
|
||||
"metrics_to_track": [
|
||||
"demos_booked",
|
||||
"pilots_active",
|
||||
"drafts_approved",
|
||||
"risks_blocked",
|
||||
"proof_events",
|
||||
],
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
"""Incident routing stub (no paging, no secrets)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_incident_playbook() -> dict[str, Any]:
|
||||
return {
|
||||
"steps_ar": [
|
||||
"تصنيف الخطورة (P0–P3) وفق وصف الحادث.",
|
||||
"إيقاف أي إجراء live إن وُجد حتى التحقق.",
|
||||
"توثيق الوقت، التأثير، والخطوات المتخذة (بدون أسرار أو PII خام).",
|
||||
"إشعار العميل بلغة واضحة وخطة تعافي.",
|
||||
"مراجعة لاحقة وتحديث السياسات/الاختبارات إن لزم.",
|
||||
],
|
||||
"contacts_placeholder_ar": "يُحدَّد في العقد: بريد دعم + قناة طوارئ للـ P0.",
|
||||
}
|
||||
|
||||
|
||||
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"}}
|
||||
@ -0,0 +1,23 @@
|
||||
"""Onboarding checklist for pilots (deterministic, no external calls)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_onboarding_checklist(service_id: str | None = None) -> dict[str, Any]:
|
||||
sid = (service_id or "growth_starter").strip() or "growth_starter"
|
||||
return {
|
||||
"service_id": sid,
|
||||
"steps_ar": [
|
||||
"تأكيد الهدف (عملاء جدد / قائمة / شراكات / تشغيل يومي).",
|
||||
"جمع بيانات الشركة: القطاع، المدينة، العرض، رابط الموقع.",
|
||||
"تحديد القنوات المتاحة (إيميل، واتساب opt-in، CRM، نماذج).",
|
||||
"رفع قائمة اختيارية أو تأكيد عدم وجود قائمة.",
|
||||
"مراجعة سياسة الموافقات وعدم الإرسال الحي الافتراضي.",
|
||||
"تشغيل أول مهمة (تشخيص أو 10 فرص أو List Intelligence).",
|
||||
"تسليم أول Proof Pack أو ملخص أثر خلال النافذة المتفق عليها.",
|
||||
],
|
||||
"approval_required": True,
|
||||
"live_send_default": False,
|
||||
}
|
||||
37
dealix/auto_client_acquisition/customer_ops/sla_tracker.py
Normal file
37
dealix/auto_client_acquisition/customer_ops/sla_tracker.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""SLA summary for support tiers (static policy text + JSON for API)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_sla_summary() -> dict[str, Any]:
|
||||
return {
|
||||
"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 عند التوسع.",
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
"""Map issue text to priority bucket (deterministic heuristics)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,6 @@
|
||||
"""Growth curator — deterministic grading and weekly report (no live sends)."""
|
||||
|
||||
from auto_client_acquisition.growth_curator.curator_report import build_weekly_curator_report
|
||||
from auto_client_acquisition.growth_curator.message_curator import grade_message
|
||||
|
||||
__all__ = ["build_weekly_curator_report", "grade_message"]
|
||||
@ -0,0 +1,19 @@
|
||||
"""Weekly curator narrative — Arabic, deterministic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_weekly_curator_report(context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or {}
|
||||
return {
|
||||
"week_label_ar": str(ctx.get("week_label_ar") or "أسبوع تجريبي"),
|
||||
"summary_ar": "تمت مراجعة رسائل المسودات: أرشفة ٣ نسخ ضعيفة، دمج تشابه في عنوانين، تحسين CTA في ٤ رسائل.",
|
||||
"actions_ar": [
|
||||
"حافظ على سؤال واحد لكل رسالة واتساب.",
|
||||
"قلل الوعود المطلقة في البريد.",
|
||||
"فعّل متابعة ٤٨ ساعة بعد الاجتماع فقط بعد الموافقة.",
|
||||
],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
"""Grade Arabic outreach messages — heuristic MVP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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
|
||||
notes.append("قصير جداً — أضف سياقاً ولماذا الآن.")
|
||||
if len(text) > 900:
|
||||
score -= 10
|
||||
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))
|
||||
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}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Mission / playbook curation suggestions — no automatic deletion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def curate_missions_weekly() -> dict[str, Any]:
|
||||
return {
|
||||
"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}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Playbook merge hints — deterministic stub."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,22 @@
|
||||
"""Curated list of playbook/message skills — deterministic inventory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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}
|
||||
1
dealix/auto_client_acquisition/integrations/__init__.py
Normal file
1
dealix/auto_client_acquisition/integrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Draft-only integration helpers (no OAuth, no network) — Growth Control Tower."""
|
||||
@ -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.",
|
||||
}
|
||||
@ -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.",
|
||||
}
|
||||
53
dealix/auto_client_acquisition/integrations/moyasar_draft.py
Normal file
53
dealix/auto_client_acquisition/integrations/moyasar_draft.py
Normal 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}¤cy={currency}&description={invoice_ref}"
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"source": params.get("source") if isinstance(params.get("source"), dict) else {"type": "creditcard"},
|
||||
"description": str(params.get("description_ar") or params.get("description") or "Dealix draft"),
|
||||
"metadata": {"invoice_reference": invoice_ref},
|
||||
}
|
||||
return {
|
||||
"approval_required": True,
|
||||
"valid": True,
|
||||
"errors": [],
|
||||
"payload": payload,
|
||||
"payment_link_draft": payment_link_draft,
|
||||
"invoice_reference": invoice_ref,
|
||||
"note_ar": "مسودة تحقق فقط — لا يُنشأ دفع عبر Moyasar في MVP؛ الرابط للعرض الشكلي فقط.",
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
"""Intelligence layer — deterministic JSON, optional bridge to innovation."""
|
||||
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"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",
|
||||
]
|
||||
@ -0,0 +1,35 @@
|
||||
"""Deterministic action graph: signal → policy → approval → outcome → proof (demo)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
"""Executive board brief — Arabic headline + bullets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_board_brief(snapshot: dict[str, Any] | None) -> dict[str, Any]:
|
||||
sn = snapshot or {}
|
||||
title = str(sn.get("title_ar") or "موجز أسبوعي — Dealix")
|
||||
return {
|
||||
"title_ar": title,
|
||||
"bullets_ar": [
|
||||
"زخم الصفقات: مستقر مع حاجة لمتابعة ما بعد الاجتماع.",
|
||||
"الامتثال: لا إرسال جماعي حتى اكتمال opt-in.",
|
||||
"الفرص: ركّز على قطاعين بدل تشتيت ICP.",
|
||||
],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Safe competitive move suggestions (display-only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_competitive_moves(sector: str | None = None) -> dict[str, Any]:
|
||||
sec = sector or "عام"
|
||||
return {
|
||||
"sector": sec,
|
||||
"moves_ar": [
|
||||
"تضييق رسالة القيمة على نتيجة واحدة قابلة للقياس لكل عميل.",
|
||||
"عرض تجربة ٧ أيام مع حدود واضحة للنطاق وتقرير إثبات أسبوعي.",
|
||||
"تفعيل غرفة صفقة مشتركة مع SLA داخلي ٢٤ ساعة للرد.",
|
||||
],
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
"""In-memory decision snippets for demos — replace with DB in production."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_STORE: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
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()
|
||||
@ -0,0 +1,82 @@
|
||||
"""Growth profile from JSON — no LLM required for MVP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
_DEFAULT_BLOCKED = (
|
||||
"cold_whatsapp",
|
||||
"auto_linkedin_dm",
|
||||
"bulk_send_without_approval",
|
||||
"purchased_list_bulk",
|
||||
)
|
||||
|
||||
|
||||
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 ["صناع القرار المالي", "مدراء المشتريات", "العمليات"]
|
||||
@ -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}
|
||||
@ -0,0 +1,51 @@
|
||||
"""Mission catalog — references innovation missions without duplicating HTTP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.innovation.growth_missions import list_growth_missions
|
||||
|
||||
|
||||
_MISSIONS_META: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": "first_10_opportunities",
|
||||
"title_ar": "10 فرص في 10 دقائق",
|
||||
"canonical_http": "POST /api/v1/intelligence/missions/first-10-opportunities",
|
||||
"safety_rules_ar": ["لا واتساب بارد", "موافقة على المسودات"],
|
||||
"required_integrations": [],
|
||||
},
|
||||
{
|
||||
"id": "revenue_leak_rescue",
|
||||
"title_ar": "إنقاذ تسريب إيراد",
|
||||
"canonical_http": "GET /api/v1/intelligence/command-feed/demo",
|
||||
"safety_rules_ar": ["مراجعة المصدر", "حد أسبوعي للمسودات"],
|
||||
"required_integrations": ["gmail_draft"],
|
||||
},
|
||||
{
|
||||
"id": "partnership_sprint",
|
||||
"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": "إعادة تفعيل عملاء",
|
||||
"canonical_http": "POST /api/v1/platform/contacts/import-preview",
|
||||
"safety_rules_ar": ["تصنيف القائمة", "opt-out فوري"],
|
||||
"required_integrations": [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def list_mission_catalog() -> dict[str, Any]:
|
||||
gm = list_growth_missions()
|
||||
return {"missions": _MISSIONS_META, "innovation_growth_missions": gm, "demo": True}
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,23 @@
|
||||
"""Simple numeric opportunity scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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 {
|
||||
"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,
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
"""Revenue DNA snapshot — structured JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_revenue_dna(context: dict[str, Any] | None) -> dict[str, Any]:
|
||||
ctx = context or {}
|
||||
return {
|
||||
"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,
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Trust score 0–100 from simple signals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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}
|
||||
5
dealix/auto_client_acquisition/launch_ops/__init__.py
Normal file
5
dealix/auto_client_acquisition/launch_ops/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Launch operations — private beta offer, demo script, outreach, go/no-go."""
|
||||
|
||||
from auto_client_acquisition.launch_ops.private_beta import build_private_beta_offer
|
||||
|
||||
__all__ = ["build_private_beta_offer"]
|
||||
52
dealix/auto_client_acquisition/launch_ops/demo_flow.py
Normal file
52
dealix/auto_client_acquisition/launch_ops/demo_flow.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""12-minute demo script structure for founders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_demo_script() -> dict[str, Any]:
|
||||
return {
|
||||
"duration_minutes": 12,
|
||||
"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", "الخطوة التالية"],
|
||||
},
|
||||
],
|
||||
"closing_line_ar": "لا نعد نتائج مضمونة — نعد مسودات وموافقات وتقارير قياس.",
|
||||
"demo": True,
|
||||
}
|
||||
41
dealix/auto_client_acquisition/launch_ops/go_no_go.py
Normal file
41
dealix/auto_client_acquisition/launch_ops/go_no_go.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Deterministic go/no-go for private beta launch checklist."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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)),
|
||||
}
|
||||
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 {
|
||||
"go": go,
|
||||
"checks": checks,
|
||||
"blockers": blockers,
|
||||
"warnings_ar": warnings_ar,
|
||||
"verdict_ar": "جاهز للبيتا الخاصة (كود وعمليات أساسية)" if go else "موقوف — راجع قائمة الـ blockers.",
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
"""Readiness scorecard for launch — simple weighted score."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from auto_client_acquisition.launch_ops.go_no_go import evaluate_go_no_go
|
||||
|
||||
|
||||
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 {
|
||||
"readiness_score": score,
|
||||
"status": status,
|
||||
"go_no_go": g,
|
||||
"summary_ar": f"درجة الجاهزية {score}/١٠٠ — {status}.",
|
||||
"demo": True,
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
"""First outreach batch — templates for warm outbound (manual send only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_first_twenty_outreach() -> dict[str, Any]:
|
||||
return {
|
||||
"count": 20,
|
||||
"disclaimer_ar": "هذه قوالب للنسخ اليدوي — لا يُرسل من Dealix تلقائياً.",
|
||||
"messages": [
|
||||
{
|
||||
"id": 1,
|
||||
"audience_ar": "مؤسس B2B",
|
||||
"subject_ar": "تجربة بيتا — ١٠ فرص خلال أسبوع",
|
||||
"body_ar": (
|
||||
"هلا [الاسم]، نجرب Dealix كمدير نمو عربي: نطلع لك فرصاً مناسبة، "
|
||||
"نكتب رسائل عربية، وأنت توافق قبل أي تواصل. مهتم نعرض لك عينة قصيرة؟"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"audience_ar": "وكالة تسويق",
|
||||
"subject_ar": "Pilot مشترك مع وكالة",
|
||||
"body_ar": (
|
||||
"هلا [الاسم]، نبحث وكالة واحدة لتجربة Dealix على عميل حقيقي: "
|
||||
"فرص + مسودات + Proof Pack. يناسبكم ديمو ١٥ دقيقة؟"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"audience_ar": "شركة تدريب",
|
||||
"subject_ar": "فرص شركات في قطاعكم",
|
||||
"body_ar": (
|
||||
"هلا [الاسم]، Dealix يساعد فرق التدريب تطلع فرص B2B مع سبب «لماذا الآن» "
|
||||
"ومسودات عربية. نقدر نرسل لكم تشخيصاً مجانياً مختصراً؟"
|
||||
),
|
||||
},
|
||||
{
|
||||
"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": "إذا التوقيت مو مناسب، أقدر أرجع بعد أسبوعين — أو أغلق الملف عندكم برسالة «لا شكراً».",
|
||||
},
|
||||
],
|
||||
"note_ar": "كرّر وأكيّف الرسائل ٤–٦ لباقي الـ ٢٠ جهة — نفس النبرة الدافئة.",
|
||||
"demo": True,
|
||||
}
|
||||
28
dealix/auto_client_acquisition/launch_ops/private_beta.py
Normal file
28
dealix/auto_client_acquisition/launch_ops/private_beta.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Private beta commercial offer — deterministic copy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_private_beta_offer() -> dict[str, Any]:
|
||||
return {
|
||||
"title_ar": "Dealix — البيتا الخاصة",
|
||||
"tagline_ar": "مدير نمو عربي: فرص، مسودات، موافقة، Proof — بدون إرسال حي افتراضياً.",
|
||||
"included_ar": [
|
||||
"تشخيص نمو مجاني أو مدفوع حسب الاتفاق",
|
||||
"سباق ١٠ فرص أو ذكاء قائمة (حسب الحالة)",
|
||||
"كروت موافقة عربية (أزرار ≤٣)",
|
||||
"Proof Pack أسبوعي تجريبي",
|
||||
],
|
||||
"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,
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
"""Meeting intelligence — transcript text to brief/follow-up (no Calendar insert)."""
|
||||
|
||||
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",
|
||||
"summarize_transcript_text",
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
"""Deal risk hint from simple signals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,15 @@
|
||||
"""Post-meeting follow-up draft (Arabic)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
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"
|
||||
"ننتظر تأكيدكم للمتابعة."
|
||||
)
|
||||
return {"subject_ar": "متابعة — ملخص الاجتماع والخطوة التالية", "body_ar": body, "approval_required": True, "demo": True}
|
||||
@ -0,0 +1,29 @@
|
||||
"""Pre-meeting brief from company/contact context."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
) -> dict[str, Any]:
|
||||
c = company or {}
|
||||
p = contact or {}
|
||||
o = opportunity or {}
|
||||
return {
|
||||
"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,
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
"""Extract objection-like phrases from transcript text — keyword MVP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_KEYWORDS = ("ميزانية", "غالي", "لاحقاً", "نراجع", "ليس أولوية", "تكامل", "أمان", "عقد", "منافس")
|
||||
|
||||
|
||||
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}
|
||||
@ -0,0 +1,18 @@
|
||||
"""Parse plain-text transcript lines into a short Arabic summary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
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 {
|
||||
"bullets_ar": bullets,
|
||||
"word_count": word_count,
|
||||
"demo": True,
|
||||
"note_ar": "ملخص من نص خام — ربط Google Meet API لاحقاً مع OAuth.",
|
||||
}
|
||||
5
dealix/auto_client_acquisition/model_router/__init__.py
Normal file
5
dealix/auto_client_acquisition/model_router/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Model routing hints by task type — configuration only, no vendor calls."""
|
||||
|
||||
from auto_client_acquisition.model_router.task_router import list_tasks, route_task
|
||||
|
||||
__all__ = ["list_tasks", "route_task"]
|
||||
@ -0,0 +1,16 @@
|
||||
"""Static provider labels for routing display."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_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"]},
|
||||
]
|
||||
|
||||
|
||||
def list_providers() -> dict[str, Any]:
|
||||
return {"providers": list(_PROVIDERS), "demo": True}
|
||||
30
dealix/auto_client_acquisition/model_router/task_router.py
Normal file
30
dealix/auto_client_acquisition/model_router/task_router.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Map task types to suggested provider + cost class — deterministic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_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},
|
||||
}
|
||||
|
||||
|
||||
def list_tasks() -> dict[str, Any]:
|
||||
return {"task_types": sorted(_ROUTES.keys()), "demo": True}
|
||||
|
||||
|
||||
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}
|
||||
23
dealix/auto_client_acquisition/platform_services/__init__.py
Normal file
23
dealix/auto_client_acquisition/platform_services/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Platform Services — Growth Control Tower (policy, inbox, catalog, no live sends)."""
|
||||
|
||||
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__ = [
|
||||
"ActionLedger",
|
||||
"EventType",
|
||||
"build_proof_summary",
|
||||
"evaluate_action",
|
||||
"event_to_inbox_card",
|
||||
"execute_tool",
|
||||
"get_action_ledger",
|
||||
"get_service_catalog",
|
||||
"list_channels",
|
||||
"validate_event",
|
||||
]
|
||||
@ -0,0 +1,41 @@
|
||||
"""In-memory decision log for platform tools (MVP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
_counter = itertools.count(1)
|
||||
_lock = threading.Lock()
|
||||
_entries: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
class ActionLedger:
|
||||
"""Thread-safe append-only ledger."""
|
||||
|
||||
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 recent(self, limit: int = 50) -> list[dict[str, Any]]:
|
||||
with _lock:
|
||||
return list(_entries[-limit:])
|
||||
|
||||
|
||||
_ledger = ActionLedger()
|
||||
|
||||
|
||||
def get_action_ledger() -> ActionLedger:
|
||||
return _ledger
|
||||
@ -0,0 +1,82 @@
|
||||
"""Deterministic policy — no network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from core.config.settings import get_settings
|
||||
|
||||
PolicyState = Literal["approved", "blocked", "approval_required", "review"]
|
||||
|
||||
|
||||
def evaluate_action(
|
||||
*,
|
||||
action: str,
|
||||
channel_id: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
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 {}
|
||||
reason_ar = ""
|
||||
state: PolicyState = "approval_required"
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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 action in ("send", "send_live", "external_send", "smtp_send"):
|
||||
state = "approval_required"
|
||||
reason_ar = "أي إرسال خارجي يتطلب موافقة بشرية في هذا الإصدار."
|
||||
|
||||
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 action in ("draft_only", "draft_message", "draft_email"):
|
||||
state = "approved"
|
||||
reason_ar = "مسودة داخلية — مسموح للعرض فقط."
|
||||
|
||||
return {"state": state, "reason_ar": reason_ar or "قرار سياسة افتراضي.", "action": action, "channel_id": channel_id}
|
||||
@ -0,0 +1,69 @@
|
||||
"""Channel capabilities — registered-only social channels, no OAuth in MVP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_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"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def list_channels() -> dict[str, Any]:
|
||||
return {"channels": list(_CHANNEL_DEFS), "demo": True}
|
||||
@ -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.",
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
"""Unified event types and field validation — no transport."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
_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 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}
|
||||
@ -0,0 +1,22 @@
|
||||
"""Deterministic identity merge demo — no external graph DB."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
@ -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}
|
||||
@ -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}
|
||||
@ -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",
|
||||
},
|
||||
}
|
||||
@ -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
Loading…
Reference in New Issue
Block a user