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>
This commit is contained in:
parent
8bd3c2e7e1
commit
3c4d2fb92c
1 changed files with 182 additions and 59 deletions
101
TextToModel.py
101
TextToModel.py
|
|
@ -5,13 +5,11 @@ import sys
|
|||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from checks.model_checks import check_figure_readings, check_spec, check_understanding, has_errors
|
||||
from checks.model_checks import check_figure_readings, check_spec, check_understanding
|
||||
from checks.review import needs_work, review_assumptions, revision_reasons, select_best
|
||||
from core.llm import DEFAULT_MODEL, LLMSession
|
||||
from core.model_schema import (
|
||||
AssumptionReview,
|
||||
SolverStatus,
|
||||
CheckIssue,
|
||||
CheckSeverity,
|
||||
OptimizationModelSpecification,
|
||||
PipelineResult,
|
||||
|
|
@ -31,7 +29,7 @@ MAX_RESTARTS = int(os.environ.get("MAX_RESTARTS", "1"))
|
|||
# "unlösbar" kann ein Modellierungsfehler sein (beobachtet bei qwen3-235b: Aufgabe war lösbar).
|
||||
# 1 = einmal unabhängig neu modellieren und erneut lösen, bevor "unlösbar" berichtet wird.
|
||||
INFEASIBLE_RECHECK = int(os.environ.get("INFEASIBLE_RECHECK", "1"))
|
||||
# Ein Nachfassen, wenn die Verstehensstufe Zahlen oder Teilfragen übersehen hat (0 schaltet die Stufe ab)
|
||||
# Nachfassen, wenn die Verstehensstufe Zahlen oder Teilfragen übersehen hat (0 = kein Nachfassen, -1 = Stufe aus)
|
||||
UNDERSTAND_RETRIES = int(os.environ.get("UNDERSTAND_RETRIES", "1"))
|
||||
|
||||
task_description = """
|
||||
|
|
@ -107,7 +105,7 @@ def _model_attempt(session, task: str, max_revisions: int, label: str, hint: str
|
|||
log(f"[{label}] Übernehme gespeicherte Spezifikation ({len(spec.constraints)} Restriktionen) ...")
|
||||
issues = check_spec(spec, task, understanding)
|
||||
log(f"[{label}] Prüfe Annahmen ...")
|
||||
review = review_assumptions(session, task, spec)
|
||||
review = review_assumptions(session, task, spec, data_prompt)
|
||||
|
||||
candidates = [(spec, issues, review)]
|
||||
revisions = 0
|
||||
|
|
@ -116,9 +114,9 @@ def _model_attempt(session, task: str, max_revisions: int, label: str, hint: str
|
|||
log(f"[{label}] Probleme gefunden – Korrekturrunde {revisions}/{max_revisions} ...")
|
||||
for grund in revision_reasons(review, issues):
|
||||
log(f" Grund: {grund}")
|
||||
spec = revise_model(session, task, spec, review, issues)
|
||||
spec = revise_model(session, task, spec, review, issues, data_prompt)
|
||||
issues = check_spec(spec, task, understanding)
|
||||
review = review_assumptions(session, task, spec)
|
||||
review = review_assumptions(session, task, spec, data_prompt)
|
||||
candidates.append((spec, issues, review))
|
||||
|
||||
selected = select_best(candidates)
|
||||
|
|
@ -147,7 +145,7 @@ INFEASIBLE_HINT = (
|
|||
|
||||
|
||||
def _recheck_infeasible(session, task: str, model: str, result: PipelineResult,
|
||||
max_revisions: int, solver_retries: int) -> PipelineResult:
|
||||
max_revisions: int, solver_retries: int, data_files=(), data_prompt: str = "") -> PipelineResult:
|
||||
"""'unlösbar' nicht ungeprüft glauben: unabhängig neu modellieren und erneut lösen."""
|
||||
status = result.solver_run.status.value
|
||||
if INFEASIBLE_RECHECK < 1:
|
||||
|
|
@ -157,7 +155,8 @@ def _recheck_infeasible(session, task: str, model: str, result: PipelineResult,
|
|||
|
||||
log(f"[{model}] Solver meldet '{status}' – Gegenprobe mit unabhängiger Modellierung ...")
|
||||
zweiter = _model_attempt(session, task, max_revisions, f"{model} Gegenprobe",
|
||||
hint=INFEASIBLE_HINT.format(status=status))
|
||||
hint=INFEASIBLE_HINT.format(status=status),
|
||||
understanding=result.understanding, data_prompt=data_prompt)
|
||||
if not zweiter["review"].objective_check.matches:
|
||||
log(" Gegenprobe verworfen: Zielfunktion der neuen Modellierung passt nicht zur Frage.")
|
||||
result.reliable = False
|
||||
|
|
@ -166,13 +165,16 @@ def _recheck_infeasible(session, task: str, model: str, result: PipelineResult,
|
|||
|
||||
spec = zweiter["spec"]
|
||||
log(f"[{model}] Gegenprobe: Solver-Code erzeugen und starten ({spec.target_stack.value}) ...")
|
||||
code, run = solve(session, task, spec, retries=solver_retries, log=log)
|
||||
code, run = solve(session, task, spec, retries=solver_retries, log=log, understanding=result.understanding,
|
||||
data_files=data_files, data_prompt=data_prompt)
|
||||
|
||||
if run.status in (SolverStatus.OPTIMAL, SolverStatus.FEASIBLE) and not run.feasibility:
|
||||
log(f" Gegenprobe widerlegt '{status}': Zielwert {run.objective_value} – diese Lösung wird verwendet.")
|
||||
result.specification, result.review, result.checks = spec, zweiter["review"], check_spec(spec, task)
|
||||
result.specification, result.review = spec, zweiter["review"]
|
||||
result.checks = check_spec(spec, task, result.understanding)
|
||||
result.solver_code, result.solver_run = code, run
|
||||
result.restarts += 1
|
||||
_assess_run(result) # die neue Lösung durchläuft dieselbe Abnahme wie die erste
|
||||
return result
|
||||
|
||||
if run.status.value == status:
|
||||
|
|
@ -199,6 +201,15 @@ def _solver_stage(session, task: str, result: PipelineResult, model: str,
|
|||
result.solver_code, result.solver_run = solve(session, task, spec, retries=solver_retries, log=log,
|
||||
understanding=result.understanding,
|
||||
data_files=data_files, data_prompt=data_prompt)
|
||||
_assess_run(result)
|
||||
if result.solver_run.status in (SolverStatus.INFEASIBLE, SolverStatus.UNBOUNDED):
|
||||
result = _recheck_infeasible(session, task, model, result, max_revisions, solver_retries,
|
||||
data_files=data_files, data_prompt=data_prompt)
|
||||
return result
|
||||
|
||||
|
||||
def _assess_run(result: PipelineResult) -> None:
|
||||
"""Abnahme eines Solver-Laufs: Befunde, die das Ergebnis unzuverlässig machen, in `result` übernehmen."""
|
||||
run = result.solver_run
|
||||
log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else ""))
|
||||
if run.feasibility:
|
||||
|
|
@ -220,10 +231,6 @@ def _solver_stage(session, task: str, result: PipelineResult, model: str,
|
|||
if result.solver_code.data_assumptions:
|
||||
log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).")
|
||||
|
||||
if run.status in (SolverStatus.INFEASIBLE, SolverStatus.UNBOUNDED):
|
||||
result = _recheck_infeasible(session, task, model, result, max_revisions, solver_retries)
|
||||
return result
|
||||
|
||||
|
||||
def _log_usage(session, result: PipelineResult) -> None:
|
||||
result.usage = session.usage
|
||||
|
|
@ -268,7 +275,7 @@ def run_pipeline(
|
|||
if figur.uncertain:
|
||||
log(f" {figur.id} – unsicher gelesen: " + "; ".join(figur.uncertain))
|
||||
task = with_transcriptions(task, understanding.figures)
|
||||
if not understanding.is_optimization_task:
|
||||
if understanding is not None and not understanding.is_optimization_task:
|
||||
# Keine Entscheidung verlangt: jedes Modell wäre erfunden, jede Zahl ohne Frage dahinter
|
||||
grund = f"Keine Optimierungsaufgabe: {understanding.reason}"
|
||||
log(f"ABGELEHNT – {grund} Es wird weder modelliert noch gerechnet.")
|
||||
|
|
@ -319,12 +326,14 @@ STAGES = ("pruefung", "solver")
|
|||
|
||||
def continue_pipeline(
|
||||
result_json: str, stage: str, model: str = DEFAULT_MODEL, max_revisions: int = MAX_REVISIONS,
|
||||
solver_retries: int = SOLVER_RETRIES, session: LLMSession | None = None,
|
||||
solver_retries: int = SOLVER_RETRIES, session: LLMSession | None = None, data_files=(),
|
||||
) -> PipelineResult:
|
||||
"""Gespeichertes Ergebnis ab einer Stufe fortsetzen – die davor liegenden LLM-Aufrufe entfallen.
|
||||
|
||||
'pruefung' behält die Spezifikation und prüft/korrigiert sie neu; 'solver' übernimmt auch den
|
||||
Prüfbericht und erzeugt nur den Solver-Code neu. Aufgabentext kommt aus dem gespeicherten Ergebnis.
|
||||
Prüfbericht und erzeugt nur den Solver-Code neu. Aufgabentext, Verständnis und Abbildungen kommen aus
|
||||
dem gespeicherten Ergebnis; beigelegte Daten (`data_files`) sind dort nur als Profil gespeichert und
|
||||
müssen erneut angegeben werden, sonst findet das Programm seine Dateien nicht.
|
||||
"""
|
||||
if stage not in STAGES:
|
||||
raise ValueError(f"Unbekannte Stufe '{stage}' (erlaubt: {', '.join(STAGES)})")
|
||||
|
|
@ -332,39 +341,45 @@ def continue_pipeline(
|
|||
if stored.rejected or stored.specification is None:
|
||||
raise ValueError(f"{result_json} ist eine abgelehnte Aufgabe (keine Optimierungsaufgabe) – nichts fortzusetzen")
|
||||
session = session or LLMSession(model)
|
||||
task = stored.task
|
||||
task, understanding = stored.task, stored.understanding
|
||||
profile = data_profile.profile_files(data_files) if data_files else stored.data_files
|
||||
data_prompt = data_profile.to_prompt(profile)
|
||||
if stored.data_files and not data_files:
|
||||
log("WARNUNG – das gespeicherte Ergebnis hatte beigelegte Daten; ohne --daten findet das Programm sie nicht.")
|
||||
|
||||
log(f"Setze {result_json} ab Stufe '{stage}' fort – Extraktion entfällt.")
|
||||
if stage == "solver":
|
||||
result = stored.model_copy(update={
|
||||
"checks": check_spec(stored.specification, task), # deterministisch, kostet nichts
|
||||
"solver_code": None, "solver_run": None, "usage": None,
|
||||
"reliable": True, "reliability_issues": [],
|
||||
})
|
||||
problems = _unresolved({"review": stored.review, "issues": result.checks})
|
||||
result.reliable, result.reliability_issues = not problems, problems
|
||||
checks = check_spec(stored.specification, task, understanding) # deterministisch, kostet nichts
|
||||
problems = _unresolved({"review": stored.review, "issues": checks})
|
||||
update = {"checks": checks}
|
||||
else:
|
||||
attempt = _model_attempt(session, task, max_revisions, model, spec=stored.specification)
|
||||
attempt = _model_attempt(session, task, max_revisions, model, spec=stored.specification,
|
||||
understanding=understanding, data_prompt=data_prompt)
|
||||
problems = _unresolved(attempt)
|
||||
result = PipelineResult(
|
||||
task=task, specification=attempt["spec"], review=attempt["review"], revisions=attempt["revisions"],
|
||||
selected_revision=attempt["selected"], checks=attempt["issues"],
|
||||
reliable=not problems, reliability_issues=problems,
|
||||
)
|
||||
update = {"specification": attempt["spec"], "review": attempt["review"], "revisions": attempt["revisions"],
|
||||
"selected_revision": attempt["selected"], "restarts": 0, "checks": attempt["issues"]}
|
||||
# Verständnis, Abbildungen und Datenprofil bleiben erhalten; alles ab der Fortsetzungsstufe entsteht neu
|
||||
result = stored.model_copy(update={
|
||||
**update, "data_files": profile, "solver_code": None, "solver_run": None, "usage": None,
|
||||
"infeasible_confirmed": False, "reliable": not problems, "reliability_issues": problems,
|
||||
})
|
||||
for issue in result.checks:
|
||||
log(f" {issue.severity.value.upper()} [{issue.code}] {issue.message}")
|
||||
if result.reliability_issues:
|
||||
log("ERGEBNIS UNZUVERLÄSSIG: " + "; ".join(result.reliability_issues))
|
||||
|
||||
result = _solver_stage(session, task, result, model, max_revisions, solver_retries)
|
||||
result = _solver_stage(session, task, result, model, max_revisions, solver_retries,
|
||||
data_files=data_files, data_prompt=data_prompt)
|
||||
_log_usage(session, result)
|
||||
return result
|
||||
|
||||
|
||||
def resolve_result(result_json: str, data_file: str | None = None, solver_timeout: int = 60) -> PipelineResult:
|
||||
def resolve_result(result_json: str, data_file: str | None = None, solver_timeout: int = 60,
|
||||
data_files=()) -> PipelineResult:
|
||||
"""Gespeichertes Ergebnis mit (geänderten) Daten neu rechnen – ohne einen einzigen LLM-Aufruf.
|
||||
|
||||
Das erzeugte Programm hält alle Eingabezahlen in DATA; nur diese werden ersetzt.
|
||||
Das erzeugte Programm hält alle Eingabezahlen in DATA; nur diese werden ersetzt. Liest es beigelegte
|
||||
Dateien, müssen sie über `data_files` erneut angegeben werden.
|
||||
"""
|
||||
result = PipelineResult.model_validate_json(Path(result_json).read_text(encoding="utf-8"))
|
||||
if not result.solver_code:
|
||||
|
|
@ -372,14 +387,16 @@ def resolve_result(result_json: str, data_file: str | None = None, solver_timeou
|
|||
data = json.loads(Path(data_file).read_text(encoding="utf-8")) if data_file else None
|
||||
|
||||
log(f"Rechne {result_json} neu" + (f" mit Daten aus {data_file}" if data_file else "") + " – ohne LLM ...")
|
||||
run = run_solver_code(result.solver_code.code, timeout=solver_timeout, data=data)
|
||||
run = run_solver_code(result.solver_code.code, timeout=solver_timeout, data=data, data_files=data_files)
|
||||
run.feasibility = check_feasibility(run, result.specification)
|
||||
run.warnings += check_sensitivity(run, result.specification.objective.sense.value)
|
||||
run.warnings += check_multistart(run, result.specification.objective.sense.value)
|
||||
|
||||
result.solver_run = run
|
||||
result.reliable = not run.feasibility
|
||||
result.reliability_issues = [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility]
|
||||
# Frühere Befunde (Zielgröße, Abbildung, Datenherkunft) gelten weiter – nur die Zulässigkeit ist neu geprüft
|
||||
result.reliability_issues = ([p for p in result.reliability_issues if not p.startswith("Zulässigkeitsprüfung: ")]
|
||||
+ [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility])
|
||||
result.reliable = not result.reliability_issues
|
||||
result.usage = None # keine LLM-Nutzung in diesem Lauf
|
||||
log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else ""))
|
||||
for problem in run.feasibility:
|
||||
|
|
@ -416,7 +433,8 @@ if __name__ == "__main__":
|
|||
"'solver' (auch den Prüfbericht behalten, nur Solver-Code neu erzeugen)")
|
||||
parser.add_argument("--daten", nargs="+", metavar="DATEI",
|
||||
help="beigelegte Datendateien (CSV/Excel); ihr Profil geht in die Prompts, "
|
||||
"die Dateien selbst ins Arbeitsverzeichnis des Programms")
|
||||
"die Dateien selbst ins Arbeitsverzeichnis des Programms "
|
||||
"(mit --resolve/--von erneut angeben, gespeichert ist nur ihr Profil)")
|
||||
parser.add_argument("--data", metavar="DATEN.JSON",
|
||||
help="mit --resolve: Eingabewerte (DATA) des Programms ganz oder teilweise ersetzen")
|
||||
parser.add_argument("--report", metavar="DATEI", help="Report mit gerenderten Formeln und Solver-Code (.md oder .html)")
|
||||
|
|
@ -439,9 +457,10 @@ if __name__ == "__main__":
|
|||
parser.error("--von endet immer mit der Solver-Stufe; --no-solve wäre wirkungslos")
|
||||
|
||||
if args.resolve:
|
||||
result = resolve_result(args.resolve, args.data)
|
||||
result = resolve_result(args.resolve, args.data, data_files=args.daten or ())
|
||||
elif args.von:
|
||||
result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions)
|
||||
result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions,
|
||||
data_files=args.daten or ())
|
||||
else:
|
||||
try:
|
||||
result = run_pipeline(
|
||||
|
|
|
|||
Loading…
Reference in a new issue