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"