fix(maria): codul de eroare decide acoperirea, nu cosinusul

O captura cu ORA-06550 / PLS-00906 primea raspunsul despre ORA-12541 (TNS no
listener) si nu pleca la suport. Cauza: doua erori Oracle diferite se scriu
aproape la fel ("[Oracle][ODBC][Ora]ORA-..."), asa ca chunk-ul gresit a iesit la
cosinus 0,736 — peste RANK_STRONG_COSINE, care scurtcircuita orice alta dovada.
Chunk-ul corect din dictionar exista, dar statea pe pozitia 4, in afara TOP_K:
OCR-ul unei ferestre intregi aduce zeci de tokeni de zgomot, iar BM25 ineca tocmai
codul.

- `rank.codes()` scoate codurile de oriunde din text, nu doar ca token intreg —
  OCR-ul le lipeste de ce e langa ("OraJORA-06550").
- `rank.by_codes()` e un al treilea clasament in fuziune: chunk-ul care poarta
  codul cerut urca peste cele care doar seamana. Acum iese primul.
- `rank.assess()`: cand intrebarea are coduri, decid doar ele. Niciun cod gasit =
  escaladare la suport, oricat de mare ar fi cosinusul.
- `consumer.search()` judeca acoperirea pe cosinusul chunk-urilor date chiar
  modelului, nu pe cel mai bun din tot indexul.

Calibrare 21/21 (cele 19 cazuri + captura reala + ORA-00600, cod care nu e in
documente si trebuie sa plece la suport). Teste: 79 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
Claude Agent
2026-08-31 22:25:45 +00:00
parent 44763869ce
commit b7d75f05ee
5 changed files with 143 additions and 10 deletions

View File

