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