Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
101
scripts/repair_send_info_quote_requests.py
Executable file
101
scripts/repair_send_info_quote_requests.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user