@@ -121,9 +121,11 @@ def search(index: Index, query: str, top_k: int) -> dict:
q_vec = embed(query)
cosines = [cosine(q_vec, e["embedding"]) for e in index.entries]
bm_scores = index.bm25.scores(query)
order = rank.rank(cosines, bm_scores)[:top_k]
order = rank.rank(cosines, bm_scores, rank.by_codes(query, index.texts))[:top_k]
texts = [index.texts[i] for i in order]
best = max(cosines)
# cosinusul chunk-urilor CHIAR date modelului, nu cel mai bun din tot indexul:
# altfel acoperirea se judeca pe o dovada care nu ajunge in context.
best = max((cosines[i] for i in order), default=0.0)
covered, reason = rank.assess(query, best, texts, index.bm25)
return {
"chunks": texts,

View File

@@ -14,6 +14,11 @@ una semantica ii topeste in "ceva despre facturi". Asa ca ordonam de doua ori,
independent, si fuzionam clasamentele (Reciprocal Rank Fusion): un chunk urcat de
ambele metode iese primul, iar unul urcat doar de una ramane in cursa.
Codurile merg mai departe de atat: ele dau un al treilea clasament (`by_codes`) si
sunt singurul criteriu de acoperire cand intrebarea contine unul (`assess`). Un cod
e un identificator exact — un document care nu-l pomeneste nu raspunde la el, oricat
de bine ar semana textul din jur.
Fuziunea da mereu un clasament, si la o intrebare complet straina — de aceea
decizia "avem sau nu acoperire in documente" se ia separat, in `assess()`, pe
dovezi (cat de sus e cosinusul, si daca termenii rari din intrebare chiar apar in
@@ -71,6 +76,44 @@ _STOP = {
# Coduri de eroare si formulare: valoreaza cat zece cuvinte obisnuite.
_COD = re.compile(r"^(?:ora|pls|tns|sp2|imp|exp|ora)-\d+$|^d\d{3}$|^e\d{3,}$")
# Acelasi lucru, cautat ORIUNDE in text, nu doar ca token intreg. OCR-ul lipeste
# codul de ce e langa el ("[Oracle][ODBC][OraJORA-06550" -> un singur token
# "orajora-06550"), iar codul e exact bucata care nu are voie sa se piarda.
_COD_IN_TEXT = re.compile(
r"(?:ora|pls|tns|sp2|imp|exp)-\d+|(?<![a-z0-9])(?:d\d{3}|br-co-\d+)(?![a-z0-9])"
)
def codes(text: str) -> list[str]:
"""Codurile de eroare din text, in ordinea aparitiei, fara duplicate.
Un cod e un identificator exact, nu un cuvant: `ORA-06550` si `ORA-12541`
sunt doua erori fara nimic in comun, desi textele din jurul lor se aseamana
pana la confuzie pentru o cautare semantica.
"""
return list(dict.fromkeys(_COD_IN_TEXT.findall(fold(text))))
def by_codes(query: str, texts: list[str]) -> list[int]:
"""Chunk-urile care contin codurile din intrebare, cele cu mai multe intai.
Al treilea clasament, langa cosinus si BM25. BM25 stie de coduri, dar le
ineaca: o captura de ecran aduce prin OCR zeci de tokeni („oracle", „odbc",
„line", „column", numele butoanelor din fereastra), iar chunk-urile care se
potrivesc pe zgomotul ala pot trece inaintea celui care poarta chiar codul.
"""
cerute = codes(query)
if not cerute:
return []
gasite: list[tuple[int, int]] = []
for i, text in enumerate(texts):
ale_lui = set(codes(text))
n = sum(1 for c in cerute if c in ale_lui)
if n:
gasite.append((n, i))
gasite.sort(key=lambda g: -g[0])
return [i for _, i in gasite]
def fold(text: str) -> str:
"""Fara diacritice, litere mici. „Factură" si „factura" trebuie sa fie acelasi token."""
@@ -154,16 +197,20 @@ def rrf(rankings: list[list[int]], k: int = RRF_K) -> dict[int, float]:
def rank(
cosines: list[float],
bm25_scores: list[float],
code_order: list[int] | None = None,
recall: int | None = None,
) -> list[int]:
"""Indicii chunk-urilor, de la cel mai relevant, dupa fuziunea celor doua clasamente."""
"""Indicii chunk-urilor, de la cel mai relevant, dupa fuziunea clasamentelor."""
recall = recall_n() if recall is None else recall
by_cos = sorted(range(len(cosines)), key=lambda i: cosines[i], reverse=True)[:recall]
# chunk-urile cu scor BM25 zero nu au niciun token comun cu intrebarea: nu au
# ce cauta in clasamentul lexical, doar ar dilua fuziunea cu pozitii arbitrare.
by_bm = [i for i in sorted(range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True)
if bm25_scores[i] > 0][:recall]
fused = rrf([by_cos, by_bm])
rankings = [by_cos, by_bm]
if code_order:
rankings.append(code_order[:recall])
fused = rrf(rankings)
return sorted(fused, key=lambda i: (fused[i], cosines[i]), reverse=True)
@@ -179,15 +226,32 @@ def assess(
) -> tuple[bool, str]:
"""(avem acoperire in documente?, motivul) — decizia de a raspunde sau de a escalada.
Trei cazuri, in ordine:
Cazuri, in ordine:
0. intrebarea contine coduri de eroare -> decide DOAR prezenta lor in
chunk-uri, si cand cosinusul e mare (vezi mai jos);
1. cosinus mare -> raspundem, indiferent de lexical;
2. cosinus la mijloc, dar o fractiune suficienta din termenii rari ai
intrebarii apare chiar in chunk-uri (tipic: un cod de eroare) -> raspundem;
3. altfel -> nu avem acoperire, intrebarea pleaca la suport.
Cazul 0 exista fiindca cosinusul mare NU e o dovada cand e vorba de coduri:
doua erori Oracle diferite se scriu aproape la fel („[Oracle][ODBC][Ora]ORA-…"),
deci un chunk despre ORA-12541 sta la 0,74 de o intrebare despre ORA-06550.
Fara asta, Maria raspundea sigur pe ea cu eroarea gresita, in loc sa escaladeze.
"""
strong = strong_cosine() if strong is None else strong
weak = weak_cosine() if weak is None else weak
min_ratio = min_rare_ratio() if min_ratio is None else min_ratio
cerute = codes(query)
if cerute:
blob_cod = set(codes("\n".join(top_texts)))
gasite = [c for c in cerute if c in blob_cod]
if gasite:
return True, f"cod exact in documente: {', '.join(gasite)}"
return False, (
f"intrebarea are coduri ({', '.join(cerute[:4])}) care nu apar in "
f"documente (cosinus {best_cosine:.3f})"
)
if best_cosine >= strong:
return True, f"cosinus {best_cosine:.3f} >= {strong}"
if best_cosine < weak: