104 lines
7.1 KiB
Python
104 lines
7.1 KiB
Python
"""Export measured pilot results into LaTeX and a blinded review sheet."""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import random
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
def load_lines(path):
|
|
return [json.loads(s) for s in path.read_text(encoding="utf-8").splitlines() if s.strip()] if path.exists() else []
|
|
|
|
|
|
def main():
|
|
p=argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument("--run",type=Path,required=True)
|
|
p.add_argument("--before",type=Path,default=Path("outputs/evaluation_pilot"))
|
|
p.add_argument("--project",type=Path,default=Path("ZAVERECNA_PRACA/ZAVERECNA_PRACA"))
|
|
args=p.parse_args()
|
|
rows=list({(r["case_id"],r["mode"]):r for r in load_lines(args.run/"answers.jsonl")}.values())
|
|
manifest=json.loads((args.run/"manifest.json").read_text(encoding="utf-8"))
|
|
expected=len(manifest["identity"]["case_ids"])*len(manifest["identity"]["modes"])
|
|
if len(rows)!=expected: raise SystemExit("Run is incomplete; refusing final table")
|
|
groups=defaultdict(list)
|
|
for row in rows: groups[row["mode"]].append(row)
|
|
judgments={(r["case_id"],r["mode"]):r for r in load_lines(args.run/"judgments.jsonl")}
|
|
tex=[r"Pilot zahŕňa desať otázok a päť konfigurácií, teda 50 plánovaných požiadaviek. "
|
|
r"Tabuľka uvádza skutočne uložené výstupy, nie odhadované hodnoty. "
|
|
r"Dve otázky v každej kategórii nestačia na všeobecné závery o kvalite systému.",
|
|
r"\begin{table}[!htbp]\centering\small",
|
|
r"\caption{Pilotné výsledky po rezervovaní rozpočtu pre pôvodné texty}",
|
|
r"\label{tab:pilot-results}",
|
|
r"\begin{tabularx}{\textwidth}{@{}lrrrrX@{}}\toprule",
|
|
r"Režim & OK & Chyby & Zdroj & Úryvky & Medián času \\\midrule"]
|
|
output={}
|
|
for mode in ["bypass","naive","local","hybrid","mix"]:
|
|
items=groups[mode]; ok=[r for r in items if r["status"]=="ok"]
|
|
hits=[r["metrics"]["source_hit"] for r in ok if r["metrics"]["source_hit"] is not None]
|
|
evidence=sum(bool(r["metrics"]["evidence_chunks"]) for r in ok)
|
|
times=sorted(r["seconds"] for r in ok)
|
|
median=(times[(len(times)-1)//2]+times[len(times)//2])/2 if times else None
|
|
hit=f"{sum(hits)}/{len(hits)}" if hits else "--"
|
|
time=f"{median:.2f} s" if median is not None else "--"
|
|
tex.append(f"{mode} & {len(ok)} & {len(items)-len(ok)} & {hit} & {evidence}/{len(ok)} & {time}"+r" \\")
|
|
output[mode]={"successful":len(ok),"failed":len(items)-len(ok),"source_hit_n":len(hits),"source_hits":sum(hits),
|
|
"answers_with_source_chunks":evidence,"median_seconds":median}
|
|
tex += [r"\bottomrule\end{tabularx}\end{table}",
|
|
r"Stĺpec Zdroj uvádza počet odpovedí obsahujúcich referenčný produkt z ôsmich otázok s referenciou. "
|
|
r"Stĺpec Úryvky uvádza počet odpovedí, pri ktorých API vrátilo aspoň jeden zdrojový úryvok. "
|
|
r"Pri režime bypass sa zdroj nevyhodnocuje. Časy zahŕňajú možné zásahy cache, vyťaženosť školského servera "
|
|
r"a súbežnú funkčnú kontrolu dema; nie sú izolovaným výkonnostným benchmarkom."]
|
|
before=load_lines(args.before/"answers.jsonl")
|
|
old=[r for r in before if r["mode"] in {"local","hybrid","mix"} and r["status"]=="ok"]
|
|
new=[r for r in rows if r["mode"] in {"local","hybrid","mix"} and r["status"]=="ok"]
|
|
if old and new:
|
|
a=sum(bool(r["metrics"]["evidence_chunks"]) for r in old)
|
|
b=sum(bool(r["metrics"]["evidence_chunks"]) for r in new)
|
|
tex.append(f"Diagnostický beh pred úpravou vrátil zdrojové úryvky pri {a} z {len(old)} grafových odpovedí; "
|
|
f"po explicitnom obmedzení grafového kontextu ich obsahovalo {b} z {len(new)}. "
|
|
"V pôvodnom behu bol celkový limit 6\\,000 tokenov bez osobitného zníženia predvolených limitov entít a vzťahov. "
|
|
"V novom behu je celkový limit 8\\,000 tokenov a grafové limity 1\\,000 a 1\\,500 tokenov. "
|
|
"Keďže sa zmenili oba parametre, výsledok opisuje opravu konfigurácie a nie izolovaný účinok jednej premennej.")
|
|
ok_j=[v for v in judgments.values() if v["status"]=="ok"]
|
|
if ok_j:
|
|
tex += [r"\begin{table}[!htbp]\centering\small",
|
|
r"\caption{Orientačná vlastná rubrika LLM-as-a-judge, nie metriky RAGAS}",
|
|
r"\label{tab:pilot-judge}",r"\begin{tabularx}{\textwidth}{@{}lXXX@{}}\toprule",
|
|
r"Režim & Podpora tvrdení & Pokrytie referencie & Primerané odmietnutie \\\midrule"]
|
|
for mode in ["bypass","naive","local","hybrid","mix"]:
|
|
js=[j for j in ok_j if j["mode"]==mode]
|
|
vals=[]
|
|
for metric in ["grounding","coverage"]:
|
|
scores=[j[metric] for j in js if j.get(metric) is not None and
|
|
(metric!="coverage" or not j["case_id"].startswith("unanswerable-"))]
|
|
vals.append(f"{sum(scores)/len(scores):.2f} (n={len(scores)})" if scores else "--")
|
|
neg=[j for j in js if j["case_id"].startswith("unanswerable-")]
|
|
abstain=f"{sum(j['verdict']['appropriate_abstention'] for j in neg)}/{len(neg)}" if neg else "--"
|
|
tex.append(f"{mode} & {vals[0]} & {vals[1]} & {abstain}"+r" \\")
|
|
output[mode]["judge_successful"]=len(js)
|
|
tex += [r"\bottomrule\end{tabularx}\end{table}",
|
|
f"Platný štruktúrovaný verdikt bol získaný pre {len(ok_j)} z {len(rows)} odpovedí. "
|
|
r"Priemery obsahujú iba definované skóre; počty sú uvedené pri hodnotách. "
|
|
r"Podpora tvrdení sa nehodnotí pre bypass ani pri chýbajúcom texte. "
|
|
r"Hodnotiteľ používa rovnakú službu ako generátor a nie je nezávislým odborným posúdením."]
|
|
tex.append(r"Pilot preukazuje technickú vykonateľnosť merania a opravu dostupnosti zdrojových textov. "
|
|
r"Nájdenie zdroja ani orientačné skóre hodnotiaceho modelu samo osebe nedokazuje klinickú správnosť. "
|
|
r"Na potvrdenie prínosu grafu treba širšiu nezávislú sadu, kontrolu referencií a odborné posúdenie odpovedí.")
|
|
(args.project/"evaluation_results.tex").write_text("\n\n".join(tex)+"\n",encoding="utf-8")
|
|
(args.run/"report.json").write_text(json.dumps(output,indent=2),encoding="utf-8")
|
|
# A reproducible shuffle hides configuration labels from a human reviewer.
|
|
random.Random(20261017).shuffle(rows)
|
|
cases={c["id"]:c for c in json.loads(Path("evaluation/scenarios.json").read_text(encoding="utf-8"))}
|
|
with (args.run/"manual_review.csv").open("w",encoding="utf-8-sig",newline="") as f:
|
|
w=csv.writer(f)
|
|
w.writerow(["review_id","question","reference","answer","correctness_0_2","completeness_0_2","unsupported_claims","appropriate_abstention","notes"])
|
|
for i,row in enumerate(rows):
|
|
w.writerow([f"R{i+1:03}",row["question"],cases[row["case_id"]]["reference_answer"],row.get("response",{}).get("response",""),"","","","",""])
|
|
(args.run/"review_key.json").write_text(json.dumps({f"R{i+1:03}":{"case_id":r["case_id"],"mode":r["mode"]} for i,r in enumerate(rows)},indent=2),encoding="utf-8")
|
|
print(json.dumps(output,indent=2))
|
|
|
|
|
|
if __name__=="__main__": main()
|