Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
117
scripts/meli_questions_sla.py
Normal file
117
scripts/meli_questions_sla.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MeLi questions SLA sweep for a tenant.
|
||||
|
||||
Lists unanswered MercadoLibre buyer questions older than N hours (the ones
|
||||
that hurt the seller's response score / reputation), oldest first, so the
|
||||
operator can answer them before the SLA is blown.
|
||||
|
||||
Auto-answering is separate and OPT-IN:
|
||||
* It only runs when the tenant has `meli_auto_answer_questions = true`.
|
||||
* `--auto-answer` sends the high-confidence, template-based answers built by
|
||||
services/question_ai (never free-form LLM text) through
|
||||
marketplace_external_service.auto_answer_questions().
|
||||
* Without `--auto-answer` the script only reports; it never sends anything.
|
||||
|
||||
Note: MercadoLibre returns the question status as UNANSWERED / ANSWERED. The
|
||||
service now normalizes it to lowercase on upsert, and the queries here are
|
||||
case-insensitive either way.
|
||||
|
||||
Usage (from /home/Autopartes):
|
||||
python3 scripts/meli_questions_sla.py --tenant 31
|
||||
python3 scripts/meli_questions_sla.py --tenant 31 --max-age-hours 48
|
||||
python3 scripts/meli_questions_sla.py --tenant 31 --auto-answer
|
||||
python3 scripts/meli_questions_sla.py --tenant 31 --json
|
||||
|
||||
Suggested systemd timer (per tenant), once an hour:
|
||||
ExecStart=/usr/bin/python3 /home/Autopartes/scripts/meli_questions_sla.py \
|
||||
--tenant 31 --max-age-hours 24
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
|
||||
|
||||
|
||||
def _load_env():
|
||||
env_file = '/etc/nexus/pos.env'
|
||||
if os.environ.get('MASTER_DB_URL') or not os.path.exists(env_file):
|
||||
return
|
||||
for line in open(env_file):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
key, value = line.split('=', 1)
|
||||
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--tenant', type=int, required=True, help='tenant id (e.g. 31 = Rached)')
|
||||
ap.add_argument('--max-age-hours', type=float, default=24,
|
||||
help='age threshold in hours (default 24)')
|
||||
ap.add_argument('--limit', type=int, default=20,
|
||||
help='max questions to auto-answer when --auto-answer is used')
|
||||
ap.add_argument('--auto-answer', action='store_true',
|
||||
help='send high-confidence answers (requires meli_auto_answer_questions = true)')
|
||||
ap.add_argument('--json', action='store_true', help='print the full result as JSON')
|
||||
args = ap.parse_args()
|
||||
|
||||
_load_env()
|
||||
|
||||
from tenant_db import get_tenant_conn
|
||||
from services import marketplace_external_service as meli
|
||||
|
||||
conn = get_tenant_conn(args.tenant)
|
||||
try:
|
||||
stale = meli.list_stale_questions(conn, max_age_hours=args.max_age_hours)
|
||||
cfg = meli.get_meli_config(conn)
|
||||
result = {
|
||||
"tenant": args.tenant,
|
||||
"max_age_hours": args.max_age_hours,
|
||||
"auto_answer_enabled": bool(cfg.get("meli_auto_answer_questions")),
|
||||
"stale_count": len(stale),
|
||||
"stale": stale,
|
||||
}
|
||||
if args.auto_answer:
|
||||
if not cfg.get("meli_auto_answer_questions"):
|
||||
result["auto_answer"] = {
|
||||
"enabled": False,
|
||||
"reason": "meli_auto_answer_questions is not enabled for this tenant",
|
||||
}
|
||||
else:
|
||||
result["auto_answer"] = meli.auto_answer_questions(conn, limit=args.limit)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
|
||||
return 0
|
||||
|
||||
print(f"[tenant {args.tenant}] questions SLA — unanswered older than "
|
||||
f"{args.max_age_hours}h: {result['stale_count']}")
|
||||
if not stale:
|
||||
print(" Nothing overdue.")
|
||||
for q in stale:
|
||||
age = q.get('age_hours')
|
||||
print(f" q#{q['id']} ({age}h) {q['buyer_nickname'] or 'comprador'}: "
|
||||
f"{(q['question_text'] or '')[:70]!r} — item {q['external_item_id']}")
|
||||
|
||||
if args.auto_answer:
|
||||
aa = result.get("auto_answer") or {}
|
||||
if aa.get("enabled") is False:
|
||||
print(f"\n auto-answer not run: {aa.get('reason')}")
|
||||
else:
|
||||
print(f"\n auto-answer: answered={aa.get('answered')} "
|
||||
f"skipped={aa.get('skipped')}")
|
||||
else:
|
||||
print("\n (report-only; re-run with --auto-answer to send high-confidence "
|
||||
"answers when meli_auto_answer_questions = true)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user