102 lines
4.3 KiB
Python
Executable File
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())
|