system-prompts-and-models-o.../dealix/auto_client_acquisition/email/daily_targeting.py
2026-05-01 14:03:52 +03:00

224 lines
9.6 KiB
Python

"""
Daily Targeting Agent — the autonomous revenue brain.
Runs every morning at 7am Asia/Riyadh (via /api/v1/automation/daily-targeting/run).
Process:
1. Pull candidates from Saudi directory accounts + Maps + previous queue.
2. Exclude opt_outs, suppressed, bounced, recently-contacted, high-risk, no allowed_use.
3. Enrich top scored candidates (crawl + tech detect + emails) — capped to budget.
4. Re-score with fresh signals.
5. Pick TOP 50 across diversified sectors.
6. For each: generate Khaliji email (LLM if Groq available, else template).
7. Queue with approval_required=True.
8. Return daily plan + exact follow-up schedule.
LLM usage:
- Personalization upgrade per account (one short LLM call to write angle).
- Reply classification on incoming emails.
- Both gracefully degrade to rules-mode if no LLM key.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any
from auto_client_acquisition.pipelines.scoring import (
compute_data_quality,
compute_lead_score,
)
log = logging.getLogger(__name__)
@dataclass
class DailyTargetingResult:
generated_at: str
target_date: str
candidates_evaluated: int
excluded_opt_out: int
excluded_suppressed: int
excluded_recently_contacted: int
excluded_high_risk: int
excluded_no_allowed_use: int
excluded_personal_email_phone_only: int
selected_count: int
selected: list[dict[str, Any]] = field(default_factory=list)
sector_split: dict[str, int] = field(default_factory=dict)
daily_email_limit: int = 50
notes: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"generated_at": self.generated_at,
"target_date": self.target_date,
"candidates_evaluated": self.candidates_evaluated,
"excluded": {
"opt_out": self.excluded_opt_out,
"suppressed": self.excluded_suppressed,
"recently_contacted": self.excluded_recently_contacted,
"high_risk": self.excluded_high_risk,
"no_allowed_use": self.excluded_no_allowed_use,
"personal_email_only": self.excluded_personal_email_phone_only,
},
"selected_count": self.selected_count,
"selected": self.selected,
"sector_split": self.sector_split,
"daily_email_limit": self.daily_email_limit,
"notes": self.notes,
}
# ── Sector-specific message angle map ─────────────────────────────
ANGLE_MAP: dict[str, str] = {
"real_estate_developer": (
"كل lead عقاري متأخر دقيقة = احتمال خسارة العميل لمنافس. Dealix يرد خلال 45 ثانية بالعربي الخليجي، "
"يأخذ الميزانية + الموقع + الموعد، ويسلم العميل المؤهل لمندوبكم."
),
"construction": (
"بدل ما تضيع طلبات تسعير المشاريع بين واتساب + اتصالات + إيميلات، Dealix يجمع المواصفات + الميزانية + "
"المهلة الزمنية لكل طلب، ويفرز الجاهز للتسعير عن الباقي."
),
"hospitality": (
"حجوزات MICE + إفطار/سحور + قاعات = leads عربية تحتاج رد فوري. Dealix يخدم العميل بالعربي ويحجز موعد معاينة."
),
"events": (
"كل lead لقاعة حفل = موسم. Dealix يرد فوراً، يأخذ التاريخ + العدد + الباقة، ويحجز معاينة في تقويم فريقكم."
),
"logistics": (
"RFQ شحن: العميل يطلب عرض، إذا تأخرتم 10 دقائق رحل لمنافس. Dealix يرد بالعربي خلال دقيقة، "
"يجمع الوزن + الوجهة + التاريخ، ويفتح ticket في نظامكم."
),
"restaurant": (
"Dealix يرد على استفسارات التموين + الحجوزات + الفرنشايز بالعربي خلال 45 ثانية، ويفرز الجاد منها للإدارة."
),
"saas": (
"Dealix هو AI sales rep بالعربي الخليجي يتكامل مع HubSpot/Salesforce/Zoho. "
"إذا تبيعون SaaS داخل السعودية، نضمن الرد على inbound leads خلال 45 ثانية."
),
"marketing_agency": (
"Dealix هو AI sales rep بالعربي يتكامل مع HubSpot/Salesforce/Zoho. كشركة تسويق سعودية، لكم خياران: "
"تستخدمونه لعملائكم (resell) → 25% MRR شهرياً، أو تشترون لعملاء وكالتكم. كلاهما revenue share."
),
}
def angle_for(sector: str | None) -> str:
if not sector:
return "Dealix يرد على inbound leads بالعربي الخليجي خلال 45 ثانية."
return ANGLE_MAP.get(sector.lower(),
"Dealix يرد على inbound leads بالعربي الخليجي خلال 45 ثانية.")
def opener_for(priority: str) -> str:
return "السلام عليكم" if priority == "P0" else "مرحباً"
def render_email_template(account: dict[str, Any], priority: str) -> dict[str, str]:
"""Deterministic email template — used as fallback or LLM seed."""
company = (account.get("company_name") or "فريقكم").strip()
angle = angle_for(account.get("sector"))
opener = opener_for(priority)
cta = (
"Pilot 7 أيام بـ 499 ريال — نشتغل على leadsكم نحن، تشوفون النتيجة، ثم تقرّرون. "
"تناسبكم 20 دقيقة هذا الأسبوع؟"
)
body = (
f"{opener} {company}،\n\n"
f"{angle}\n\n"
f"{cta}\n\n"
"سامي\n"
"Dealix — https://dealix.me\n"
"📅 https://calendly.com/sami-assiri11/dealix-demo"
)
subject = f"Dealix — تجربة تأهيل عملاء لـ {company[:60]}"
return {"subject_ar": subject, "body_ar": body}
async def llm_personalize(account: dict[str, Any], base_email: dict[str, str]) -> dict[str, str]:
"""
Optional LLM upgrade — returns the personalized email if Groq available,
else returns base unchanged. Single short call per account.
"""
import asyncio
has_llm = bool(
os.getenv("GROQ_API_KEY") or os.getenv("ANTHROPIC_API_KEY") or os.getenv("OPENAI_API_KEY")
)
if not has_llm:
return base_email
try:
from core.llm.router import get_router
from core.llm.base import Message
except Exception:
return base_email
prompt = (
"أنت محرر إيميلات بيع B2B بالعربي الخليجي السعودي.\n"
f"الشركة: {account.get('company_name')}\n"
f"القطاع: {account.get('sector_ar') or account.get('sector')}\n"
f"المدينة: {account.get('city_ar') or account.get('city')}\n"
f"الإيميل المقترح:\n{base_email['body_ar']}\n\n"
"حسّن جملة 'سبب التواصل' بحيث تذكر شيئاً محدداً عن نشاط الشركة المحتمل في القطاع/المدينة "
"(مثلاً 'عقار في الرياض = مشاريع compounds + شقق سكنية' أو 'فندق في أبها = leads المواسم'). "
"احتفظ بنفس الطول، نفس الخاتمة، نفس الـ CTA. لا تضف وعود غير مذكورة.\n"
"أرجع فقط الـ body المحدّث بدون أي شرح."
)
try:
router = get_router()
resp = await asyncio.wait_for(
router.complete([Message(role="user", content=prompt)], max_tokens=400, temperature=0.4),
timeout=8.0,
)
new_body = (resp.content or "").strip()
if 50 < len(new_body) < 2000 and "Dealix" in new_body:
return {**base_email, "body_ar": new_body, "personalized_by_llm": "true"}
except Exception as exc: # noqa: BLE001
log.warning("llm_personalize_failed account=%s err=%s",
account.get("company_name", "?"), exc)
return base_email
def select_top_n_diversified(
candidates: list[dict[str, Any]],
*,
target_count: int,
sector_caps: dict[str, int] | None = None,
) -> list[dict[str, Any]]:
"""
Pick top-N with sector diversity so we don't blast 50 emails to the same vertical.
Default cap per sector: max(target_count // 4, 10) to ensure variety.
"""
sector_caps = sector_caps or {}
default_cap = max(target_count // 4, 10)
chosen: list[dict[str, Any]] = []
counts: dict[str, int] = {}
# Sort by total_score desc, then DQ desc
sorted_pool = sorted(
candidates,
key=lambda c: (-(c.get("total_score") or 0), -(c.get("data_quality_score") or 0)),
)
for c in sorted_pool:
sec = (c.get("sector") or "other").lower()
cap = sector_caps.get(sec, default_cap)
if counts.get(sec, 0) >= cap:
continue
chosen.append(c)
counts[sec] = counts.get(sec, 0) + 1
if len(chosen) >= target_count:
break
return chosen
def compute_followup_schedule(send_date: datetime) -> dict[str, str]:
"""Return ISO-8601 timestamps for +2/+5/+10/+30 follow-ups."""
return {
"day_2": (send_date + timedelta(days=2)).isoformat(),
"day_5": (send_date + timedelta(days=5)).isoformat(),
"day_10": (send_date + timedelta(days=10)).isoformat(),
"day_30_nurture": (send_date + timedelta(days=30)).isoformat(),
}