text_to_solver_program/checks/review.py
dschlueter 3c4d2fb92c fix: Nebenpfade behalten Kontext – Gegenprobe, --von, --resolve, UNDERSTAND_RETRIES=-1
- UNDERSTAND_RETRIES=-1 (A/B-Arm der Verstehensstufe) stürzte seit dem Ablehnungstor ab
- Unlösbar-Gegenprobe bekommt Verständnis, Datenprofil und Datendateien; ihre Lösung
  durchläuft dieselbe Abnahme (_assess_run)
- --von pruefung behält Verständnis, Abbildungen und Datenprofil
- --resolve überschreibt frühere Befunde nicht mehr (nur Zulässigkeit neu)
- --daten wirkt auch mit --resolve und --von
- Prüfung und Korrektur sehen das Datenprofil
- Reparaturmeldung nennt alle Befundarten zugleich
- Test der Balkenhöhe prüft die Höhe tatsächlich; unbenutzte Imports entfernt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:22:53 +02:00

78 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
data_prompt: str = "") -> AssumptionReview:
# Mit beigelegten Daten gehört ihr Profil zum Aufgabentext – sonst beurteilt die Prüfung Annahmen
# über Spalten, die sie nie gesehen hat.
aufgabe = f"{task}\n\n{data_prompt}" if data_prompt else task
return session.create(
AssumptionReview,
[
{"role": "system", "content": REVIEW_PROMPT_FILE.read_text(encoding="utf-8")},
{
"role": "user",
"content": f"AUFGABENTEXT:\n{aufgabe}\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)