Files
rar-autopass/tools/mapare-llm/fix_anvelope_oe8.py
Claude Agent 6674d0e3e1 fix(sugestii): inlocuirea de anvelope -> OE-8 + vot k-NN ponderat pe rang
Seed-ul Haiku eticheta sistematic "INLOCUIT ANVELOPE ..." cu OE-1 (223 vs 17
OE-8), desi RAR are cod dedicat OE-8 (inlocuire sezoniera anvelope), asa ca
votul k-NN propunea OE-1 pentru orice inlocuire de anvelope.

- tools/mapare-llm/fix_anvelope_oe8.py: re-etichetare deterministica (187
  randuri DB + seed JSON -> OE-8; regula exclude singular cu pozitie, D/R,
  janta, vulcanizare) + 13 intrari canonice curate manual pentru formularile
  sezoniere absente din corpus ("MONTAT CAUCIUCURI VARA" etc.); idempotent
- enrich_suggestions: ponderea votului = EMB_VOTE_DECAY^rang (0.7) in loc de
  similaritatea bruta — anizotropia modelului lasa zgomotul de coada (ex.
  INLOCUIT BECURII la 0.934 de INLOCUIRE ANVELOPE) sa invinga vecinii corecti
  de rang 1-2; LOO: precizie egala (93.1%), cod-gresit 4.67% -> 4.63%
- test_fix_anvelope_oe8: gardii pe regula + seed-ul din repo (regresie la
  regenerare); auditul k-NN al dezacordurilor nu a gasit alte erori
  sistematice (curatat=OE-2, reglat=OE-4, bujii/ulei=OE-3 raman apararabile)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:26:30 +00:00

166 lines
5.7 KiB
Python

"""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()