"""Corectie etichete SILVER: inlocuirea de anvelope -> OE-8. Seed-ul Haiku a etichetat sistematic "INLOCUIT ANVELOPE ..." cu OE-1 (REPARATIE), desi nomenclatorul RAR are un cod dedicat: OE-8 (INLOCUIRE SEZONIERA A ANVELOPELOR). Eroarea domina vecinatatea k-NN (223 OE-1 vs 17 OE-8), asa ca votul sugera OE-1 pentru orice "Inlocuire anvelope". Regula (deterministica, aceeasi in DB si in seed): text NORMALIZAT care contine un verb de inlocuire/montare (INLOCUIT/INLOCUIRE/INL/SCHIMB*/MONTAT/MONTARE) SI anvelope la plural (ANVELOPE/CAUCIUCURI), FARA context de reparatie (JANTA, D/R, D/M, REPARAT, VULCANIZ, PRESIUNE, DEPRESAT) -> OE-8. Singularul cu pozitie ("INLOCUIT ANVELOPA DR FATA") ramane OE-1: e tipic inlocuire dupa pana/uzura in cadrul unei reparatii, nu schimb sezonier. In plus, adauga intrari canonice OE-8 curate manual (`CURATE_OE8`) pentru formularile uzuale de schimb sezonier absente din corpus ("MONTAT CAUCIUCURI VARA" etc.) — fara ele, k-NN nu are niciun vecin relevant si votul cade pe zgomot de coada. Aplica idempotent pe: - tabela `mapping_suggestions` (sursa corpusului k-NN la runtime) - `app/data/operatii-etichetate.json` (seed-ul pentru instalari noi) `source` devine "haiku_seed+fix_oe8" pe randurile corectate, "curat_manual" pe cele adaugate (audit). Rulare: python3 tools/mapare-llm/fix_anvelope_oe8.py --db data/autopass.db [--dry-run] """ from __future__ import annotations import argparse import json import os import re import sqlite3 _HERE = os.path.dirname(os.path.abspath(__file__)) _ROOT = os.path.abspath(os.path.join(_HERE, "..", "..")) SEED_PATH = os.path.join(_ROOT, "app", "data", "operatii-etichetate.json") VERB = re.compile(r"\b(INLOCUIT|INLOCUIRE|INL|SCHIMB|SCHIMBAT|SCHIMBARE|MONTAT|MONTARE)\b") PLURAL = re.compile(r"ANVELOPE|CAUCIUCURI") EXCLUDERE = re.compile(r"JANTA|D/R|D/M|REPARAT|VULCANIZ|PRESIUNE|DEPRESAT") # Formulari canonice de schimb sezonier, absente din corpusul Haiku. # Texte deja in forma normalizata (majuscule ASCII, fara diacritice). CURATE_OE8 = [ "INLOCUIRE ANVELOPE", "SCHIMBARE ANVELOPE", "SCHIMB ANVELOPE", "SCHIMBAT ANVELOPE IARNA", "SCHIMBAT ANVELOPE VARA", "MONTAT ANVELOPE IARNA", "MONTAT ANVELOPE VARA", "MONTAT CAUCIUCURI IARNA", "MONTAT CAUCIUCURI VARA", "SCHIMB CAUCIUCURI", "SCHIMBAT ROTI IARNA", "SCHIMBAT ROTI VARA", "INLOCUIRE SEZONIERA ANVELOPE", ] def este_inlocuire_anvelope(text_normalizat: str) -> bool: """True daca textul descrie o inlocuire/montare de anvelope (plural).""" t = text_normalizat or "" return bool(VERB.search(t) and PLURAL.search(t) and not EXCLUDERE.search(t)) def fix_db(conn: sqlite3.Connection, dry_run: bool) -> int: rows = conn.execute( "SELECT id, denumire_normalizata FROM mapping_suggestions " "WHERE is_nul=0 AND cod_prestatie != 'OE-8' " "AND (denumire_normalizata LIKE '%ANVELOP%' OR denumire_normalizata LIKE '%CAUCIUC%')" ).fetchall() ids = [r[0] for r in rows if este_inlocuire_anvelope(r[1])] if not dry_run and ids: conn.executemany( "UPDATE mapping_suggestions SET cod_prestatie='OE-8', " "source='haiku_seed+fix_oe8', updated_at=datetime('now') WHERE id=?", [(i,) for i in ids], ) conn.commit() return len(ids) def adauga_curate_db(conn: sqlite3.Connection, dry_run: bool) -> int: n = 0 for t in CURATE_OE8: exists = conn.execute( "SELECT 1 FROM mapping_suggestions WHERE denumire_normalizata=?", (t,) ).fetchone() if exists: continue n += 1 if not dry_run: conn.execute( "INSERT INTO mapping_suggestions " "(denumire_normalizata, cod_prestatie, is_nul, source, confidence) " "VALUES (?, 'OE-8', 0, 'curat_manual', 1.0)", (t,), ) if not dry_run: conn.commit() return n def adauga_curate_seed(items: list[dict], dry_run: bool) -> int: existente = {it.get("denumire_normalizata") for it in items} n = 0 for t in CURATE_OE8: if t in existente: continue n += 1 if not dry_run: items.append({ "denumire": t, "denumire_normalizata": t, "cod": "OE-8", "is_nul": False, "source": "curat_manual", "confidence": 1.0, }) return n def fix_seed(dry_run: bool) -> tuple[int, int]: with open(SEED_PATH, encoding="utf-8") as f: items = json.load(f) n = 0 for it in items: if it.get("is_nul") or it.get("cod") == "OE-8": continue if este_inlocuire_anvelope(it.get("denumire_normalizata") or ""): it["cod"] = "OE-8" it["source"] = "haiku_seed+fix_oe8" n += 1 n_curate = adauga_curate_seed(items, dry_run) if not dry_run and (n or n_curate): with open(SEED_PATH, "w", encoding="utf-8") as f: # acelasi format ca genereaza_seed.py (diff git minimal la corectii) json.dump(items, f, ensure_ascii=False, indent=2) return n, n_curate def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--db", default=os.path.join(_ROOT, "data", "autopass.db")) ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() conn = sqlite3.connect(args.db) n_db = fix_db(conn, args.dry_run) n_db_curate = adauga_curate_db(conn, args.dry_run) n_seed, n_seed_curate = fix_seed(args.dry_run) eticheta = "(dry-run) " if args.dry_run else "" print( f"{eticheta}DB: {n_db} relabel + {n_db_curate} curate; " f"seed JSON: {n_seed} relabel + {n_seed_curate} curate" ) if __name__ == "__main__": main()