This commit is contained in:
team3
2026-07-08 21:14:33 +02:00
parent 9a6ab0937b
commit f9d77a113b
30 changed files with 1064 additions and 654 deletions

View File

@@ -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,