update
This commit is contained in:
@@ -11,6 +11,7 @@ previous report of the same topic.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
@@ -277,8 +278,10 @@ def hygiene(blocks: list[dict]) -> list[dict]:
|
||||
|
||||
def _zaehlbare_luecken(lk: list[dict], llm: bool) -> list[dict]:
|
||||
"""With --llm only non-refuted gaps count ('?' = unjudged stays, conservative) — refuted
|
||||
ones dragged the note although the judge cleared them (aak: 5 of 8, weight 3.0)."""
|
||||
return [x for x in lk if x.get("llm") != "nein"] if llm else lk
|
||||
ones dragged the note although the judge cleared them (aak: 5 of 8, weight 3.0).
|
||||
Persistierte Freisprüche (2:1 im Repair) zählen nie."""
|
||||
offen = [x for x in lk if not x.get("freispruch")]
|
||||
return [x for x in offen if x.get("llm") != "nein"] if llm else offen
|
||||
|
||||
|
||||
def note(quoten: dict, gewichte: dict = NOTE_GEWICHTE) -> float:
|
||||
@@ -335,11 +338,13 @@ def _qa_prompt(name: str, **kwargs) -> str:
|
||||
|
||||
|
||||
async def judge_wave(template: str, topic: str, key: str, slot: str, items: list[str],
|
||||
*, chunk: int = JUDGE_CHUNK, prefix: str = "qa", label: str = "QA") -> dict[int, str]:
|
||||
*, chunk: int = JUDGE_CHUNK, prefix: str = "qa", label: str = "QA",
|
||||
extra: str = "") -> dict[int, str]:
|
||||
"""Gechunkte Ja/Nein-Judge-Welle über ALLE Items, Chunks parallel (die Semaphoren in
|
||||
agents.py begrenzen); Ergebnis mit globalen 1-basierten Indizes. Fail-open pro Chunk
|
||||
(Items bleiben ohne Urteil), aber nie stumm. Ersetzt die drei strukturgleichen
|
||||
Handkopien in repair/qa/guide_qa."""
|
||||
Handkopien in repair/qa/guide_qa. `extra` füllt den {extra}-Slot des Templates
|
||||
(z. B. Inventar-Kontext für das Lücken-Urteil im Repair)."""
|
||||
from agents import run_agent
|
||||
from pipeline import _timeout, _yesno_schema
|
||||
from jsonio import parse_json_text
|
||||
@@ -349,7 +354,7 @@ async def judge_wave(template: str, topic: str, key: str, slot: str, items: list
|
||||
listing = "\n\n".join(f"{k}. {it}" for k, it in enumerate(teil, 1))
|
||||
try:
|
||||
rc, out, _err = await run_agent(
|
||||
f"{prefix}-{topic}-{key}-{lo}", _qa_prompt(template, topic=topic, extra="", **{slot: listing}),
|
||||
f"{prefix}-{topic}-{key}-{lo}", _qa_prompt(template, topic=topic, extra=extra, **{slot: listing}),
|
||||
_timeout("qa_judge"), role="judge", capabilities="none", scope=topic, label=f"{label} {key}")
|
||||
except Exception:
|
||||
log.exception("[%s] %s-Judge %s+%d fehlgeschlagen — Items ohne Urteil", topic, label, key, lo)
|
||||
@@ -406,6 +411,22 @@ def _paar_key(a: str, b: str) -> str:
|
||||
return "||".join(sorted((_norm_title(a), _norm_title(b))))
|
||||
|
||||
|
||||
def luecken_key(item: dict) -> str:
|
||||
"""Stabiler Freispruch-Schlüssel einer Lücken-Fundstelle: Datei + Vorschau-Hash.
|
||||
Abschnittsnummern verschieben sich bei Korpusänderung — dann veralten die Keys
|
||||
und werden inert (gewollt: neue Fundstellen brauchen ein neues Urteil)."""
|
||||
v = " ".join(str(item.get("vorschau", "")).casefold().split())
|
||||
return f"{item.get('datei', '')}||{hashlib.sha1(v.encode()).hexdigest()[:16]}"
|
||||
|
||||
|
||||
def offene_luecken(report: dict) -> tuple[list[dict], list[str]]:
|
||||
"""Zählbare offene Lücken eines Reports: (luecken-Items ohne Freispruch/LLM-nein,
|
||||
konzept_luecken-Namen). Grundlage für Lücken-Research und Lücken-Urteil im Repair."""
|
||||
lk = [x for x in report.get("luecken", [])
|
||||
if not x.get("freispruch") and x.get("llm") != "nein"]
|
||||
return lk, list(report.get("konzept_luecken", []))
|
||||
|
||||
|
||||
def lade_freispruch(topic: str) -> dict[str, list[str]]:
|
||||
"""Persistierte 2:1-Freisprüche des Repair-Stichentscheids (repair._mit_stichentscheid):
|
||||
mehrheitlich als „behalten" geurteilte Befunde zählen nicht mehr in die Note — sonst
|
||||
@@ -445,16 +466,31 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
|
||||
frei_fremd = set(frei.get("fremd") or [])
|
||||
fremd_frei = [t for t in fr if _norm_title(t) in frei_fremd]
|
||||
fr = [t for t in fr if _norm_title(t) not in frei_fremd]
|
||||
# Persistierte 2:1-Freisprüche: markiert bleibt sichtbar (diffbar), zählt aber nicht mehr
|
||||
# in Quote und Judge-Welle — sonst pendelte der Verdachts-Floor ewig unter 10.
|
||||
frei_dub = set(frei.get("dubletten") or [])
|
||||
for p in d:
|
||||
if _paar_key(p["a"], p["b"]) in frei_dub:
|
||||
p["freispruch"] = True
|
||||
d_offen = [p for p in d if not p.get("freispruch")]
|
||||
frei_lk = set(frei.get("luecken") or [])
|
||||
for x in lk:
|
||||
if luecken_key(x) in frei_lk:
|
||||
x["freispruch"] = True
|
||||
lk_offen = [x for x in lk if not x.get("freispruch")]
|
||||
frei_kl = set(frei.get("konzept_luecken") or [])
|
||||
kl_frei = [nm for nm in kl if _norm_title(nm) in frei_kl]
|
||||
kl = [nm for nm in kl if _norm_title(nm) not in frei_kl]
|
||||
|
||||
if llm and d:
|
||||
if llm and d_offen:
|
||||
v = await judge_wave("QA-Dubletten", topic, "dubletten", "pairs",
|
||||
[f"A: {p['a']}\nB: {p['b']}" for p in d[:LLM_SAMPLE]])
|
||||
for k, p in enumerate(d[:LLM_SAMPLE], 1):
|
||||
[f"A: {p['a']}\nB: {p['b']}" for p in d_offen[:LLM_SAMPLE]])
|
||||
for k, p in enumerate(d_offen[:LLM_SAMPLE], 1):
|
||||
p["llm"] = v.get(k, "?")
|
||||
if llm and lk:
|
||||
if llm and lk_offen:
|
||||
v = await judge_wave("QA-Luecken", topic, "luecken", "sections",
|
||||
[f"[{x['datei']} #{x['abschnitt']}] {x['vorschau']}" for x in lk[:LLM_SAMPLE]])
|
||||
for k, x in enumerate(lk[:LLM_SAMPLE], 1):
|
||||
[f"[{x['datei']} #{x['abschnitt']}] {x['vorschau']}" for x in lk_offen[:LLM_SAMPLE]])
|
||||
for k, x in enumerate(lk_offen[:LLM_SAMPLE], 1):
|
||||
x["llm"] = v.get(k, "?")
|
||||
if llm and sd: # full coverage in chunks — a sampled quota would mislead the note
|
||||
v = await judge_wave("QA-Sub-Dubletten", topic, "sub-dubletten", "pairs",
|
||||
@@ -502,7 +538,7 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
|
||||
"topic": topic, "erstellt": datetime.now(timezone.utc).isoformat(),
|
||||
"run_id": summary.get("run_id", ""), "bloecke": len(blocks),
|
||||
"quoten": {
|
||||
"dubletten_verdacht": round(len(d) / max(len(blocks), 1), 3),
|
||||
"dubletten_verdacht": round(len(d_offen) / max(len(blocks), 1), 3),
|
||||
"luecken": round(len(_zaehlbare_luecken(lk, llm)) / n_sections, 3),
|
||||
"fremd": round(len(fr) / max(len(blocks), 1), 3),
|
||||
**({"konzept_luecken": round(len(kl) / max(len(named), 1), 3)} if corpus else {}),
|
||||
@@ -512,6 +548,7 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
|
||||
"quoten_artefakte": quoten_art,
|
||||
**({"unecht": unecht} if unecht is not None else {}),
|
||||
**({"fremd_freigesprochen": fremd_frei} if fremd_frei else {}),
|
||||
**({"konzept_luecken_freigesprochen": kl_frei} if kl_frei else {}),
|
||||
"dubletten": d, "sub_dubletten": sd, "luecken": lk, "konzept_luecken": kl,
|
||||
"fremd": fr, "beleg": bl, "hygiene": hy,
|
||||
"artefakte": art,
|
||||
|
||||
Reference in New Issue
Block a user