system-prompts-and-models-o.../dealix/auto_client_acquisition/growth_operator/targeting.py
Dealix Builder 8942c6e84c feat(growth-operator): Arabic Growth Operator — 10 modules + 16 endpoints + 50 tests
Builds the full Saudi Autonomous Revenue OS surface as 10 deterministic
modules + a 16-endpoint router under /api/v1/growth-operator/.
Approval-first: every outbound is draft. No live send / charge / calendar
insert from this layer.

MODULES (auto_client_acquisition/growth_operator/)

1. client_profile.py — ClientGrowthProfile + Saudi-default approval
   + compliance rules (no cold WhatsApp, blocked keywords, weekly cap,
   quiet_hours_riyadh)
2. contact_importer.py — normalize_phone (Saudi E.164),
   dedupe_contacts (richer-record-wins), classify_contact_source
   (existing/inbound/event/referral/old_lead/cold/unknown), detect_opt_out
   (Arabic + English markers), summarize_import (dashboard report)
3. contactability.py — score_contactability returns
   safe/needs_review/blocked with Arabic reasons; default policy:
   no cold WhatsApp without lawful basis (PDPL Art.5)
4. targeting.py — segment_contacts, rank_targets (filters unsafe),
   recommend_top_10, why_now_stub (deterministic, sector-aware)
5. message_planner.py — draft_arabic_message (Saudi tone, 4-sector
   opener bank, no overhyped phrases, always pending_approval),
   draft_followup (4 outcome modes), draft_objection_response
   (6 indexed Saudi B2B objections with score_delta + next_action)
6. partnership_planner.py — 6 partner types catalog
   (agency / consultant / integrator / crm / community / influencer)
   + suggest_partner_types (size/sector aware) + draft_partner_outreach
   + partner_scorecard (platinum/gold/silver/bronze)
7. meeting_planner.py — build_meeting_agenda (15/20-30/45+ min slot
   plans), build_calendar_draft (Google Calendar shape, live_inserted=False,
   conferenceData for Meet, Asia/Riyadh timezone), build_post_meeting_followup
8. payment_offer.py — sar_to_halalas, build_moyasar_payment_link_draft
   (full payload + in-chat message + 4-plan catalog, live_charged=False)
9. proof_pack.py — build_weekly_proof_pack with grade A+/A/B/C/D,
   activity/money/quality/best-of sections, dynamic next_week_plan_ar,
   markdown export
10. mission_planner.py — 6 GROWTH_MISSIONS (first_10_opportunities 
    kill feature, recover_stalled_deals, partnership_sprint,
    safe_whatsapp_campaign, meeting_booking_sprint, list_cleanup);
    list_missions() + run_mission()

ROUTER (api/routers/growth_operator.py) — 16 endpoints

POST /contacts/import-preview · POST /contactability/score
POST /targets/top-10 · POST /messages/draft · POST /messages/followup
POST /messages/objection-response · POST /partners/suggest
POST /partners/outreach · POST /partners/scorecard
POST /meetings/draft · POST /meetings/post-followup
POST /payment-offer/draft · GET /missions · POST /missions/{id}/run
GET /proof-pack/demo · POST /profile

WIRING: api/main.py adds growth_operator import + router include
(positioned after personal_operator, before public).

DOCS

- docs/ARABIC_GROWTH_OPERATOR_FULL_SPEC.md (NEW): 20-section vision +
  customer-type table + upload flow + contactability rules +
  WhatsApp/Gmail/Calendar/Moyasar drafts + 6 missions + 16-endpoint
  catalog + competitive comparison + beta readiness checklist

TESTS — 50 passing on Python 3.10 venv

tests/unit/test_growth_operator.py covers:
- Phone normalization across 5 input formats including invalid
- Dedupe richer-record invariant
- Source classification (existing/inbound/event/cold/unknown)
- Opt-out detection (Arabic + English notes + status)
- Import summary aggregation
- Contactability: opt-out blocked, cold WhatsApp blocked,
  unknown→needs_review, existing→safe, inbound→safe
- Bulk contactability summary
- Top-10 filtering (unsafe excluded), max-cap enforcement
- Segment buckets
- Arabic message: pending_approval invariant + Arabic content
  + no overhyped phrases (banned list)
- Followup approval invariant
- Objection response: known + unknown→diagnostic
- Partner suggestions size-aware (SMB→agency/consultant/community)
- Partner outreach approval invariant
- Partner unknown type returns error
- Partner scorecard tier ordering
- Meeting agenda + calendar draft (live_inserted=False) +
  Asia/Riyadh timezone + post-followup pending
- Payment: halalas conversion (1 SAR=100), negative raises,
  draft NEVER charges (live_charged=False), unknown plan→error
- Proof pack: grade range + structure + markdown export
- Missions: first_10_opportunities present + kill feature ID
  + run mission known/unknown
- Profile: demo specialized + partial not specialized
  + default compliance blocks 'ضمان 100' + no_cold_whatsapp_without_lawful_basis

VERIFICATION
- 527 unit tests pass (was 477; +50 growth_operator)
- 2 skipped (provider smoke needs API keys)
- AST green on all 13 new files
- Approval invariant holds across every drafting function

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:33:11 +03:00

