"""Conversational lead qualification.""" from __future__ import annotations import re from typing import Any, Optional from app.models import Lead EMAIL_RE = re.compile( r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+" ) PHONE_RE = re.compile(r"\+?\d[\d\s\-()]{7,}\d") BUDGET_HINTS = ( "budget", "€", "$", "eur", "usd", "rub", "₽", "тысяч", "euros", "dollars", ) NEED_HINTS = ( "need", "besoin", "нужн", "want", "looking", "cherche", "ищ", "order", "commande", "заказ", ) CONTACT_HINTS = ( "email", "mail", "phone", "téléphone", "телефон", "whatsapp", "call", "звон", ) def lead_to_dict(lead: Optional[Lead]) -> dict[str, Any]: """Serialize lead fields for prompts/UI.""" if lead is None: return { "name": None, "need": None, "budget": None, "contact_channel": None, "email": None, "phone": None, "complete": False, } return { "name": lead.name, "need": lead.need, "budget": lead.budget, "contact_channel": lead.contact_channel, "email": lead.email, "phone": lead.phone, "complete": lead.complete, } def update_lead_from_message( lead: Lead, message: str, previous_bot: Optional[str] = None, ) -> Lead: """Extract lead fields from the latest user message.""" text = message.strip() lowered = text.lower() bot = (previous_bot or "").lower() email = EMAIL_RE.search(text) if email and not lead.email: lead.email = email.group(0) if not lead.contact_channel: lead.contact_channel = "email" phone = PHONE_RE.search(text) if phone and not lead.phone: lead.phone = re.sub(r"\s+", "", phone.group(0)) if not lead.contact_channel: lead.contact_channel = "phone" if not lead.name and ( "name" in bot or "nom" in bot or "зовут" in bot or "зовут" in bot or "call me" in lowered or "je m'appelle" in lowered or "меня зовут" in lowered ): lead.name = _extract_name(text) if not lead.name and _looks_like_name(text): lead.name = text.strip()[:80] if not lead.budget and ( any(h in lowered for h in BUDGET_HINTS) or "budget" in bot or "бюджет" in bot ): lead.budget = text[:64] if not lead.need and ( any(h in lowered for h in NEED_HINTS) or "need" in bot or "besoin" in bot or "помощь" in bot ): lead.need = text[:500] if not lead.contact_channel and ( any(h in lowered for h in CONTACT_HINTS) or "reach" in bot or "contacter" in bot or "связаться" in bot ): if "whatsapp" in lowered: lead.contact_channel = "whatsapp" elif "mail" in lowered or "email" in lowered: lead.contact_channel = "email" elif "phone" in lowered or "tél" in lowered or "телефон" in lowered: lead.contact_channel = "phone" else: lead.contact_channel = text[:64] lead.complete = all( [ lead.name, lead.need, lead.budget, lead.contact_channel, ] ) return lead def _looks_like_name(text: str) -> bool: words = text.strip().split() if not (1 <= len(words) <= 3): return False if any(ch.isdigit() for ch in text): return False if len(text) > 40: return False return text[:1].isupper() or all( w[:1].isupper() for w in words if w ) def _extract_name(text: str) -> str: patterns = [ r"(?:je m'appelle|my name is|i am|i'm|меня зовут)\s+(.+)", r"(?:call me)\s+(.+)", ] lowered = text.strip() for pattern in patterns: match = re.search(pattern, lowered, flags=re.I) if match: return match.group(1).strip(" .,!")[:80] return text.strip()[:80]