system-prompts-and-models-o.../salesflow-saas/backend/app/services/strategic_deals/deal_agent.py
Claude d7a5af9156
feat: Add Strategic Deals Engine — autonomous B2B deal-making system
Revolutionary AI system for autonomous B2B partnerships, negotiations, and deals:

Models (strategic_deal.py - 238 lines):
- CompanyProfile: Rich Saudi company profiles with CR, capabilities, needs
- StrategicDeal: Full deal lifecycle (discovery → negotiation → close)
- DealMatch: AI-generated company matches with scoring

Services (4 files, ~2,060 lines):
- company_profiler.py: Profile creation, AI enrichment, needs/capability analysis
- deal_matcher.py: 6-dimension scoring, semantic matching, barter chain discovery
- deal_negotiator.py: Multi-round Arabic negotiation with cultural awareness
- deal_agent.py: Autonomous outreach via WhatsApp/LinkedIn/Email

API (strategic_deals.py - 681 lines, 16 endpoints):
- Profile management + AI enrichment
- Match discovery + approval
- Deal lifecycle (create → negotiate → proposal → term sheet → close)
- Barter chain scanning
- Analytics dashboard

Deal types: partnership, distribution, franchise, JV, referral, acquisition, barter
Channels: WhatsApp (primary), LinkedIn, Email
Languages: Arabic (Saudi dialect) + English
Cultural: Saudi negotiation norms, relationship-first, face-saving

https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
2026-04-11 09:15:29 +00:00

597 lines
24 KiB
Python

