Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion generative/eval_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,23 @@ def _calc_kpis(
}
n_canonical_pdfs = len(_eval_gk | _log_gk)

# "Evaluierte Notes"-Quote (#224): Zähler (n_notes, oben) und Nenner müssen
# dieselbe Versions-Basis teilen, sonst entstehen Werte >100 % (Zähler aus
# DB/note_evals früh gefiltert, alter Nenner `total_generated` aus
# all_log_runs über ALLE Versionen gepoolt). Nenner hier = all_log_runs
# exakt der kpi_version (`latest_pver`, derselbe Anker wie `latest_qrows`
# oben) — bei explizitem Versions-Filter sind quality_rows/all_log_runs
# bereits serverseitig auf dieselbe Version eingeschränkt, ungefiltert
# erzwingt der exakte Vergleich unten dieselbe Beschränkung nachträglich.
generated_kpi_ver = latest_pver
kpi_gen_runs = [r for r in all_log_runs if r.get("ver") == generated_kpi_ver]
kpi_generated = sum(r["n_total"] for r in kpi_gen_runs)
_n_notes = _distinct_notes(latest_qrows)
# Guard statt Absurd-Wert: Nenner 0/fehlend (keine Log-/DB-Zeile zur
# Version) oder Zähler > Nenner (Datenlücke, z. B. Re-Evals ohne
# zugehörigen Log-Run) → null, Client zeigt "–" statt >100 %.
notes_eval_pct = round(100 * _n_notes / kpi_generated, 1) if kpi_generated and _n_notes <= kpi_generated else None

