system-prompts-and-models-o.../salesflow-saas/backend/app/services/strategic_deals/deal_negotiator.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

480 lines
19 KiB
Python

"""
Deal Negotiator — Autonomous AI negotiator for B2B deals.
المفاوض الذكي: مفاوض آلي بالذكاء الاصطناعي للصفقات بين الشركات
"""
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.strategic_deal import (
StrategicDeal, CompanyProfile, DealStatus, DealType,
)
from app.services.llm.provider import get_llm
logger = logging.getLogger("dealix.strategic_deals.negotiator")
# ── Models ───────────────────────────────────────────────────────────────────
class NegotiationStrategy(BaseModel):
"""Strategy configuration for autonomous negotiation."""
target_terms: dict = {} # Ideal outcome
acceptable_range: dict = {} # Min/max for each variable
walk_away_point: dict = {} # Absolute limits / deal breakers
priorities: list[str] = [] # Ordered from most to least important
style: str = "collaborative" # collaborative, competitive, accommodating
@dataclass
class NegotiationRound:
"""Result of a single negotiation round."""
round_number: int = 0
action: str = "" # opening_offer, counter_offer, acceptance, rejection, escalation
our_terms: dict = field(default_factory=dict)
their_terms: dict = field(default_factory=dict)
message_ar: str = ""
message_en: str = ""
concessions_made: list[str] = field(default_factory=list)
concessions_gained: list[str] = field(default_factory=list)
within_range: bool = True
confidence: float = 0.0
timestamp: str = ""
# ── Escalation thresholds ────────────────────────────────────────────────────
ESCALATION_VALUE_SAR = 500_000 # Deals above this need human oversight
MAX_AUTO_ROUNDS = 5 # After this many rounds, escalate
STALL_THRESHOLD = 3 # Same terms repeated this many times = stall
class DealNegotiator:
"""
Autonomous AI negotiator that handles B2B deal negotiations.
Respects Saudi business culture: relationship-first, patience, mutual respect.
مفاوض ذكي يحترم ثقافة الأعمال السعودية
"""
def __init__(self):
self.llm = get_llm()
# ── Start Negotiation ────────────────────────────────────────────────────
async def start_negotiation(
self,
deal_id,
strategy: NegotiationStrategy,
db: AsyncSession,
) -> NegotiationRound:
"""
Generate opening offer based on strategy and Saudi negotiation culture.
إنشاء العرض الأولي بناءً على الاستراتيجية وثقافة التفاوض السعودية
"""
deal = await self._load_deal(deal_id, db)
initiator = await self._load_profile(deal.initiator_profile_id, db)
target_name = deal.target_company_name or "الطرف الآخر"
if deal.target_profile_id:
target = await self._load_profile(deal.target_profile_id, db)
target_name = target.company_name if target else target_name
context = f"""Deal: {deal.deal_title}
Deal type: {deal.deal_type}
Our company: {initiator.company_name}
Target company: {target_name}
Our offer: {deal.our_offer or 'not specified'}
Our need: {deal.our_need or 'not specified'}
Strategy style: {strategy.style}
Target terms: {json.dumps(strategy.target_terms, ensure_ascii=False)}
Priorities: {', '.join(strategy.priorities)}"""
style_guidance = {
"collaborative": "ابدأ بعرض عادل ومتوازن يظهر الرغبة في شراكة طويلة المدى",
"competitive": "ابدأ بعرض طموح لكن معقول مع ترك مساحة للتفاوض",
"accommodating": "ابدأ بعرض سخي يظهر حسن النية والرغبة في بناء علاقة",
}
system_prompt = f"""أنت مفاوض أعمال سعودي محترف. أنشئ عرضاً أولياً للصفقة.
التوجيه: {style_guidance.get(strategy.style, style_guidance['collaborative'])}
Important Saudi negotiation culture:
- Start with relationship building (سلامات واستفسار عن الأحوال)
- Show respect for the other party
- Be patient, don't rush to numbers
- Present win-win framing
Return JSON:
{{
"opening_terms": {{"key": "value for each negotiable item"}},
"message_ar": "رسالة العرض الأولي بالعربي (تبدأ بالسلام والتحية)",
"message_en": "Opening message in English",
"rationale_ar": "مبررات العرض",
"confidence": 0.0 to 1.0
}}"""
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 {}
now_str = datetime.now(timezone.utc).isoformat()
round_data = NegotiationRound(
round_number=1,
action="opening_offer",
our_terms=result.get("opening_terms", strategy.target_terms),
their_terms={},
message_ar=result.get("message_ar", ""),
message_en=result.get("message_en", ""),
concessions_made=[],
concessions_gained=[],
within_range=True,
confidence=result.get("confidence", 0.5),
timestamp=now_str,
)
# Update deal
deal.proposed_terms = round_data.our_terms
deal.status = DealStatus.NEGOTIATING.value
deal.ai_confidence = round_data.confidence
history = list(deal.negotiation_history or [])
history.append({
"round": round_data.round_number,
"action": round_data.action,
"our_terms": round_data.our_terms,
"their_terms": round_data.their_terms,
"message_ar": round_data.message_ar,
"timestamp": now_str,
})
deal.negotiation_history = history
await db.flush()
logger.info("Started negotiation for deal %s (round 1)", deal_id)
return round_data
# ── Handle Counter-Offer ─────────────────────────────────────────────────
async def handle_counter_offer(
self,
deal_id,
their_terms: dict,
db: AsyncSession,
) -> NegotiationRound:
"""
Analyze a counter-offer and generate a response.
تحليل عرض مضاد وتوليد رد مناسب
"""
deal = await self._load_deal(deal_id, db)
history = list(deal.negotiation_history or [])
round_num = len(history) + 1
# Get the latest strategy from proposed terms
our_latest = deal.proposed_terms or {}
context = f"""Deal: {deal.deal_title}
Deal type: {deal.deal_type}
Our latest terms: {json.dumps(our_latest, ensure_ascii=False)}
Their counter-offer: {json.dumps(their_terms, ensure_ascii=False)}
Negotiation history (rounds): {len(history)}
Estimated value SAR: {deal.estimated_value_sar or 'unknown'}"""
system_prompt = """أنت مفاوض أعمال سعودي محترف. الطرف الآخر قدم عرضاً مضاداً.
حلل العرض وقرر:
1. هل العرض مقبول؟
2. هل نحتاج عرض مضاد؟
3. هل يجب رفع الموضوع لإنسان؟
Saudi culture: never be aggressive. Show appreciation for their offer before countering.
Handle common responses: "غالي" (too expensive), "نبي نفكر" (need to think), "عندنا عرض ثاني" (we have another offer)
Return JSON:
{
"action": "accept/counter/reject/escalate",
"counter_terms": {"key": "value"},
"message_ar": "الرد بالعربي",
"message_en": "Response in English",
"concessions_made": ["what we gave up"],
"concessions_gained": ["what we got"],
"within_acceptable_range": true/false,
"confidence": 0.0 to 1.0,
"analysis_ar": "تحليل العرض المضاد"
}"""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
json_mode=True,
temperature=0.3,
)
result = llm_response.parse_json() or {}
action = result.get("action", "counter")
now_str = datetime.now(timezone.utc).isoformat()
round_data = NegotiationRound(
round_number=round_num,
action=action,
our_terms=result.get("counter_terms", our_latest),
their_terms=their_terms,
message_ar=result.get("message_ar", ""),
message_en=result.get("message_en", ""),
concessions_made=result.get("concessions_made", []),
concessions_gained=result.get("concessions_gained", []),
within_range=result.get("within_acceptable_range", True),
confidence=result.get("confidence", 0.5),
timestamp=now_str,
)
# Update deal state
if action == "accept":
deal.agreed_terms = their_terms
deal.status = DealStatus.TERM_SHEET.value
elif action == "reject":
deal.status = DealStatus.CLOSED_LOST.value
deal.closed_at = datetime.now(timezone.utc)
else:
deal.proposed_terms = round_data.our_terms
deal.ai_confidence = round_data.confidence
history.append({
"round": round_data.round_number,
"action": action,
"our_terms": round_data.our_terms,
"their_terms": their_terms,
"message_ar": round_data.message_ar,
"timestamp": now_str,
})
deal.negotiation_history = history
await db.flush()
logger.info("Handled counter-offer for deal %s (round %d, action=%s)", deal_id, round_num, action)
return round_data
# ── Generate Negotiation Response ────────────────────────────────────────
async def generate_response(
self,
deal_id,
message: str,
db: AsyncSession,
) -> str:
"""
Generate a culturally appropriate Arabic/English negotiation response.
توليد رد تفاوضي مناسب ثقافياً بالعربي أو الإنجليزي
"""
deal = await self._load_deal(deal_id, db)
history = deal.negotiation_history or []
# Summarize negotiation context
history_summary = ""
for h in history[-3:]: # Last 3 rounds
history_summary += f"Round {h.get('round', '?')}: {h.get('action', '?')} - {h.get('message_ar', '')[:100]}\n"
context = f"""Deal: {deal.deal_title}
Deal type: {deal.deal_type}
Current status: {deal.status}
Our proposed terms: {json.dumps(deal.proposed_terms or {}, ensure_ascii=False)}
Recent history:
{history_summary}
Incoming message from counter-party: {message}"""
system_prompt = """أنت مفاوض أعمال سعودي محترف. رد على رسالة الطرف الآخر بشكل مناسب.
Rules:
- إذا الرسالة بالعربي، رد بالعربي
- إذا الرسالة بالإنجليزي، رد بالإنجليزي
- كن محترماً وودوداً دائماً
- لا تكن عدوانياً أبداً
- حافظ على العلاقة حتى لو الصفقة لم تنجح
Handle:
- "غالي" → أظهر المرونة واعرض بدائل
- "نبي نفكر" → أعطهم وقت مع اقتراح موعد متابعة
- "عندنا عرض ثاني" → أبرز المميزات الفريدة بدون تقليل المنافسين
- "ما يناسبنا" → اسأل عن التفاصيل واعرض تعديلات
Return the response message directly as text (not JSON)."""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
temperature=0.5,
)
response_text = llm_response.content.strip()
# Log the exchange in negotiation history
history = list(deal.negotiation_history or [])
history.append({
"round": len(history) + 1,
"action": "response",
"their_message": message[:500],
"our_response": response_text[:500],
"timestamp": datetime.now(timezone.utc).isoformat(),
})
deal.negotiation_history = history
await db.flush()
logger.info("Generated negotiation response for deal %s", deal_id)
return response_text
# ── Should Escalate? ─────────────────────────────────────────────────────
async def should_escalate(
self,
deal_id,
db: AsyncSession,
) -> bool:
"""
Determine if a human should take over the negotiation.
تحديد ما إذا كان يجب تصعيد التفاوض لإنسان
"""
deal = await self._load_deal(deal_id, db)
history = deal.negotiation_history or []
round_count = len(history)
# Rule 1: High-value deals
value = float(deal.estimated_value_sar or 0)
if value > ESCALATION_VALUE_SAR:
logger.info("Escalation: deal %s value (%.0f SAR) exceeds threshold", deal_id, value)
return True
# Rule 2: Too many rounds without resolution
if round_count >= MAX_AUTO_ROUNDS:
logger.info("Escalation: deal %s reached %d rounds", deal_id, round_count)
return True
# Rule 3: Stalled negotiation (same terms repeating)
if round_count >= STALL_THRESHOLD:
recent_terms = [
json.dumps(h.get("our_terms", {}), sort_keys=True)
for h in history[-STALL_THRESHOLD:]
]
if len(set(recent_terms)) == 1:
logger.info("Escalation: deal %s stalled for %d rounds", deal_id, STALL_THRESHOLD)
return True
# Rule 4: Low confidence
if deal.ai_confidence is not None and deal.ai_confidence < 0.3:
logger.info("Escalation: deal %s AI confidence too low (%.2f)", deal_id, deal.ai_confidence)
return True
# Rule 5: Counter-party explicitly requested human contact
if history:
last_msg = (history[-1].get("their_message", "") or "").lower()
human_keywords = [
"أبي أكلم شخص", "أبي أكلم المدير", "ابي اتكلم مع انسان",
"speak to someone", "talk to a person", "human", "manager",
"مدير", "مسؤول",
]
for kw in human_keywords:
if kw in last_msg:
logger.info("Escalation: deal %s counter-party requested human", deal_id)
return True
return False
# ── Generate Term Sheet ──────────────────────────────────────────────────
async def generate_term_sheet(
self,
deal_id,
db: AsyncSession,
) -> dict:
"""
Generate a formal Arabic term sheet from agreed terms.
إنشاء ورقة شروط رسمية بالعربي من الشروط المتفق عليها
"""
deal = await self._load_deal(deal_id, db)
initiator = await self._load_profile(deal.initiator_profile_id, db)
target_name = deal.target_company_name or "الطرف الثاني"
target_cr = ""
if deal.target_profile_id:
target = await self._load_profile(deal.target_profile_id, db)
if target:
target_name = target.company_name
target_cr = target.cr_number or ""
terms = deal.agreed_terms or deal.proposed_terms or {}
context = f"""Parties:
- Party A: {initiator.company_name} (CR: {initiator.cr_number or 'N/A'})
- Party B: {target_name} (CR: {target_cr or 'N/A'})
Deal: {deal.deal_title}
Deal type: {deal.deal_type}
Agreed terms: {json.dumps(terms, ensure_ascii=False)}
Estimated value: {deal.estimated_value_sar or 'TBD'} SAR
Our offer: {deal.our_offer or 'N/A'}
Our need: {deal.our_need or 'N/A'}"""
system_prompt = """أنت مستشار قانوني سعودي متخصص في صياغة أوراق الشروط.
أنشئ ورقة شروط رسمية باللغة العربية.
Return JSON:
{
"title_ar": "عنوان ورقة الشروط",
"date": "التاريخ",
"parties": [
{"name": "اسم الطرف", "role": "الطرف الأول/الطرف الثاني", "cr": "رقم السجل التجاري"}
],
"preamble_ar": "مقدمة ورقة الشروط",
"scope_ar": "نطاق الاتفاقية",
"terms": [
{"title_ar": "عنوان البند", "description_ar": "تفاصيل البند"}
],
"obligations_party_a_ar": ["التزامات الطرف الأول"],
"obligations_party_b_ar": ["التزامات الطرف الثاني"],
"financial_terms_ar": "الشروط المالية",
"duration_ar": "مدة الاتفاقية",
"termination_ar": "شروط الإنهاء",
"confidentiality_ar": "شرط السرية",
"dispute_resolution_ar": "حل النزاعات",
"governing_law_ar": "القانون الحاكم: أنظمة المملكة العربية السعودية",
"next_steps_ar": ["الخطوات التالية"]
}"""
llm_response = await self.llm.complete(
system_prompt=system_prompt,
user_message=context,
json_mode=True,
temperature=0.2,
)
term_sheet = llm_response.parse_json() or {}
# Update deal status
if deal.status == DealStatus.NEGOTIATING.value:
deal.status = DealStatus.TERM_SHEET.value
await db.flush()
logger.info("Generated term sheet for deal %s", deal_id)
return term_sheet
# ── Helpers ──────────────────────────────────────────────────────────────
async def _load_deal(self, deal_id, db: AsyncSession) -> StrategicDeal:
result = await db.execute(select(StrategicDeal).where(StrategicDeal.id == deal_id))
deal = result.scalar_one_or_none()
if not deal:
raise ValueError(f"Deal {deal_id} not found")
return deal
async def _load_profile(self, profile_id, db: AsyncSession) -> Optional[CompanyProfile]:
if not profile_id:
return None
result = await db.execute(select(CompanyProfile).where(CompanyProfile.id == profile_id))
return result.scalar_one_or_none()