"""Stufe 4 der Pipeline: LLM-Prüfung von Zielgröße und Annahmen; Entscheidung über Korrekturrunden.""" from pathlib import Path from typing import List from checks.model_checks import has_errors from core.llm import LLMSession from core.model_schema import AssumptionReview, AssumptionVerdict, CheckIssue, CheckSeverity, OptimizationModelSpecification REVIEW_PROMPT_FILE = Path(__file__).parent / "Pruef-Prompt.txt" def review_assumptions(session: LLMSession, task: str, spec: OptimizationModelSpecification) -> AssumptionReview: return session.create( AssumptionReview, [ {"role": "system", "content": REVIEW_PROMPT_FILE.read_text(encoding="utf-8")}, { "role": "user", "content": f"AUFGABENTEXT:\n{task}\n\nMODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}", }, ], step="Prüfung", ) # Warnungen, die eine Korrekturrunde wert sind: keine Fehler (ein richtiges Ergebnis bleibt zuverlässig), # aber in einer Runde billig zu beheben. Unerklärte Fachbegriffe kosten den Leser sonst das Verständnis. REVISION_WORTHY_WARNINGS = {"UNEXPLAINED_JARGON", "UNUSED_FACTS"} def needs_work(review: AssumptionReview, issues: List[CheckIssue]) -> bool: """Rechtfertigt der Stand eine Korrekturrunde?""" return (review.needs_revision or has_errors(issues) or any(i.code in REVISION_WORTHY_WARNINGS for i in issues)) def revision_reasons(review: AssumptionReview, issues: List[CheckIssue]) -> List[str]: """Warum eine Korrekturrunde nötig ist – fürs Log. Bisher stand dort nur „Probleme gefunden“; welche Beanstandung eine Runde auslöste, ließ sich im Nachhinein nicht mehr feststellen (beobachtet an wagner_whitin_dijkstra, 20260917-160618). """ gruende = [] oc = review.objective_check if not oc.matches: gruende.append(f"Zielgröße passt nicht: {oc.explanation[:160]}") if not oc.units_consistent: gruende.append(f"Einheiten der Zielfunktion uneinheitlich: {oc.unit_explanation[:160]}") faulty = {AssumptionVerdict.CONTRADICTS_TASK, AssumptionVerdict.STRICTER_THAN_TASK, AssumptionVerdict.WEAKER_THAN_TASK} for check in review.checks: if check.verdict in faulty: gruende.append(f"Annahme {check.verdict.value}: {check.unclear_point[:120]}") for fehlend in review.missing_assumptions: gruende.append(f"fehlende Annahme: {fehlend.unclear_point[:120]}") for issue in issues: if issue.severity == CheckSeverity.ERROR or issue.code in REVISION_WORTHY_WARNINGS: gruende.append(f"[{issue.code}] {issue.message[:120]}") return gruende def select_best(candidates: List[tuple]) -> int: """Index der besten (spec, issues, review)-Runde. Rangfolge: passende Zielgröße vor unpassender (sonst womöglich Fremdproblem), keine Check-Errors vor Errors, Prüfung ohne Beanstandung vor Beanstandung, weniger Errors; bei Gleichstand gewinnt die spätere Runde. Verhindert, dass eine Korrekturrunde, die das Modell verschlechtert (z. B. halluziniertes Fremdproblem), die bessere frühere Fassung ersetzt. """ def score(index): _, issues, review = candidates[index] errors = sum(1 for i in issues if i.severity == CheckSeverity.ERROR) return (not review.objective_check.matches, errors > 0, review.needs_revision, errors, -index) return min(range(len(candidates)), key=score)