#!/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())