95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
import re
|
|
from typing import Optional, Tuple
|
|
|
|
from app.operation_noise import BOUNCE_NDR_PATTERNS, SYSTEM_SENDER_PATTERNS
|
|
from app.action_llm_client import decide_action_with_llm
|
|
from app.action_mapper import map_action_decision
|
|
from app.schemas import ActionDecision, ActionResult, AnalyzeRequest, UsageInfo
|
|
|
|
|
|
# v4.9.0 keeps NDR/Bounce patterns centralized in app.operation_noise.
|
|
# Regression terms: Your message couldn't be delivered, Recipient wasn't found, Office 365.
|
|
_BOUNCE_PATTERNS = SYSTEM_SENDER_PATTERNS + BOUNCE_NDR_PATTERNS
|
|
|
|
_NO_INTEREST_PATTERNS = [
|
|
r"\bn[aã]o\s+temos\s+(?:na\s+nossa\s+)?frota\s+(?:de\s+)?ve[ií]culos\s+el[eé]tricos\b",
|
|
r"\bn[aã]o\s+temos\s+(?:ve[ií]culos|viaturas|carros)\s+el[eé]tricos\b",
|
|
r"\bn[aã]o\s+possu[ií]mos\s+(?:ve[ií]culos|viaturas|carros)\s+el[eé]tricos\b",
|
|
r"\bn[aã]o\s+(?:estamos|temos)\s+interessad[oa]s?\b",
|
|
r"\bn[aã]o\s+(?:necessitamos|precisamos)\b",
|
|
r"\bn[aã]o\s+se\s+aplica\b",
|
|
r"\bsem\s+interesse\b",
|
|
r"\bsem\s+necessidade\b",
|
|
]
|
|
|
|
|
|
def _normalize_text(value: str) -> str:
|
|
text = str(value or "").casefold()
|
|
text = re.sub(r"<[^>]+>", " ", text)
|
|
text = re.sub(r"https?://\S+", " ", text)
|
|
text = re.sub(r"\s+", " ", text).strip()
|
|
return text
|
|
|
|
|
|
def detect_deterministic_action(request: AnalyzeRequest) -> Optional[ActionDecision]:
|
|
"""Regras de alta confiança antes do LLM.
|
|
|
|
Usadas só para respostas inequívocas que devem gerar uma ação operacional
|
|
própria. A regra evita classificar recusas explícitas como SUPPORT ou
|
|
REVIEW_MANUALLY.
|
|
"""
|
|
text = _normalize_text("\n".join([request.previous_context or "", request.last_customer_message or ""]))
|
|
if not text:
|
|
return None
|
|
|
|
for pattern in _BOUNCE_PATTERNS:
|
|
if re.search(pattern, text, flags=re.I):
|
|
return ActionDecision(
|
|
action_code="IGNORE_BOUNCE",
|
|
note="Mensagem automática de devolução/erro de entrega. Ignorar no fluxo operacional.",
|
|
confidence=0.99,
|
|
)
|
|
|
|
for pattern in _NO_INTEREST_PATTERNS:
|
|
if re.search(pattern, text, flags=re.I):
|
|
return ActionDecision(
|
|
action_code="MARK_NO_INTEREST",
|
|
note="Cliente indicou que não tem interesse/necessidade atual.",
|
|
confidence=0.95,
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
async def decide_action(request: AnalyzeRequest) -> Tuple[ActionDecision, ActionResult, UsageInfo, str]:
|
|
"""Triagem de mensagens.
|
|
|
|
Mantém LLM para a maioria dos casos, mas aplica regras determinísticas de
|
|
alta confiança para intenções críticas/inequívocas que devem ser estáveis.
|
|
"""
|
|
deterministic = detect_deterministic_action(request)
|
|
if deterministic:
|
|
result = map_action_decision(deterministic)
|
|
usage = UsageInfo(
|
|
id=None,
|
|
model="deterministic-rule",
|
|
provider="rule",
|
|
prompt_tokens=0,
|
|
completion_tokens=0,
|
|
total_tokens=0,
|
|
cost=0.0,
|
|
)
|
|
return deterministic, result, usage, "rule"
|
|
|
|
decision, usage, _raw = await decide_action_with_llm(request)
|
|
result = map_action_decision(decision)
|
|
|
|
# Garante que a decisão persistida reflete o código normalizado/permitido.
|
|
decision = ActionDecision(
|
|
action_code=result.action_code,
|
|
note=decision.note or result.note,
|
|
confidence=decision.confidence,
|
|
)
|
|
|
|
return decision, result, usage, "llm"
|