"""
Deal Agent — Autonomous outreach agent for B2B deal discovery and engagement.
وكيل الصفقات: وكيل ذكي مستقل للتواصل واكتشاف الشراكات
"""
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.strategic_deal import (
CompanyProfile, StrategicDeal, DealMatch,
DealStatus, DealChannel, MatchStatus, DealType,
)
from app.services.llm.provider import get_llm
logger = logging.getLogger("dealix.strategic_deals.agent")
# ── WhatsApp outreach templates (Arabic) ─────────────────────────────────────
TEMPLATES = {
"introduction_collaborative": (
"السلام عليكم {contact_name}\n\n"
"أتمنى تكون بخير وعافية. أنا {sender_name} من {company_name}.\n"
"اطلعت على أعمالكم في مجال {target_industry} وعجبني اللي تقدمونه.\n\n"
"عندنا خبرة في {our_capability} ونشوف فرصة تعاون مثمرة بيننا "
"خصوصاً في مجال {collaboration_area}.\n\n"
"هل ممكن نحدد وقت مناسب نتكلم فيه عن إمكانية التعاون؟\n\n"
"تحياتي"
),
"introduction_as_ai": (
"السلام عليكم {contact_name}\n\n"
"أنا مساعد ذكي من شركة {company_name}. فريقنا مهتم بالتعاون معكم "
"بناءً على تحليل التكامل بين خدماتنا.\n\n"
"شركة {company_name} تقدم {our_capability} وشفنا إن عندكم احتياج في "
"هذا المجال.\n\n"
"هل تحبون نرسل لكم تفاصيل أكثر عن فرصة التعاون؟\n\n"
"شكراً لوقتكم"
),
"follow_up_1": (
"مرحباً {contact_name}\n\n"
"تابعت معكم بخصوص موضوع التعاون اللي ذكرناه.\n"
"أبشركم جهزنا مقترح مبدئي يوضح كيف ممكن نستفيد من بعض.\n\n"
"هل تفضلون نرسله عبر الإيميل أو نحدد اجتماع قصير؟\n\n"
"تحياتي"
),
"proposal_summary": (
"حبيت أشاركك ملخص المقترح:\n\n"
"- نوع التعاون: {deal_type_ar}\n"
"- القيمة المتوقعة: {estimated_value}\n"
"- المدة: {duration}\n"
"- المنافع المشتركة: {mutual_benefits}\n\n"
"المقترح الكامل بالمرفق. ننتظر ملاحظاتكم.\n\n"
"شاكرين لكم"
),
"negotiation_counter": (
"شاكرين لكم على الرد والاهتمام {contact_name}.\n\n"
"نقدر وجهة نظركم. بعد دراسة مقترحكم، حبينا نقدم لكم عرض معدل:\n\n"
"{counter_details}\n\n"
"نتمنى يكون العرض مناسب ونتطلع لشراكة ناجحة.\n\n"
"تحياتي"
),
}
DEAL_TYPE_AR = {
"partnership": "شراكة استراتيجية",
"distribution": "توزيع",
"franchise": "امتياز تجاري",
"jv": "مشروع مشترك",
"referral": "إحالة",
"acquisition": "استحواذ",
"barter": "مقايضة",
}
@dataclass
class OutreachResult:
"""Result of an outreach attempt."""
success: bool = False
channel: str = ""
message_sent: str = ""
response_received: Optional[str] = None
interest_level: Optional[str] = None # high, medium, low, none
next_action: str = ""
next_action_ar: str = ""
error: Optional[str] = None
class DealAgent:
"""
Autonomous outreach agent that discovers, contacts, and qualifies B2B partners.
وكيل ذكي مستقل يكتشف ويتواصل ويؤهل الشركاء
"""
def __init__(self):
self.llm = get_llm()
# ── Outreach Campaign ────────────────────────────────────────────────────
async def run_outreach_campaign(
self,
deal_match_id,
channel: str,
db: AsyncSession,
) -> OutreachResult:
"""
Execute multi-step outreach: research, craft intro, send, handle response.
تنفيذ حملة تواصل متعددة الخطوات
"""
# Load match and related profiles
match_result = await db.execute(select(DealMatch).where(DealMatch.id == deal_match_id))
match = match_result.scalar_one_or_none()
if not match:
return OutreachResult(success=False, error="Match not found")
a_result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == match.company_a_id))
company_a = a_result.scalar_one_or_none()
if not company_a:
return OutreachResult(success=False, error="Initiator profile not found")
target_name = match.company_b_name or "الشركة المستهدفة"
target_industry = ""
target_contact = ""
company_b = None
if match.company_b_id:
b_result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == match.company_b_id))
company_b = b_result.scalar_one_or_none()
if company_b:
target_name = company_b.company_name
target_industry = company_b.industry or ""
target_contact = company_b.whatsapp_number or ""
# Step 1: Research the target
research = await self._research_target(company_a, company_b, match)
# Step 2: Craft personalized introduction
style = "as_company" # Default to speaking as the company
intro_message = await self.craft_introduction(
match=match,
channel=channel,
style=style,
db=db,
)
# Step 3: Prepare outreach result (actual sending delegated to channel adapters)
# In production, this would call WhatsApp/LinkedIn/Email service
outreach = OutreachResult(
success=True,
channel=channel,
message_sent=intro_message,
next_action="await_response",
next_action_ar="انتظار الرد من الطرف الآخر",
)
# Step 4: Update match status
match.status = MatchStatus.OUTREACH_SENT.value
await db.flush()
# Step 5: Create a strategic deal from this outreach
deal = StrategicDeal(
tenant_id=company_a.tenant_id,
initiator_profile_id=company_a.id,
target_profile_id=match.company_b_id,
target_company_name=target_name,
deal_type=match.deal_type_suggested or DealType.PARTNERSHIP.value,
deal_title=f"شراكة مع {target_name}",
deal_title_ar=f"شراكة مع {target_name}",
our_offer=research.get("our_value_proposition", ""),
our_need=research.get("what_we_need_from_them", ""),
proposed_terms=match.terms_suggested or {},
status=DealStatus.OUTREACH.value,
channel=channel,
ai_confidence=match.match_score,
negotiation_history=[{
"round": 0,
"action": "outreach",
"channel": channel,
"message": intro_message[:500],
"timestamp": datetime.now(timezone.utc).isoformat(),
}],
)
db.add(deal)
await db.flush()
logger.info(
"Outreach campaign executed for match %s via %s (deal %s created)",
deal_match_id, channel, deal.id,
)
return outreach
# ── Craft Introduction ───────────────────────────────────────────────────
async def craft_introduction(
self,
match: DealMatch,
channel: str,
style: str,
db: AsyncSession,
) -> str:
"""
Generate a personalized Arabic introduction message.
إنشاء رسالة تعريفية عربية مخصصة
"""
a_result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == match.company_a_id))
company_a = a_result.scalar_one_or_none()
target_name = match.company_b_name or "الشركة المستهدفة"
target_industry = ""
target_caps = []
target_needs = []
if match.company_b_id:
b_result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == match.company_b_id))
company_b = b_result.scalar_one_or_none()
if company_b:
target_name = company_b.company_name
target_industry = company_b.industry or ""
target_caps = company_b.capabilities or []
target_needs = company_b.needs or []
elif match.company_b_data:
target_industry = match.company_b_data.get("industry", "")
target_caps = match.company_b_data.get("capabilities", [])
target_needs = match.company_b_data.get("needs", [])
# Channel-specific length guidance
length_guidance = {
"whatsapp": "اكتب رسالة قصيرة ومباشرة (3-5 أسطر). لا تكتب أكثر من 300 حرف.",
"email": "اكتب رسالة مفصلة ومهنية (8-12 سطر) مع سطر موضوع.",
"linkedin": "اكتب رسالة قصيرة ومهنية (4-6 أسطر).",
"in_person": "جهز نقاط حديث مختصرة (5-7 نقاط).",
}
style_guidance = {
"as_company": "تكلم باسم الشركة مباشرة (نحن في شركة X...)",
"as_ai": "كن شفافاً أنك مساعد ذكي (أنا مساعد ذكي من شركة X...)",
}
context = f"""Our company: {company_a.company_name if company_a else 'unknown'}
Our capabilities: {', '.join((company_a.capabilities or [])[:5]) if company_a else 'unknown'}
Target company: {target_name}
Target industry: {target_industry}
Target capabilities: {', '.join(target_caps[:5])}
Target needs: {', '.join(target_needs[:5])}
Match reasons: {', '.join(match.match_reasons or [])}
Match score: {match.match_score}
Suggested deal type: {match.deal_type_suggested}"""
system_prompt = f"""أنت كاتب رسائل أعمال سعودي محترف.
اكتب رسالة تعريفية للتواصل مع شركة محتملة للتعاون.
Channel: {channel}
{length_guidance.get(channel, length_guidance['whatsapp'])}
Style: {style}
{style_guidance.get(style, style_guidance['as_company'])}
قواعد مهمة:
- ابدأ بالسلام
- اذكر سبب التواصل بوضوح
- أبرز نقطة التكامل بين الشركتين
- اختم بطلب واضح (اجتماع، مكالمة، تفاصيل أكثر)
- لا تبالغ في المدح
- كن مهنياً وودوداً
Return the message directly as text (not JSON)."""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
temperature=0.6,
)
message = llm_response.content.strip()
logger.info("Crafted introduction for match %s via %s", match.id, channel)
return message
# ── Handle Response ──────────────────────────────────────────────────────
async def handle_response(
self,
deal_id,
message: str,
channel: str,
db: AsyncSession,
) -> str:
"""
Analyze incoming response and generate appropriate follow-up.
تحليل الرد الوارد وتوليد متابعة مناسبة
"""
deal_result = await db.execute(select(StrategicDeal).where(StrategicDeal.id == deal_id))
deal = deal_result.scalar_one_or_none()
if not deal:
raise ValueError(f"Deal {deal_id} not found")
# Load initiator profile
initiator = None
if deal.initiator_profile_id:
init_result = await db.execute(
select(CompanyProfile).where(CompanyProfile.id == deal.initiator_profile_id)
)
initiator = init_result.scalar_one_or_none()
history_summary = ""
for h in (deal.negotiation_history or [])[-3:]:
history_summary += f"- {h.get('action', '?')}: {h.get('message', '')[:100]}\n"
context = f"""Deal: {deal.deal_title}
Our company: {initiator.company_name if initiator else 'unknown'}
Target: {deal.target_company_name or 'unknown'}
Channel: {channel}
Current status: {deal.status}
Recent conversation:
{history_summary}
Incoming message: {message}"""
system_prompt = """أنت مساعد أعمال سعودي. حلل الرسالة الواردة وحدد نوعها وقدم رداً مناسباً.
أولاً حلل الرسالة:
- اهتمام (interest): الطرف الآخر مهتم
- اعتراض (objection): عنده تحفظات
- سؤال (question): يحتاج معلومات إضافية
- رفض (rejection): غير مهتم
- طلب معلومات (request_for_info): يريد تفاصيل أكثر
ثم اكتب رداً مناسباً:
- إذا مهتم: حدد الخطوة التالية (اجتماع، مقترح)
- إذا متحفظ: عالج التحفظ بلطف
- إذا عنده سؤال: أجب بوضوح
- إذا رافض: اشكره واترك الباب مفتوحاً
- إذا يبي تفاصيل: وعده بإرسالها
Return JSON:
{
"response_type": "interest/objection/question/rejection/request_for_info",
"interest_level": "high/medium/low/none",
"response_message": "الرد بالعربي",
"next_action": "schedule_meeting/send_proposal/send_info/follow_up_later/close",
"next_action_ar": "الخطوة التالية بالعربي"
}"""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
json_mode=True,
temperature=0.4,
)
result = llm_response.parse_json() or {}
response_msg = result.get("response_message", "شكراً لردكم، سنتواصل معكم قريباً.")
interest = result.get("interest_level", "medium")
next_action = result.get("next_action", "follow_up_later")
# Update deal based on response
if interest == "high" or next_action == "schedule_meeting":
deal.status = DealStatus.NEGOTIATING.value
elif interest == "none" or next_action == "close":
deal.status = DealStatus.CLOSED_LOST.value
deal.closed_at = datetime.now(timezone.utc)
# Log in negotiation history
history = list(deal.negotiation_history or [])
history.append({
"round": len(history) + 1,
"action": "response_handling",
"their_message": message[:500],
"our_response": response_msg[:500],
"response_type": result.get("response_type", "unknown"),
"interest_level": interest,
"next_action": next_action,
"channel": channel,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
deal.negotiation_history = history
await db.flush()
logger.info(
"Handled response for deal %s: type=%s, interest=%s",
deal_id, result.get("response_type"), interest,
)
return response_msg
# ── Generate Proposal ────────────────────────────────────────────────────
async def generate_proposal(
self,
deal_id,
db: AsyncSession,
) -> dict:
"""
Generate a full Arabic business proposal document.
إنشاء مقترح أعمال عربي كامل
"""
deal_result = await db.execute(select(StrategicDeal).where(StrategicDeal.id == deal_id))
deal = deal_result.scalar_one_or_none()
if not deal:
raise ValueError(f"Deal {deal_id} not found")
initiator = None
if deal.initiator_profile_id:
init_result = await db.execute(
select(CompanyProfile).where(CompanyProfile.id == deal.initiator_profile_id)
)
initiator = init_result.scalar_one_or_none()
target_name = deal.target_company_name or "الطرف الآخر"
target = None
if deal.target_profile_id:
t_result = await db.execute(
select(CompanyProfile).where(CompanyProfile.id == deal.target_profile_id)
)
target = t_result.scalar_one_or_none()
if target:
target_name = target.company_name
context = f"""Our company: {initiator.company_name if initiator else 'unknown'}
Our industry: {initiator.industry if initiator else 'unknown'}
Our capabilities: {', '.join((initiator.capabilities or [])[:8]) if initiator else 'unknown'}
Target company: {target_name}
Target industry: {target.industry if target else 'unknown'}
Target capabilities: {', '.join((target.capabilities or [])[:8]) if target else 'unknown'}
Target needs: {', '.join((target.needs or [])[:8]) if target else 'unknown'}
Deal: {deal.deal_title}
Deal type: {deal.deal_type}
Our offer: {deal.our_offer or 'TBD'}
Our need: {deal.our_need or 'TBD'}
Proposed terms: {json.dumps(deal.proposed_terms or {}, ensure_ascii=False)}
Estimated value: {deal.estimated_value_sar or 'TBD'} SAR"""
system_prompt = """أنت كاتب مقترحات أعمال سعودي محترف.
أنشئ مقترح أعمال شامل ومهني باللغة العربية.
Return JSON:
{
"title_ar": "عنوان المقترح",
"executive_summary_ar": "الملخص التنفيذي (3-5 جمل)",
"about_us_ar": "نبذة عن شركتنا",
"understanding_your_needs_ar": "فهمنا لاحتياجاتكم",
"proposed_solution_ar": "الحل المقترح",
"our_capabilities_ar": ["قدرة 1", "قدرة 2"],
"mutual_benefits_ar": ["منفعة مشتركة 1", "منفعة مشتركة 2"],
"deal_structure_ar": "هيكل الصفقة",
"financial_overview_ar": "النظرة المالية",
"timeline_ar": [
{"phase_ar": "المرحلة", "duration_ar": "المدة", "deliverables_ar": "المخرجات"}
],
"success_metrics_ar": ["مؤشر نجاح 1", "مؤشر نجاح 2"],
"risks_and_mitigations_ar": [
{"risk_ar": "المخاطرة", "mitigation_ar": "التخفيف"}
],
"next_steps_ar": ["خطوة 1", "خطوة 2"],
"closing_statement_ar": "الخاتمة"
}"""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
json_mode=True,
temperature=0.4,
)
proposal = llm_response.parse_json() or {}
logger.info("Generated proposal for deal %s", deal_id)
return proposal
# ── Discovery Scan ───────────────────────────────────────────────────────
async def run_discovery_scan(
self,
profile_id,
deal_type: Optional[str],
db: AsyncSession,
) -> list[DealMatch]:
"""
Full autonomous scan: analyze profile, find matches, generate outreach plan.
فحص مستقل كامل: تحليل الملف، إيجاد مطابقات، تجهيز خطة تواصل
"""
from app.services.strategic_deals.company_profiler import CompanyProfiler
from app.services.strategic_deals.deal_matcher import DealMatcher
profiler = CompanyProfiler()
matcher = DealMatcher()
# Step 1: Enrich profile if needed
prof_result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == profile_id))
profile = prof_result.scalar_one_or_none()
if not profile:
raise ValueError(f"Profile {profile_id} not found")
if not profile.capabilities or len(profile.capabilities) < 2:
logger.info("Discovery scan: enriching profile %s first", profile_id)
await profiler.enrich_profile(profile_id, db)
# Step 2: Analyze capabilities if thin
if not profile.capabilities or len(profile.capabilities) < 3:
await profiler.analyze_capabilities(profile_id, db)
# Step 3: Find matches
matches = await matcher.find_matches(
profile_id=profile_id,
deal_type=deal_type,
db=db,
limit=10,
)
# Step 4: Generate deal structure suggestions for top matches
for match in matches[:3]:
try:
await matcher.suggest_deal_structure(match.id, db)
except Exception as e:
logger.warning("Could not suggest structure for match %s: %s", match.id, e)
# Step 5: Generate Arabic summary
if matches:
summary_parts = [f"تم العثور على {len(matches)} فرصة شراكة محتملة:"]
for i, m in enumerate(matches[:5], 1):
target_name = m.company_b_name or "شركة"
if m.company_b_id:
b_res = await db.execute(
select(CompanyProfile).where(CompanyProfile.id == m.company_b_id)
)
b_prof = b_res.scalar_one_or_none()
if b_prof:
target_name = b_prof.company_name
reasons = ", ".join((m.match_reasons or [])[:2])
summary_parts.append(
f"{i}. {target_name} (نسبة التوافق: {m.match_score:.0%}) — {reasons}"
)
summary = "\n".join(summary_parts)
logger.info("Discovery scan summary:\n%s", summary)
logger.info("Discovery scan complete for profile %s: %d matches", profile_id, len(matches))
return matches
# ── Private Helpers ──────────────────────────────────────────────────────
async def _research_target(
self,
company_a: CompanyProfile,
company_b: Optional[CompanyProfile],
match: DealMatch,
) -> dict:
"""Research the target company to personalize outreach."""
target_name = company_b.company_name if company_b else (match.company_b_name or "unknown")
target_industry = company_b.industry if company_b else ""
target_caps = ", ".join((company_b.capabilities or [])[:5]) if company_b else ""
target_needs = ", ".join((company_b.needs or [])[:5]) if company_b else ""
context = f"""Our company: {company_a.company_name}
Our capabilities: {', '.join((company_a.capabilities or [])[:5])}
Our needs: {', '.join((company_a.needs or [])[:5])}
Target: {target_name}
Industry: {target_industry}
Capabilities: {target_caps}
Needs: {target_needs}
Match score: {match.match_score}
Match reasons: {', '.join(match.match_reasons or [])}"""
system_prompt = """أنت باحث أعمال. حلل الشركة المستهدفة وجهز نقاط للتواصل.
Return JSON:
{
"our_value_proposition": "ما نقدمه لهم بجملة واحدة",
"what_we_need_from_them": "ما نحتاجه منهم بجملة واحدة",
"key_talking_points_ar": ["نقطة حوار 1", "نقطة حوار 2"],
"potential_objections_ar": ["اعتراض محتمل 1"],
"recommended_approach_ar": "النهج الموصى به"
}"""
try:
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
json_mode=True,
temperature=0.3,
fast=True,
)
return llm_response.parse_json() or {}
except Exception as e:
logger.warning("Target research failed: %s", e)
return {
"our_value_proposition": "",
"what_we_need_from_them": "",
"key_talking_points_ar": [],
"potential_objections_ar": [],
"recommended_approach_ar": "",
}