return {
"avg_accept": avg_accept,
"avg_hall": avg_hall,
Expand All @@ -598,7 +615,12 @@ def _calc_kpis(
# blähten die Zahl auf (50 Instanzen / 39 Notes) und gewichteten die
# Poolung implizit nach Testlauf-Häufigkeit. Fehlt ein Note-Identifier
# (synthetische Rows), zählt der Laufindex jede Zeile einzeln.
"n_notes": _distinct_notes(latest_qrows),
"n_notes": _n_notes,
# Server-berechnete "% der Notes"-Quote + ihre Versions-Basis (#224) —
# Client rechnet nicht mehr selbst aus n_notes/total_generated (die
# unterschiedliche Versions-/Filterkontexte hatten). null = "–".
"notes_eval_pct": notes_eval_pct,
"generated_kpi_ver": generated_kpi_ver,
"total_runs": len(all_log_runs),
"n_pdfs": n_canonical_pdfs,
"n_versions": len(all_versions),
Expand Down
47 changes: 47 additions & 0 deletions generative/tests/test_dashboard_current_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,50 @@ def test_accept_ver_falls_back_to_latest_at_or_below_config():
runs = [_log_run("v0.1.0", 10, 5), _log_run("v0.3.135", 7, 5)]
kpis = _calc_kpis({}, runs, [], [], current_version="v0.3.140")
assert kpis["kpi_accept_ver"] == "v0.3.135"


# ── notes_eval_pct (#224): Zähler/Nenner auf derselben Versions-Basis ───────


def test_notes_eval_pct_guards_against_over_100_pct():
# Issue-Repro: 10 distinct Eval-Notes (Re-Evals) vs. nur 3 generierte Notes
# derselben Version in den Log-Runs -> vorher 333,3 %, jetzt null/"–".
qrows = [_qrow("v0.3.140", f"2026-07-05T20:00:{i:02d}", 0.0, 10, 0) for i in range(10)]
runs = [_log_run("v0.3.140", 3, 3)]
kpis = _calc_kpis({}, runs, qrows, [], current_version="v0.3.140")
assert kpis["n_notes"] == 10
assert kpis["generated_kpi_ver"] == "v0.3.140"
assert kpis["notes_eval_pct"] is None


def test_notes_eval_pct_correct_when_denominator_covers_numerator():
# Gesunder Fall: Nenner (20 generiert) >= Zähler (5 evaluiert), gleiche Version.
qrows = [_qrow("v0.3.140", f"2026-07-05T20:00:{i:02d}", 0.0, 10, 0) for i in range(5)]
runs = [_log_run("v0.3.140", 20, 15)]
kpis = _calc_kpis({}, runs, qrows, [], current_version="v0.3.140")
assert kpis["n_notes"] == 5
assert kpis["generated_kpi_ver"] == "v0.3.140"
assert kpis["notes_eval_pct"] == 25.0


def test_notes_eval_pct_none_when_no_runs_for_version():
# Läufe existieren nur für eine andere Version -> Nenner der KPI-Version = 0.
qrows = [_qrow("v0.3.140", "2026-07-05T20:00:00", 0.0, 10, 0)]
runs = [_log_run("v0.3.139", 5, 5)]
kpis = _calc_kpis({}, runs, qrows, [], current_version="v0.3.140")
assert kpis["generated_kpi_ver"] == "v0.3.140"
assert kpis["notes_eval_pct"] is None


def test_notes_eval_pct_unfiltered_uses_kpi_version_not_all_versions():
# Ungefiltert (mehrere Versionen in quality_rows/all_log_runs): der Nenner
# darf NICHT über alle Versionen summieren (das war die im tgtHint
# dokumentierte Basis-Mischung) -- nur die kpi_version-Zeilen zaehlen.
qrows = [_qrow("v0.3.130", f"2026-06-01T10:00:{i:02d}", 0.0, 10, 0) for i in range(30)]
qrows += [_qrow("v0.3.140", f"2026-07-05T20:00:{i:02d}", 0.0, 10, 0) for i in range(4)]
runs = [_log_run("v0.3.130", 100, 90), _log_run("v0.3.140", 10, 8)]
kpis = _calc_kpis({}, runs, qrows, [], current_version="v0.3.140")
assert kpis["kpi_version"] == "v0.3.140"
assert kpis["n_notes"] == 4
assert kpis["generated_kpi_ver"] == "v0.3.140"
assert kpis["notes_eval_pct"] == 40.0
4 changes: 2 additions & 2 deletions internal/dashboard/eval_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -1282,8 +1282,8 @@
tgtHint:`Schwellenwert für die Ampel-Bewertung: gut ab ${T.cov[0]} %, schlecht bei ${T.cov[1]} % oder darunter. Fest im Dashboard-Code hinterlegt, keine wissenschaftliche Norm.` },
{ id:'kpi-n', key:'n', label:'Evaluierte Notes', val:kpis.n_notes, u:'', digits:0,
hint:'Anzahl der Notes der aktuellen Pipeline-Version, die den automatischen Qualitäts-Check (Eval) durchlaufen haben — die Stichprobe hinter Fehlerquote und Belegrate.',
tone:'info', tgt: kpis.total_generated ? `${_fmtDE(100*kpis.n_notes/kpis.total_generated)} % der Notes` : '–',
tgtHint:'Zähler: evaluierte Notes NUR der aktuellen Pipeline-Version (wie die Kachel oben). Nenner: generierte Notes ALLER Pipeline-Versionen. Die Prozentzahl mischt damit zwei unterschiedliche Versions-Basen — keine reine Evaluierungs-Quote der aktuellen Version.' },
tone:'info', tgt: kpis.notes_eval_pct!=null ? `${_fmtDE(kpis.notes_eval_pct)} % der Notes` : '–',
tgtHint:'Zähler und Nenner teilen dieselbe Versions- und Filterbasis: evaluierte Notes ÷ generierte Notes, beide NUR der aktuellen Pipeline-Version (Kachel oben). „–" wenn zu dieser Version keine generierten Notes protokolliert sind oder mehr Notes evaluiert als generiert wurden (Datenlücke, z. B. Re-Evals ohne zugehörigen Log-/DB-Lauf) — dann würde die Quote über 100 % ergeben.' },
// #204 P5: "Laufzeit" ohne Scope war eine von vier unterscheidbaren
// Kennzahlen gleichen Namens (Kachel = aktuelle Version kumuliert, Strip-
// Zelle = Lifetime, Kosten-Chart = Median je Version, Agenten-Chart =
Expand Down
Loading