- embeddings: corpus ca matrice numpy cu norme precalculate; suggest_nearest
= un matvec (~0.6ms/query fata de ~500ms cosine pur-Python la 17k vectori)
- enrich_suggestions: vot ponderat cu similaritatea pe top-5 vecini (NUL =
eticheta proprie); prag 0.5 -> 0.88, calibrat LOO pe corpusul SILVER
(tools/mapare-llm/knn_calibrate.py): precizie 90.5% -> 93.1%, cod gresit
preselectat 7.2% -> 4.7%; sub prag abtinere -> preselectie fuzzy
- UI: codul sugerat de sistem afisat explicit cu sursa si scorul, separat de
lista fuzzy ("potrivire text"); indicator de progres reparat pe upload
(display:inline anula .htmx-indicator) si adaugat pe pasii 2->3 si
"Salveaza maparile"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
7.0 KiB
Python
186 lines
7.0 KiB
Python
"""enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
|
|
|
|
Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat >
|
|
exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie.
|
|
Nimic din enrich_suggestions nu intra in resolve_prestatii/load_mapping.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture()
|
|
def env(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us006.db"))
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
|
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.db import init_db
|
|
init_db()
|
|
yield monkeypatch
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def conn(env):
|
|
from app.db import get_connection
|
|
c = get_connection()
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
def _silver(conn, denumire_norm, cod, is_nul=0):
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO mapping_suggestions "
|
|
"(denumire_normalizata, cod_prestatie, is_nul, source, confidence) VALUES (?, ?, ?, 'llm_seed', 0.7)",
|
|
(denumire_norm, cod, is_nul),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _mock_embedding(monkeypatch, cod, sim, is_nul=False):
|
|
import app.embeddings as emb
|
|
monkeypatch.setattr(emb, "has_corpus", lambda: True)
|
|
monkeypatch.setattr(emb, "suggest_nearest",
|
|
lambda text, top_k=1: [{"cod": cod, "is_nul": is_nul, "similaritate": sim}])
|
|
|
|
|
|
def test_prefiltru_nul_supreseaza_inainte_de_knn(conn, monkeypatch):
|
|
# Embedding-ul AR sugera un cod, dar pre-filtrul NUL trebuie sa scurtcircuiteze.
|
|
chemat = {"da": False}
|
|
import app.embeddings as emb
|
|
monkeypatch.setattr(emb, "has_corpus", lambda: True)
|
|
|
|
def spion(text, top_k=1):
|
|
chemat["da"] = True
|
|
return [{"cod": "OE-1", "is_nul": False, "similaritate": 0.99}]
|
|
|
|
monkeypatch.setattr(emb, "suggest_nearest", spion)
|
|
from app.mapping import enrich_suggestions
|
|
out = enrich_suggestions(conn, "13 X ITP")
|
|
assert out["sugestie_principala"] is None # non-operatie -> fara cod
|
|
assert out["surse"]["nul"] is True
|
|
assert chemat["da"] is False # k-NN nici macar interogat
|
|
|
|
|
|
def test_precedenta_gold_exact_embedding(conn, monkeypatch):
|
|
from app.shared_store import record_human_validation
|
|
from app.mapping import enrich_suggestions, normalize_for_match
|
|
den = "OPERATIE DE TEST UNICA"
|
|
norm = normalize_for_match(den)
|
|
|
|
# Toate trei sursele dau coduri diferite.
|
|
record_human_validation(conn, den, "OE-1") # GOLD partajat
|
|
_silver(conn, norm, "OE-2") # SILVER exact
|
|
_mock_embedding(monkeypatch, "OE-3", 0.99) # embedding
|
|
conn.commit()
|
|
|
|
out = enrich_suggestions(conn, den)
|
|
assert out["sugestie_principala"] == {"cod_prestatie": "OE-1", "sursa": "gold_partajat"}
|
|
|
|
# Fara GOLD -> castiga SILVER.
|
|
conn.execute("DELETE FROM shared_mappings")
|
|
conn.commit()
|
|
out = enrich_suggestions(conn, den)
|
|
assert out["sugestie_principala"]["sursa"] == "silver"
|
|
assert out["sugestie_principala"]["cod_prestatie"] == "OE-2"
|
|
|
|
# Fara GOLD si fara SILVER -> castiga embedding.
|
|
conn.execute("DELETE FROM mapping_suggestions")
|
|
conn.commit()
|
|
out = enrich_suggestions(conn, den)
|
|
assert out["sugestie_principala"] == {"cod_prestatie": "OE-3", "sursa": "embedding"}
|
|
|
|
|
|
def test_prag_similaritate(conn, monkeypatch):
|
|
from app.mapping import enrich_suggestions, EMB_MIN_SIMILARITATE
|
|
_mock_embedding(monkeypatch, "OE-3", EMB_MIN_SIMILARITATE + 0.01)
|
|
out = enrich_suggestions(conn, "CEVA NEVAZUT")
|
|
assert out["surse"]["embedding"] == "OE-3"
|
|
|
|
|
|
def test_abtinere_sub_prag(conn, monkeypatch):
|
|
from app.mapping import enrich_suggestions, EMB_MIN_SIMILARITATE
|
|
_mock_embedding(monkeypatch, "OE-3", EMB_MIN_SIMILARITATE - 0.01)
|
|
out = enrich_suggestions(conn, "CEVA NEVAZUT")
|
|
assert out["surse"]["embedding"] is None # sub prag -> abtinere
|
|
assert out["sugestie_principala"] is None
|
|
|
|
|
|
def _mock_embedding_multi(monkeypatch, vecini):
|
|
"""Mock suggest_nearest cu o lista de vecini [(cod, sim, is_nul), ...]."""
|
|
import app.embeddings as emb
|
|
monkeypatch.setattr(emb, "has_corpus", lambda: True)
|
|
monkeypatch.setattr(
|
|
emb, "suggest_nearest",
|
|
lambda text, top_k=1: [
|
|
{"cod": c, "is_nul": n, "similaritate": s} for c, s, n in vecini
|
|
][:top_k],
|
|
)
|
|
|
|
|
|
def test_vot_topk_bate_top1_pe_etichete_contradictorii(conn, monkeypatch):
|
|
"""Corpus cu etichete contradictorii pe denumiri aproape identice:
|
|
top-1 ar da OE-1, dar votul ponderat (2x OE-8 vs 1x OE-1) da OE-8."""
|
|
from app.mapping import enrich_suggestions
|
|
_mock_embedding_multi(monkeypatch, [
|
|
("OE-1", 0.94, False),
|
|
("OE-8", 0.93, False),
|
|
("OE-8", 0.92, False),
|
|
])
|
|
out = enrich_suggestions(conn, "INLOCUIRE ANVELOPE")
|
|
assert out["surse"]["embedding"] == "OE-8"
|
|
assert out["surse"]["embedding_similaritate"] == 0.93
|
|
|
|
|
|
def test_vot_vecini_sub_prag_nu_voteaza(conn, monkeypatch):
|
|
"""Vecinii sub EMB_MIN_SIMILARITATE nu intra in vot, chiar daca sunt majoritari."""
|
|
from app.mapping import enrich_suggestions, EMB_MIN_SIMILARITATE
|
|
_mock_embedding_multi(monkeypatch, [
|
|
("OE-3", EMB_MIN_SIMILARITATE + 0.01, False),
|
|
("OE-1", EMB_MIN_SIMILARITATE - 0.05, False),
|
|
("OE-1", EMB_MIN_SIMILARITATE - 0.05, False),
|
|
])
|
|
out = enrich_suggestions(conn, "CEVA NEVAZUT")
|
|
assert out["surse"]["embedding"] == "OE-3"
|
|
|
|
|
|
def test_vot_nul_majoritar_supreseaza(conn, monkeypatch):
|
|
"""Majoritate NUL in vecinatate -> supresie, chiar daca top-1 e un cod."""
|
|
from app.mapping import enrich_suggestions
|
|
_mock_embedding_multi(monkeypatch, [
|
|
("OE-1", 0.93, False),
|
|
(None, 0.92, True),
|
|
(None, 0.92, True),
|
|
])
|
|
out = enrich_suggestions(conn, "CEVA CARE SEAMANA CU GUNOI")
|
|
assert out["surse"]["embedding"] is None
|
|
assert out["surse"]["nul"] is True
|
|
assert out["sugestie_principala"] is None
|
|
|
|
|
|
def test_vecin_knn_nul_supreseaza(conn, monkeypatch):
|
|
from app.mapping import enrich_suggestions
|
|
_mock_embedding(monkeypatch, None, 0.99, is_nul=True) # vecin NUL peste prag
|
|
out = enrich_suggestions(conn, "CEVA CARE SEAMANA CU GUNOI")
|
|
assert out["surse"]["embedding"] is None # NUL -> nu produce cod
|
|
assert out["surse"]["nul"] is True
|
|
assert out["sugestie_principala"] is None
|
|
|
|
|
|
def test_invariant_13_resolve_neatins(conn):
|
|
"""SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
|
|
from app.mapping import resolve_prestatii, normalize_for_match
|
|
_silver(conn, normalize_for_match("OPERATIE X"), "OE-1")
|
|
resolved, unmapped = resolve_prestatii(
|
|
[{"cod_op_service": "OPERATIE X", "denumire": "OPERATIE X"}], mapping={}, valid_codes={"OE-1"}
|
|
)
|
|
assert resolved[0]["cod_prestatie"] is None # ramane nemapat, NU ia codul din SILVER
|
|
assert unmapped and unmapped[0]["cod_op_service"] == "OPERATIE X"
|