Files
clientflow_backend/scripts/repair_send_info_quote_requests.py

102 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Reclassify pending SEND_INFO tasks that are explicit quote requests.
Read-only by default. Use --apply to update matching tasks to SEND_QUOTE.
This fixes cases where a customer writes "necessito de um orçamento" but the
triage produced SEND_INFO. The reply assistant can still generate a textual
proposal when no ORC/document exists.
"""
from __future__ import annotations
import argparse
import re
from typing import Any, Dict, List
from sqlalchemy import text
from app.db import engine
from app.task_service import reclassify_task
QUOTE_RE = re.compile(
r"\b(necessito|preciso|agradeco|agradeço|envie|enviar|mande|mandar|solicito|peço|peco|pretendo|queria|gostaria)\b.{0,80}\b(orcamento|orçamento|cotacao|cotação|proposta|quote|quotation)\b|\b(orcamento|orçamento|cotacao|cotação|proposta)\b.{0,80}\b(preco|preço|valor|valores|quantidade|equipamento|carregador)",
re.IGNORECASE | re.DOTALL,
)
INFO_ONLY_RE = re.compile(r"\b(ficha tecnica|ficha técnica|manual|datasheet|foto|fotografia|imagem|catalogo|catálogo|caracteristicas|características|dimensoes|dimensões)\b", re.IGNORECASE)
def _rows(limit: int) -> List[Dict[str, Any]]:
with engine.begin() as conn:
return [dict(r) for r in conn.execute(text("""
SELECT
t.id::text AS task_id,
t.action_code,
t.route,
t.status,
t.action,
t.note,
t.conversation_id,
t.opportunity_id::text,
COALESCE(o.title, '') AS opportunity_title,
COALESCE(o.customer_name, '') AS opportunity_customer_name,
COALESCE(
NULLIF(re.payload->'conversation'->'additional_attributes'->>'mail_subject', ''),
NULLIF(re.payload->'content_attributes'->'email'->>'subject', ''),
''
) AS subject,
COALESCE(NULLIF(m.clean_body, ''), NULLIF(m.raw_body, ''), NULLIF(re.payload->>'content', ''), '') AS body
FROM tasks t
LEFT JOIN messages m ON m.id = t.message_id
LEFT JOIN raw_events re ON re.id = t.raw_event_id
LEFT JOIN opportunities o ON o.id = t.opportunity_id
WHERE t.status = 'pending'
AND upper(COALESCE(t.action_code, '')) = 'SEND_INFO'
ORDER BY t.created_at DESC
LIMIT :limit
"""), {"limit": int(limit or 200)}).mappings().all()]
def _match_reason(row: Dict[str, Any]) -> str:
text_value = "\n".join(str(row.get(k) or "") for k in ["subject", "body", "action", "note"])
if INFO_ONLY_RE.search(text_value) and not QUOTE_RE.search(text_value):
return ""
m = QUOTE_RE.search(text_value)
if not m:
return ""
excerpt = " ".join(text_value[m.start():m.end()].split())[:220]
return f"pedido explícito de orçamento/cotação/proposta: {excerpt}"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true")
parser.add_argument("--limit", type=int, default=200)
args = parser.parse_args()
rows = _rows(args.limit)
matches = [(row, _match_reason(row)) for row in rows]
matches = [(row, reason) for row, reason in matches if reason]
print(f"apply={args.apply} scanned={len(rows)} matches={len(matches)}")
for row, reason in matches:
print()
print(f"task={row['task_id']} opp={row.get('opportunity_id') or '-'} conversation={row.get('conversation_id') or '-'}")
print(f"title={row.get('opportunity_title') or '-'} customer={row.get('opportunity_customer_name') or '-'}")
print(f"reason={reason}")
if args.apply:
result = reclassify_task(
task_id=row["task_id"],
new_action_code="SEND_QUOTE",
reason="Reclassificado automaticamente: pedido explícito de orçamento/cotação/proposta. SEND_QUOTE pode ser proposta textual sem anexo quando ainda não existe ORC.",
reclassified_by="repair_send_info_quote_requests",
reopen=True,
)
print(f"updated={result.get('ok')} status={result.get('status')}")
if not args.apply:
print("Dry-run: nenhuma alteração aplicada.")
return 0
if __name__ == "__main__":
raise SystemExit(main())