- 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>
78 lines
3.7 KiB
Python
78 lines
3.7 KiB
Python
"""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)
|