mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
224 lines
9.6 KiB
Python
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(),
|
|
}
|