147 lines
5.3 KiB
Python

"""
Targeting — turn a list of safe contacts into a ranked Top-N with Why-Now.
Pure functions; no LLM calls. Heuristic ranking:
- existing customer / inbound lead: highest base score
- event lead: strong recency boost
- old lead with last_contacted_at: medium
- referral: high trust
- unknown / cold: filtered out unless explicitly allowed
"""
from __future__ import annotations
import hashlib
from typing import Any
from auto_client_acquisition.growth_operator.contact_importer import (
classify_contact_source,
detect_opt_out,
normalize_phone,
)
from auto_client_acquisition.growth_operator.contactability import (
score_contactability,
)
# ── Segments ─────────────────────────────────────────────────────
_SEGMENT_BASE_SCORE: dict[str, float] = {
"existing_customer": 90.0,
"inbound_lead": 85.0,
"referral": 80.0,
"event_lead": 75.0,
"old_lead": 60.0,
"unknown": 35.0,
"cold_list": 20.0,
}
def segment_contacts(contacts: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
"""Group contacts into segments using classify_contact_source."""
segs: dict[str, list[dict[str, Any]]] = {
"existing_customer": [],
"inbound_lead": [],
"referral": [],
"event_lead": [],
"old_lead": [],
"unknown": [],
"cold_list": [],
"blocked_or_invalid": [],
}
for c in contacts:
if detect_opt_out(c) or not normalize_phone(c.get("phone")):
segs["blocked_or_invalid"].append(c)
continue
src = classify_contact_source(c)
segs.setdefault(src, []).append(c)
return segs
# ── Why-Now stub (deterministic; placeholder until live signals) ──
_WHY_NOW_TEMPLATES_AR: dict[str, str] = {
"existing_customer": "علاقة قائمة — توقيت ممتاز لعرض expansion / upsell.",
"inbound_lead": "أبدى اهتماماً مؤخراً — السرعة (≤24 ساعة) ترفع التحويل.",
"referral": "قادم بإحالة موثوقة — احترام السياق المهني.",
"event_lead": "تواصل من فعالية مؤخراً — نافذة 30 يوم ذهبية.",
"old_lead": "lead سابق — انتهز موسم/حدث جديد للعودة.",
"unknown": "مصدر غير محدد — يحتاج warm-up + توثيق lawful basis.",
"cold_list": "قائمة باردة — لا تواصل قبل توثيق العلاقة.",
}
def why_now_stub(contact: dict[str, Any], *, sector_hint: str = "") -> dict[str, Any]:
"""
Deterministic Why-Now stub.
In production, this is replaced by a live signal-driven explainer
that reads market_intelligence + company website diff + jobs.
"""
src = classify_contact_source(contact)
company = contact.get("company") or contact.get("name") or ""
rationale = _WHY_NOW_TEMPLATES_AR.get(src, "تواصل قياسي — راجع المصدر قبل الإرسال.")
if sector_hint and src in ("event_lead", "inbound_lead", "old_lead"):
rationale += f" · مرتبط بقطاع {sector_hint}."
# Synthetic stable score (testable, no entropy)
seed = hashlib.md5(f"{company}|{src}|{sector_hint}".encode()).digest()
bonus = (seed[0] % 21) - 10 # -10..+10
return {
"rationale_ar": rationale,
"score_modifier": bonus,
"source": src,
}
# ── Ranking ──────────────────────────────────────────────────────
def rank_targets(
contacts: list[dict[str, Any]],
*,
sector_hint: str = "",
channel: str = "whatsapp",
require_safe: bool = True,
) -> list[dict[str, Any]]:
"""
Score every contact, optionally filter to safe-only, return sorted desc.
Each item in the result is the original contact + score + why_now + decision.
"""
out: list[dict[str, Any]] = []
for c in contacts:
decision = score_contactability(c, channel=channel)
if require_safe and decision["label"] != "safe":
continue
why = why_now_stub(c, sector_hint=sector_hint)
base = _SEGMENT_BASE_SCORE.get(why["source"], 30.0)
score = max(0.0, min(100.0, base + why["score_modifier"]))
out.append({
**c,
"fit_score": round(score, 1),
"why_now": why,
"contactability": decision,
})
out.sort(key=lambda x: x["fit_score"], reverse=True)
return out
def recommend_top_10(
contacts: list[dict[str, Any]],
*,
sector_hint: str = "",
channel: str = "whatsapp",
) -> dict[str, Any]:
"""The Top-10 view consumed by the dashboard's Growth Radar tile."""
ranked = rank_targets(
contacts, sector_hint=sector_hint, channel=channel, require_safe=True,
)
top = ranked[:10]
return {
"channel": channel,
"sector_hint": sector_hint,
"candidates_evaluated": len(contacts),
"candidates_safe": len(ranked),
"top": top,
"recommendation_ar": (
f"اخترنا أعلى {len(top)} فرصة آمنة من قائمة {len(contacts)} "
f"بعد فلترة المخاطرة. كل واحدة بحالة pending_approval."
),
}