Files
Autoparts-DB/scripts/eval_sku_matcher.py
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
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
2026-10-12 00:57:31 +00:00

140 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Measure the SKU name-matcher: difflib (baseline) vs QWEN embeddings.
Runs ``sku_matcher.match_inventory`` in dry-run twice over the same inventory
slice — once with the difflib name fallback only, once with the semantic
(embedding) fallback enabled — and compares:
* coverage: how many items got matched at all
* precision: among items that already carry a ``catalog_part_id`` (ground
truth), how often the matcher agrees with the stored id
The second pass reuses the ``CatalogIndex`` built by the first, so catalog-name
embeddings are computed once (in-memory cache).
Usage (as root, from /home/Autopartes):
python3 scripts/eval_sku_matcher.py --tenant 45 --limit 300
python3 scripts/eval_sku_matcher.py --tenant 45 --limit 300 --prefer-below 0.9
python3 scripts/eval_sku_matcher.py --tenant 31 --limit 200 --threshold 0.88
Nothing is written: ``apply=False`` and the UPDATE guard never overwrites an
existing ``catalog_part_id`` anyway.
"""
import argparse
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
from tenant_db import get_tenant_conn, get_master_conn # noqa: E402
from services.sku_matcher import load_catalog_index, match_inventory # noqa: E402
def _precision(res):
"""(attempts_on_linked, correct) using catalog_part_id as ground truth."""
attempts = correct = 0
for r in res.get('results', []):
truth = r.get('existing_catalog_id')
if not truth:
continue
attempts += 1
if r.get('matched_catalog_id') == truth:
correct += 1
return attempts, correct
def _report(label, res):
attempts, correct = _precision(res)
prec = (correct / attempts * 100) if attempts else 0.0
print(f" {label:<12} matched={res['matched']:<4} "
f"by_method={res['by_method']}")
print(f" {'':<12} precision_on_linked={correct}/{attempts} ({prec:.1f}%)")
if res.get('semantic'):
s = res['semantic']
print(f" {'':<12} catalog_emb_cache={s['catalog_embeddings_cached']} "
f"embed_requests={s['embed_requests']}")
return res['matched'], attempts, correct
def main():
p = argparse.ArgumentParser(description='Eval SKU name matching (difflib vs embeddings).')
p.add_argument('--tenant', type=int, required=True)
p.add_argument('--limit', type=int, default=300, help='items to scan')
p.add_argument('--offset', type=int, default=0)
p.add_argument('--threshold', type=float, default=0.86,
help='semantic cosine threshold')
p.add_argument('--prefer-below', type=float, default=None,
help='also run semantic when fuzzy score < this value')
p.add_argument('--name-threshold', type=float, default=0.82)
p.add_argument('--top-k', type=int, default=64,
help='candidate names embedded per item (bounded cost)')
p.add_argument('--batch', type=int, default=64)
p.add_argument('--supplier', default=None)
p.add_argument('--sample', type=int, default=8, help='examples to print')
p.add_argument('--only-linked', action='store_true',
help='restrict to items with a catalog_part_id (ground truth)')
p.add_argument('--show-examples', action='store_true')
args = p.parse_args()
tenant = get_tenant_conn(args.tenant)
master = get_master_conn()
try:
print(f"Building catalog index (supplier={args.supplier or 'ALL'})...")
idx = load_catalog_index(master, supplier=args.supplier)
print(f" catalog items={idx.total} sku_keys={len(idx.by_sku)} "
f"xref_keys={len(idx.by_xref)}")
common = dict(
limit=args.limit, offset=args.offset,
only_unmatched=not args.only_linked, only_linked=args.only_linked,
apply=False, create_aliases=False, supplier=args.supplier,
name_threshold=args.name_threshold, sample_limit=args.sample,
collect=True,
)
print(f"\nInventory slice: tenant={args.tenant} limit={args.limit} "
f"offset={args.offset} (dry-run)")
base = match_inventory(tenant, master, use_semantic=False, index=idx, **common)
print("\n[1] difflib baseline")
_report('baseline', base)
sem = match_inventory(
tenant, master, use_semantic=True, index=idx,
semantic_threshold=args.threshold,
semantic_top_k=args.top_k,
semantic_batch=args.batch,
semantic_prefer_below=args.prefer_below,
**common,
)
print("\n[2] + semantic (embeddings)")
b_m, b_a, b_c = _report('semantic', sem)
# Deltas
print("\n--- delta ---")
print(f" matched: {base['matched']} -> {sem['matched']} "
f"(+{sem['matched'] - base['matched']})")
b_att, b_cor = _precision(base)
s_att, s_cor = _precision(sem)
bp = (b_cor / b_att * 100) if b_att else 0.0
sp = (s_cor / s_att * 100) if s_att else 0.0
print(f" precision on linked: {bp:.1f}% -> {sp:.1f}%")
if args.show_examples:
print("\nExamples (semantic pass):")
for r in sem.get('results', [])[:args.sample]:
mark = ''
if r.get('existing_catalog_id'):
mark = ' OK' if r['existing_catalog_id'] == r['matched_catalog_id'] else ' MISMATCH'
print(f" [{r['method']:<14} {r['score']:.3f}] "
f"{str(r['part_number'])[:16]:<16} {str(r['name'])[:38]:<38} "
f"-> cat {r['matched_catalog_id']}{mark}")
finally:
tenant.close()
master.close()
return 0
if __name__ == '__main__':
sys.exit(main())