import argparse import json import os import sys from pathlib import Path from typing import List from llm import DEFAULT_MODEL, LLMSession import data_profile from model_checks import check_spec, check_understanding, has_errors from model_schema import ( AssumptionReview, SolverStatus, CheckIssue, CheckSeverity, OptimizationModelSpecification, PipelineResult, TaskUnderstanding, ) from report import write_report from script_export import build_solver_script from solver_stage import check_feasibility, check_multistart, check_sensitivity, run_solver_code, solve PROMPT_DIR = Path(__file__).parent ROUTING_PROMPT_FILE = PROMPT_DIR / "Routing-und-Extraktions-Prompt.txt" REVIEW_PROMPT_FILE = PROMPT_DIR / "Pruef-Prompt.txt" UNDERSTAND_PROMPT_FILE = PROMPT_DIR / "Verstehen-Prompt.txt" MAX_REVISIONS = int(os.environ.get("MAX_REVISIONS", "2")) SOLVER_RETRIES = int(os.environ.get("SOLVER_RETRIES", "2")) 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) UNDERSTAND_RETRIES = int(os.environ.get("UNDERSTAND_RETRIES", "1")) task_description = """ Ein Logistikzentrum muss 12 Mitarbeiter auf 3 Acht-Stunden-Schichten verteilen. In Schicht 1 werden mindestens 3 Mitarbeiter benötigt, in Schicht 2 mindestens 5 und in Schicht 3 genau 2. Kein Mitarbeiter darf zwei aufeinanderfolgende Schichten arbeiten. Zudem gilt: Wenn Mitarbeiter A arbeitet, muss Mitarbeiter B in derselben Schicht sein. Die Gesamtkosten für Schichtzulagen sollen minimiert werden. """ def log(msg: str) -> None: # stderr, damit stdout reines JSON bleibt print(msg, file=sys.stderr) def understand_task(session: LLMSession, task: str, issues: List[CheckIssue] = (), data_prompt: str = "") -> TaskUnderstanding: """Stufe 0: erfassen, was im Text steht – Teilfragen, Zahlen, Machbarkeit. Noch kein Modell.""" nachtrag = "" if issues: nachtrag = ("\n\nDEIN VORIGER VERSUCH WAR UNVOLLSTÄNDIG:\n" + "\n".join(f"- [{i.code}] {i.message}" for i in issues) + "\nErfasse die fehlenden Punkte, behalte den Rest bei.") return session.create( TaskUnderstanding, [ {"role": "system", "content": UNDERSTAND_PROMPT_FILE.read_text(encoding="utf-8")}, {"role": "user", "content": f"AUFGABENTEXT:\n{task}\n\n{data_prompt}{nachtrag}".strip()}, ], step="Verstehen", ) def text_to_model(session: LLMSession, task: str, understanding: TaskUnderstanding | None = None, data_prompt: str = "") -> OptimizationModelSpecification: inhalt = task if not data_prompt else f"{task}\n\n{data_prompt}" if understanding is not None: # Die Verstehensstufe hat Teilfragen und Zahlen schon erfasst – die Extraktion muss den Text # nicht noch einmal danach absuchen, sondern baut darauf auf. inhalt += ("\n\nBEREITS ERFASSTES VERSTÄNDNIS (Teilfragen und Zahlen; übernimm die Werte aus facts " "in extracted_values, statt sie neu zu suchen):\n" + understanding.model_dump_json(indent=2)) return session.create( OptimizationModelSpecification, [ {"role": "system", "content": ROUTING_PROMPT_FILE.read_text(encoding="utf-8")}, {"role": "user", "content": inhalt}, ], step="Extraktion", ) 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", ) def revise_model( session: LLMSession, task: str, spec: OptimizationModelSpecification, review: AssumptionReview, issues: List[CheckIssue] = (), ) -> OptimizationModelSpecification: errors = [i for i in issues if i.severity == CheckSeverity.ERROR] check_text = "\n".join(f"- [{i.code}] {i.message}" for i in errors) or "keine" # Warnungen lösen für sich keine Korrekturrunde aus; findet aber ohnehin eine statt, # sollen sie mit behoben werden – sonst erreicht z. B. unerklärter Fachjargon das LLM nie. hinweise = [i for i in issues if i.severity == CheckSeverity.WARNING] hinweis_text = "\n".join(f"- [{i.code}] {i.message}" for i in hinweise) or "keine" return session.create( OptimizationModelSpecification, [ {"role": "system", "content": ROUTING_PROMPT_FILE.read_text(encoding="utf-8")}, { "role": "user", "content": ( f"AUFGABENTEXT:\n{task}\n\n" f"BISHERIGE MODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}\n\n" f"PRÜFBERICHT:\n{review.model_dump_json(indent=2)}\n\n" f"FEHLER DER AUTOMATISCHEN KONSISTENZPRÜFUNG:\n{check_text}\n\n" f"HINWEISE DER AUTOMATISCHEN PRÜFUNG (kein Fehler, aber zu verbessern):\n{hinweis_text}\n\n" "Überarbeite die Modellspezifikation: Setze alle Korrekturen aus dem Prüfbericht um, " "behebe alle Fehler der Konsistenzprüfung, passe die betroffenen Restriktionen, Variablen " "und Parameter an und weise fehlende Annahmen in open_assumptions aus. " "Arbeite auch die Hinweise ab, soweit das Modell dadurch nicht schlechter wird – " "unerklärte Fachbegriffe gehören in executive_summary, nicht weggelassen. " "Behalte korrekte Teile unverändert bei." ), }, ], step="Korrektur", ) # 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 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) def _understand(session, task: str, label: str, data_prompt: str = "") -> tuple: """Verstehensstufe samt Prüfung; ein Nachfassen, wenn Zahlen oder Teilfragen fehlen.""" if UNDERSTAND_RETRIES < 0: return None, [] log(f"[{label}] Lese die Aufgabe (Teilfragen, Zahlen) ...") understanding = understand_task(session, task, data_prompt=data_prompt) issues = check_understanding(understanding, task) for versuch in range(UNDERSTAND_RETRIES): if not issues: break log(f"[{label}] Verständnis unvollständig – fasse nach ({versuch + 1}/{UNDERSTAND_RETRIES}): " + "; ".join(i.code for i in issues)) understanding = understand_task(session, task, issues, data_prompt) issues = check_understanding(understanding, task) if understanding.sub_questions: log(f" {len(understanding.sub_questions)} Teilfrage(n): " + ", ".join(q.id for q in understanding.sub_questions) + f"; {len(understanding.facts)} Zahlenwerte erfasst") if not understanding.is_optimization_task: log(f" WARNUNG: Das ist laut Verständnis keine Optimierungsaufgabe – {understanding.reason}") return understanding, issues def _model_attempt(session, task: str, max_revisions: int, label: str, hint: str = "", spec: OptimizationModelSpecification | None = None, understanding: TaskUnderstanding | None = None, data_prompt: str = "") -> dict: """Ein Modellierungsversuch: Extraktion, Checks, Prüfung, Korrekturrunden, beste Runde. Mit `spec` wird die Extraktion übersprungen – Einstieg ab der Prüfung (siehe continue_pipeline). """ if spec is None: log(f"[{label}] Extrahiere Modell ...") spec = text_to_model(session, task + hint, understanding, data_prompt) else: 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) candidates = [(spec, issues, review)] revisions = 0 while needs_work(review, issues) and revisions < max_revisions: revisions += 1 log(f"[{label}] Probleme gefunden – Korrekturrunde {revisions}/{max_revisions} ...") spec = revise_model(session, task, spec, review, issues) issues = check_spec(spec, task, understanding) review = review_assumptions(session, task, spec) candidates.append((spec, issues, review)) selected = select_best(candidates) if selected != len(candidates) - 1: log(f"[{label}] Spätere Korrekturrunden haben das Modell verschlechtert – verwende Stand aus Runde {selected}.") spec, issues, review = candidates[selected] return {"spec": spec, "issues": issues, "review": review, "revisions": revisions, "selected": selected} def _unresolved(attempt: dict) -> List[str]: """Probleme, die nach allen Korrekturrunden einen Neustart rechtfertigen.""" problems = [] if not attempt["review"].objective_check.matches: oc = attempt["review"].objective_check problems.append(f"Zielfunktion passt nicht zur Frage (gefragt: {oc.asked_quantity}; modelliert: {oc.modeled_quantity})") problems += [f"[{i.code}] {i.message}" for i in attempt["issues"] if i.severity == CheckSeverity.ERROR] return problems INFEASIBLE_HINT = ( "\n\nHINWEIS: Eine erste, unabhängige Modellierung dieser Aufgabe ergab '{status}'. " "Modelliere die Aufgabe daher völlig neu und prüfe dabei besonders, ob eine Bedingung zu streng ausgelegt " "wurde (z. B. eine Gleichung, wo der Text einen Spielraum zulässt, oder eine Grenze, die der Text nicht " "verlangt). Ist die Aufgabe wirklich unlösbar, modelliere sie trotzdem getreu." ) def _recheck_infeasible(session, task: str, model: str, result: PipelineResult, max_revisions: int, solver_retries: int) -> 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: result.reliable = False result.reliability_issues.append(f"Solver meldet '{status}' – ohne Gegenprobe (INFEASIBLE_RECHECK=0)") return result 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)) if not zweiter["review"].objective_check.matches: log(" Gegenprobe verworfen: Zielfunktion der neuen Modellierung passt nicht zur Frage.") result.reliable = False result.reliability_issues.append(f"Solver meldet '{status}'; die Gegenprobe war unbrauchbar") return result 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) 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.solver_code, result.solver_run = code, run result.restarts += 1 return result if run.status.value == status: log(f" Gegenprobe bestätigt '{status}' mit einer unabhängigen Modellierung.") result.infeasible_confirmed = True return result log(f" Gegenprobe nicht verwertbar (Status '{run.status.value}'), erste Modellierung bleibt.") result.reliable = False result.reliability_issues.append(f"Solver meldet '{status}'; die Gegenprobe endete mit '{run.status.value}'") return result def _solver_stage(session, task: str, result: PipelineResult, model: str, max_revisions: int, solver_retries: int, data_files=(), data_prompt: str = "") -> PipelineResult: """Solver-Code erzeugen, ausführen, prüfen – gemeinsam für run_pipeline und continue_pipeline.""" spec = result.specification if not result.review.objective_check.matches: # Der Solver würde ein anderes als das gefragte Problem lösen – ein "optimal" wäre irreführend log(f"[{model}] Solver wird nicht gestartet, weil die Zielfunktion nicht zur Frage passt.") return result log(f"[{model}] Erzeuge und starte Solver-Code ({spec.target_stack.value}) ...") 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) 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: result.reliable = False result.reliability_issues += [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility] log("ERGEBNIS UNZUVERLÄSSIG – die Lösung hält das Modell nicht ein:") for problem in run.feasibility: log(f" {problem}") elif run.constraints: log(f" Zulässigkeit geprüft: {len(run.constraints)} Restriktionen, {len(run.variables)} Variablen – alles eingehalten.") if run.undeclared_data: result.reliable = False result.reliability_issues += [f"Datenherkunft: {p}" for p in run.undeclared_data] log("ERGEBNIS UNZUVERLÄSSIG – selbst erzeugte Eingabedaten sind nicht ausgewiesen.") 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 cost = f", Kosten ${session.usage.cost_usd:.4f}" if session.usage.cost_usd is not None else "" providers = f", Anbieter: {session.usage.providers}" if session.usage.providers else "" log(f"LLM-Nutzung: {session.usage.calls} Aufrufe, {session.usage.prompt_tokens} Input-/" f"{session.usage.completion_tokens} Output-Tokens (davon {session.usage.reasoning_tokens} Reasoning){cost}{providers}") for step, data in session.usage.by_step().items(): log(f" {step:18} {data['calls']}x {data['seconds']:6.1f} s {data['completion_tokens']:6} Output-Tokens " f"(davon {data['reasoning_tokens']} Reasoning)") def run_pipeline( task: str, model: str = DEFAULT_MODEL, max_revisions: int = MAX_REVISIONS, solve_model: bool = True, solver_retries: int = SOLVER_RETRIES, max_restarts: int = MAX_RESTARTS, session: LLMSession | None = None, data_files=(), ) -> PipelineResult: session = session or LLMSession(model) profile = data_profile.profile_files(data_files) data_prompt = data_profile.to_prompt(profile) for p in profile: log(f"Datei {p.name}: " + (p.error if p.error else f"{p.rows} Zeilen, {len(p.columns)} Spalten")) for step, step_model in session.overrides().items(): log(f"[{model}] Stufe '{step}' läuft mit abweichendem Modell: {step_model}") understanding, verstehen_issues = _understand(session, task, model, data_prompt) attempts = [_model_attempt(session, task, max_revisions, model, understanding=understanding, data_prompt=data_prompt)] # Beobachtet: das LLM modelliert gelegentlich ein Fremdproblem; Korrekturrunden reparieren das nicht, # ein frischer Neustart der Extraktion schon eher. while _unresolved(attempts[-1]) and len(attempts) <= max_restarts: log(f"[{model}] Modell nach allen Korrekturrunden fehlerhaft – Neustart {len(attempts)}/{max_restarts}: " + "; ".join(_unresolved(attempts[-1]))[:300]) attempts.append(_model_attempt(session, task, max_revisions, f"{model} Neustart {len(attempts)}", understanding=understanding, data_prompt=data_prompt)) best = attempts[select_best([(a["spec"], a["issues"], a["review"]) for a in attempts])] spec, issues, review = best["spec"], best["issues"] + verstehen_issues, best["review"] problems = _unresolved(best) for issue in issues: log(f" {issue.severity.value.upper()} [{issue.code}] {issue.message}") if problems: log("ERGEBNIS UNZUVERLÄSSIG: " + "; ".join(problems)) elif review.needs_revision: log("WARNUNG: Die Prüfung hat noch Anmerkungen zu Annahmen.") else: log("Prüfung ohne Beanstandung.") result = PipelineResult( task=task, understanding=understanding, data_files=profile, specification=spec, review=review, revisions=best["revisions"], selected_revision=best["selected"], restarts=len(attempts) - 1, checks=issues, reliable=not problems, reliability_issues=problems, ) if solve_model: 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 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, ) -> 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. """ if stage not in STAGES: raise ValueError(f"Unbekannte Stufe '{stage}' (erlaubt: {', '.join(STAGES)})") stored = PipelineResult.model_validate_json(Path(result_json).read_text(encoding="utf-8")) session = session or LLMSession(model) task = stored.task 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 else: attempt = _model_attempt(session, task, max_revisions, model, spec=stored.specification) 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, ) 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) _log_usage(session, result) return result def resolve_result(result_json: str, data_file: str | None = None, solver_timeout: int = 60) -> 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. """ result = PipelineResult.model_validate_json(Path(result_json).read_text(encoding="utf-8")) if not result.solver_code: raise ValueError(f"{result_json} enthält keinen Solver-Code (Lauf mit --no-solve?)") 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.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] 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: log(f" {problem}") return result def read_task(task_file: str | None) -> str: if task_file: task = Path(task_file).read_text(encoding="utf-8") elif not sys.stdin.isatty(): task = sys.stdin.read() else: task = "" # Leere Eingabe würde das LLM ein beliebiges Problem erfinden lassen return task if task.strip() else task_description if __name__ == "__main__": parser = argparse.ArgumentParser(description="Textaufgabe → Optimierungsmodell → Prüfung → Lösung") parser.add_argument("task_file", nargs="?", help="Datei mit der Aufgabe (sonst stdin, sonst Beispielaufgabe)") parser.add_argument("--no-solve", action="store_true", help="nur modellieren und prüfen, keinen Solver-Code ausführen") parser.add_argument("--max-revisions", type=int, default=MAX_REVISIONS) parser.add_argument("--max-restarts", type=int, default=MAX_RESTARTS, help="komplette Neustarts, wenn das Modell nach allen Korrekturrunden fehlerhaft ist") parser.add_argument( "--model", default=DEFAULT_MODEL, help=f"Modell (Default: {DEFAULT_MODEL}, Umgebung: LLM_MODEL); mit '/' -> OpenRouter, sonst Gemini", ) parser.add_argument("--resolve", metavar="ERGEBNIS.JSON", help="gespeichertes Ergebnis erneut rechnen, ohne LLM (z. B. nach Datenänderung)") parser.add_argument("--von", nargs=2, metavar=("STUFE", "ERGEBNIS.JSON"), help="gespeichertes Ergebnis ab einer Stufe fortsetzen, statt alles zu wiederholen: " "'pruefung' (Spezifikation behalten, neu prüfen und korrigieren) oder " "'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") 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)") parser.add_argument("--code", metavar="DATEI", help="erzeugten Solver-Code als eigenständiges Python-Skript speichern") args = parser.parse_args() if args.report and Path(args.report).suffix.lower() not in (".md", ".html", ".htm"): parser.error("--report braucht die Endung .md oder .html") if args.code and args.no_solve: parser.error("--code ist mit --no-solve nicht möglich (es wird kein Solver-Code erzeugt)") if args.data and not args.resolve: parser.error("--data wirkt nur zusammen mit --resolve") if args.von: if args.resolve: parser.error("--von und --resolve schließen sich aus") if args.von[0] not in STAGES: parser.error(f"--von braucht eine Stufe aus {{{', '.join(STAGES)}}}, nicht '{args.von[0]}'") if args.task_file: parser.error("--von nimmt die Aufgabe aus dem gespeicherten Ergebnis – keine Aufgabendatei angeben") if args.no_solve: parser.error("--von endet immer mit der Solver-Stufe; --no-solve wäre wirkungslos") if args.resolve: result = resolve_result(args.resolve, args.data) elif args.von: result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions) else: result = run_pipeline( read_task(args.task_file), model=args.model, max_revisions=args.max_revisions, solve_model=not args.no_solve, max_restarts=args.max_restarts, data_files=args.daten or (), ) print(result.model_dump_json(indent=2)) if args.report: write_report(result, args.report) log(f"Report geschrieben: {args.report}") if args.code and result.solver_code: Path(args.code).write_text(build_solver_script(result), encoding="utf-8") Path(args.code).chmod(0o755) log(f"Solver-Code geschrieben: {args.code} (ausführen: .venv/bin/python {args.code})") elif args.code: log(f"Kein Solver-Code geschrieben ({args.code}): Solver wurde nicht gestartet.") if not result.reliable: sys.exit(2) # für Skripte: Ergebnis vorhanden, aber unzuverlässig # Erwartet für das Beispiel: # result.specification.target_stack -> TargetStack.OR_TOOLS_CPSAT # result.specification.problem_class -> ProblemClass.CP