ReplyFlow / app /services /leads.py
nvikou's picture
Initial commit
582520a verified
Raw History Blame Contribute Delete
4.18 kB
"""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]