feat: TextToModel.py als CLI-Programm mit main(), gruppierten Optionen und Defaults
- main(argv) -> int, build_parser() mit Gruppen Eingabe/Modus/Ablauf/LLM/Solver/Ausgabe, Hilfe mit Defaults, Umgebungsvariablen, Beispielen und Exit-Codes - Optionen für bisher reine Umgebungsvariablen (--solver-timeout, --sandbox, --max-tokens, --llm-timeout, --reasoning, --solver-retries, --understand-retries, --infeasible-recheck) - -o/--json und -a/--ausgabe PRÄFIX; im Terminal Kurzfassung statt JSON - Exit-Codes: 1 Eingabefehler, 64 Aufruffehler (nicht argparse' 2 = unzuverlässig), 130 Abbruch - solver_stage: Zeitlimit-Default zur Aufrufzeit auflösen, damit --solver-timeout wirkt - tests/test_cli.py Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f101b63947
commit
422360ffa5
5 changed files with 382 additions and 70 deletions
|
|
@ -133,8 +133,11 @@ Die Aufgabe kann als `.txt` oder `.md` vorliegen; Markdown-Tabellen und Formeln
|
||||||
## 5. Aufrufvarianten
|
## 5. Aufrufvarianten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Aufgabe aus einer Datei
|
# Aufgabe aus einer Datei – JSON, Report und Solver-Programm auf einen Schlag
|
||||||
.venv/bin/python TextToModel.py aufgabe.md --report output/aufgabe.html > output/aufgabe.json
|
.venv/bin/python TextToModel.py aufgabe.md --ausgabe output/aufgabe # output/aufgabe.json, .html, .py
|
||||||
|
|
||||||
|
# dasselbe einzeln
|
||||||
|
.venv/bin/python TextToModel.py aufgabe.md -o output/aufgabe.json --report output/aufgabe.html
|
||||||
|
|
||||||
# Aufgabe über die Standardeingabe
|
# Aufgabe über die Standardeingabe
|
||||||
echo "Ein Betrieb …" | .venv/bin/python TextToModel.py --report output/aufgabe.md > output/aufgabe.json
|
echo "Ein Betrieb …" | .venv/bin/python TextToModel.py --report output/aufgabe.md > output/aufgabe.json
|
||||||
|
|
@ -156,7 +159,10 @@ Hinweise:
|
||||||
|
|
||||||
- `--report` braucht die Endung `.html`, `.htm` oder `.md`.
|
- `--report` braucht die Endung `.html`, `.htm` oder `.md`.
|
||||||
- `--code` ist mit `--no-solve` nicht möglich – es entsteht dann kein Programm.
|
- `--code` ist mit `--no-solve` nicht möglich – es entsteht dann kein Programm.
|
||||||
- Das **Ergebnis-JSON geht auf die Standardausgabe**. Ohne `> datei.json` erscheint es im Terminal und ist danach für `--resolve` und `--von` nicht verfügbar. Es empfiehlt sich, es immer umzuleiten.
|
- Das **Ergebnis-JSON** schreibt `-o datei.json` (oder `--ausgabe PRÄFIX`) in eine Datei; fehlende Ordner werden angelegt. Ohne diese Option geht es auf die Standardausgabe, wenn sie umgeleitet ist (`> datei.json`). Im Terminal erscheint stattdessen nur eine Kurzfassung (Status, Zielwert, Antworten, Zuverlässigkeit) – das JSON ist dann für `--resolve` und `--von` nicht gespeichert. Es empfiehlt sich, immer `-o` oder `--ausgabe` zu verwenden.
|
||||||
|
- `--ausgabe PRÄFIX` ist die Kurzform für `-o PRÄFIX.json --report PRÄFIX.html --code PRÄFIX.py`; ausdrücklich angegebene Einzeloptionen haben Vorrang.
|
||||||
|
- Aufgabe über die Standardeingabe: `-` als Aufgabendatei oder einfach umleiten.
|
||||||
|
- `.venv/bin/python TextToModel.py --help` zeigt alle Optionen mit ihren Defaults und Beispielen.
|
||||||
- Der Ordner `output/` ist von Git ausgeschlossen und eignet sich für alle Ergebnisse.
|
- Der Ordner `output/` ist von Git ausgeschlossen und eignet sich für alle Ergebnisse.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -209,9 +215,12 @@ print(d['reliable'], r['status'], r['objective_value']); [print(a['question_id']
|
||||||
| Code | Bedeutung |
|
| Code | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `0` | Ergebnis zuverlässig |
|
| `0` | Ergebnis zuverlässig |
|
||||||
|
| `1` | Fehler bei der Eingabe: Datei unlesbar, Abbildung zu groß, gespeichertes Ergebnis ungültig |
|
||||||
| `2` | Ergebnis vorhanden, aber **unzuverlässig** – Gründe in `reliability_issues` und im Warnkasten des Reports |
|
| `2` | Ergebnis vorhanden, aber **unzuverlässig** – Gründe in `reliability_issues` und im Warnkasten des Reports |
|
||||||
| `3` | **abgelehnt** – keine Optimierungsaufgabe, nichts gerechnet |
|
| `3` | **abgelehnt** – keine Optimierungsaufgabe, nichts gerechnet |
|
||||||
| andere | Programmfehler oder Abbruch (z. B. ungültiger API-Schlüssel), Meldung im Terminal |
|
| `64` | falscher Aufruf (unbekannte Option, fehlende Datei, unverträgliche Optionen) |
|
||||||
|
| `130` | mit Strg+C abgebrochen |
|
||||||
|
| andere | Programmfehler (z. B. ungültiger API-Schlüssel), Meldung im Terminal |
|
||||||
|
|
||||||
Damit lässt sich das Programm in Skripten einsetzen:
|
Damit lässt sich das Programm in Skripten einsetzen:
|
||||||
|
|
||||||
|
|
@ -416,10 +425,19 @@ Wo das Geld hingeht: rund zwei Drittel der Kosten sind **erzeugter Output**, dav
|
||||||
|
|
||||||
| Option | Bedeutung |
|
| Option | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AUFGABE` | Aufgabendatei (`.txt`, `.md`); ohne: Standardeingabe, sonst Beispielaufgabe |
|
| `AUFGABE` | Aufgabendatei (`.txt`, `.md`); `-` oder ohne: Standardeingabe, sonst Beispielaufgabe |
|
||||||
|
| `-o`, `--json DATEI` | Ergebnis-JSON in eine Datei (sonst stdout bzw. Kurzfassung im Terminal) |
|
||||||
| `--report DATEI` | Report als `.html`/`.htm` oder `.md` |
|
| `--report DATEI` | Report als `.html`/`.htm` oder `.md` |
|
||||||
| `--code DATEI` | Solver-Programm als ausführbares Skript speichern |
|
| `--code DATEI` | Solver-Programm als ausführbares Skript speichern |
|
||||||
| `--model NAME` | LLM-Modell (mit `/` → OpenRouter, sonst Gemini) |
|
| `-a`, `--ausgabe PRÄFIX` | Kurzform für `-o PRÄFIX.json --report PRÄFIX.html --code PRÄFIX.py` |
|
||||||
|
| `-m`, `--model NAME` | LLM-Modell (mit `/` → OpenRouter, sonst Gemini) |
|
||||||
|
| `--reasoning low\|medium\|high` | Denkaufwand (wie `LLM_REASONING_EFFORT`) |
|
||||||
|
| `--max-tokens N`, `--llm-timeout SEK` | wie `LLM_MAX_TOKENS`, `LLM_TIMEOUT` |
|
||||||
|
| `--solver-retries N` | Reparaturen des Solver-Codes (wie `SOLVER_RETRIES`) |
|
||||||
|
| `--understand-retries N` | Nachfassen beim Verstehen, `-1` = Stufe aus (wie `UNDERSTAND_RETRIES`) |
|
||||||
|
| `--infeasible-recheck 0\|1` | Gegenprobe bei „unlösbar“ (wie `INFEASIBLE_RECHECK`) |
|
||||||
|
| `--solver-timeout SEK` | Zeitlimit je Programmlauf (wie `SOLVER_TIMEOUT`) |
|
||||||
|
| `--sandbox auto\|bwrap\|aus` | Isolierung des Solver-Codes (wie `SOLVER_SANDBOX`) |
|
||||||
| `--daten DATEI …` | beigelegte CSV-/Excel-Dateien |
|
| `--daten DATEI …` | beigelegte CSV-/Excel-Dateien |
|
||||||
| `--no-solve` | keinen Solver-Code erzeugen |
|
| `--no-solve` | keinen Solver-Code erzeugen |
|
||||||
| `--max-revisions N` | Korrekturrunden |
|
| `--max-revisions N` | Korrekturrunden |
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,11 @@ Der Anbieter wird am Modellnamen erkannt. Das kostenlose Gemini-Kontingent reich
|
||||||
Ausführliche Schritt-für-Schritt-Anleitung – Aufgaben formulieren, Ergebnis lesen, Weiterarbeiten ohne LLM-Kosten, Fehlerbehebung: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**.
|
Ausführliche Schritt-für-Schritt-Anleitung – Aufgaben formulieren, Ergebnis lesen, Weiterarbeiten ohne LLM-Kosten, Fehlerbehebung: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Aufgabe aus Datei lösen, Report und Solver-Code speichern
|
# Aufgabe aus Datei lösen: output/ergebnis.json, .html (Report) und .py (Solver-Programm)
|
||||||
.venv/bin/python TextToModel.py aufgabe.txt --report output/modell.html --code output/solve.py > output/ergebnis.json
|
.venv/bin/python TextToModel.py aufgabe.txt --ausgabe output/ergebnis
|
||||||
|
|
||||||
|
# alle Optionen mit Defaults und Beispielen
|
||||||
|
.venv/bin/python TextToModel.py --help
|
||||||
|
|
||||||
# Aufgabe über stdin
|
# Aufgabe über stdin
|
||||||
echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report output/modell.md
|
echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report output/modell.md
|
||||||
|
|
@ -96,7 +99,7 @@ echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report output/modell.
|
||||||
.venv/bin/python TextToModel.py --von solver output/ergebnis.json > output/neu.json
|
.venv/bin/python TextToModel.py --von solver output/ergebnis.json > output/neu.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Exit-Codes: `0` Ergebnis zuverlässig, `2` Ergebnis vorhanden, aber unzuverlässig, `3` abgelehnt – der Text ist keine Optimierungsaufgabe (es wird weder modelliert noch gerechnet; Report und JSON nennen den Grund). Der Ordner `output/` ist in `.gitignore` eingetragen. Ohne Eingabe wird eine eingebaute Beispielaufgabe verwendet. Fortschritt und Kosten erscheinen auf stderr, das Ergebnis-JSON auf stdout.
|
Exit-Codes: `0` Ergebnis zuverlässig, `1` Eingabefehler (Datei, Abbildung, gespeichertes Ergebnis), `2` Ergebnis vorhanden, aber unzuverlässig, `64` falscher Aufruf, `3` abgelehnt – der Text ist keine Optimierungsaufgabe (es wird weder modelliert noch gerechnet; Report und JSON nennen den Grund). Der Ordner `output/` ist in `.gitignore` eingetragen. Ohne Eingabe wird eine eingebaute Beispielaufgabe verwendet. Fortschritt und Kosten erscheinen auf stderr; das Ergebnis-JSON geht mit `-o`/`--ausgabe` in eine Datei, sonst auf eine umgeleitete Standardausgabe – im Terminal erscheint stattdessen eine Kurzfassung. Umgebungsvariablen wie `SOLVER_TIMEOUT` haben gleichnamige Optionen (`--solver-timeout`), die Vorrang haben.
|
||||||
|
|
||||||
| Option / Variable | Bedeutung | Standard |
|
| Option / Variable | Bedeutung | Standard |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|
|
||||||
280
TextToModel.py
280
TextToModel.py
|
|
@ -7,6 +7,7 @@ from typing import List
|
||||||
|
|
||||||
from checks.model_checks import check_figure_readings, check_spec, check_understanding
|
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 checks.review import needs_work, review_assumptions, revision_reasons, select_best
|
||||||
|
from core import llm
|
||||||
from core.llm import DEFAULT_MODEL, LLMSession
|
from core.llm import DEFAULT_MODEL, LLMSession
|
||||||
from core.model_schema import (
|
from core.model_schema import (
|
||||||
SolverStatus,
|
SolverStatus,
|
||||||
|
|
@ -18,8 +19,9 @@ from core.model_schema import (
|
||||||
from data import data_profile
|
from data import data_profile
|
||||||
from modeling.extraction import revise_model, text_to_model
|
from modeling.extraction import revise_model, text_to_model
|
||||||
from reporting.report import write_report
|
from reporting.report import write_report
|
||||||
|
from solving import solver_stage
|
||||||
from solving.script_export import build_solver_script
|
from solving.script_export import build_solver_script
|
||||||
from solving.solver_stage import (SOLVER_TIMEOUT, check_feasibility, check_multistart, check_sensitivity,
|
from solving.solver_stage import (check_feasibility, check_multistart, check_sensitivity,
|
||||||
run_solver_code, solve)
|
run_solver_code, solve)
|
||||||
from understanding.figures import FigureError, normalize_figure_ids, split_figures, with_transcriptions
|
from understanding.figures import FigureError, normalize_figure_ids, split_figures, with_transcriptions
|
||||||
from understanding.understand import read_figures, understand_task
|
from understanding.understand import read_figures, understand_task
|
||||||
|
|
@ -386,7 +388,7 @@ def continue_pipeline(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def resolve_result(result_json: str, data_file: str | None = None, solver_timeout: int = SOLVER_TIMEOUT,
|
def resolve_result(result_json: str, data_file: str | None = None, solver_timeout: int | None = None,
|
||||||
data_files=()) -> PipelineResult:
|
data_files=()) -> PipelineResult:
|
||||||
"""Gespeichertes Ergebnis mit (geänderten) Daten neu rechnen – ohne einen einzigen LLM-Aufruf.
|
"""Gespeichertes Ergebnis mit (geänderten) Daten neu rechnen – ohne einen einzigen LLM-Aufruf.
|
||||||
|
|
||||||
|
|
@ -420,86 +422,250 @@ def resolve_result(result_json: str, data_file: str | None = None, solver_timeou
|
||||||
|
|
||||||
|
|
||||||
def read_task(task_file: str | None) -> str:
|
def read_task(task_file: str | None) -> str:
|
||||||
if task_file:
|
"""Aufgabe aus Datei, aus stdin ('-' oder umgeleitet) oder – ohne beides – die Beispielaufgabe."""
|
||||||
|
if task_file and task_file != "-":
|
||||||
task = Path(task_file).read_text(encoding="utf-8")
|
task = Path(task_file).read_text(encoding="utf-8")
|
||||||
elif not sys.stdin.isatty():
|
elif task_file == "-" or not sys.stdin.isatty():
|
||||||
task = sys.stdin.read()
|
task = sys.stdin.read()
|
||||||
else:
|
else:
|
||||||
task = ""
|
task = ""
|
||||||
# Leere Eingabe würde das LLM ein beliebiges Problem erfinden lassen
|
# Leere Eingabe würde das LLM ein beliebiges Problem erfinden lassen
|
||||||
return task if task.strip() else task_description
|
if not task.strip():
|
||||||
|
log("Keine Aufgabe angegeben – verwende die Beispielaufgabe (Schichtplanung).")
|
||||||
|
return task_description
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# Exit-Codes für Skripte. Aufruffehler bewusst nicht argparse' 2 – das hieße hier „unzuverlässig“.
|
||||||
parser = argparse.ArgumentParser(description="Textaufgabe → Optimierungsmodell → Prüfung → Lösung")
|
EXIT_OK, EXIT_ERROR, EXIT_UNRELIABLE, EXIT_REJECTED, EXIT_USAGE, EXIT_INTERRUPTED = 0, 1, 2, 3, 64, 130
|
||||||
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")
|
EPILOG = f"""\
|
||||||
parser.add_argument("--max-revisions", type=int, default=MAX_REVISIONS)
|
Beispiele:
|
||||||
parser.add_argument("--max-restarts", type=int, default=MAX_RESTARTS, help="komplette Neustarts, wenn das Modell nach allen Korrekturrunden fehlerhaft ist")
|
%(prog)s aufgabe.md --ausgabe output/aufgabe # schreibt output/aufgabe.json, .html und .py
|
||||||
parser.add_argument(
|
%(prog)s aufgabe.md --daten kurse.csv -o output/aufgabe.json --report output/aufgabe.html
|
||||||
"--model", default=DEFAULT_MODEL,
|
echo "Ein Betrieb …" | %(prog)s - --report output/modell.md
|
||||||
help=f"Modell (Default: {DEFAULT_MODEL}, Umgebung: LLM_MODEL); mit '/' -> OpenRouter, sonst Gemini",
|
%(prog)s aufgabe.md --no-solve --max-revisions 0 -m deepseek/deepseek-v4-pro-0813
|
||||||
|
%(prog)s --resolve output/aufgabe.json --data neue_werte.json -o output/variante.json # ohne LLM
|
||||||
|
%(prog)s --von solver output/aufgabe.json -o output/neu.json # nur Solver-Stufe neu
|
||||||
|
|
||||||
|
Ausgabe: Ohne -o/--ausgabe geht das Ergebnis-JSON nach stdout, wenn stdout umgeleitet ist; im Terminal
|
||||||
|
erscheint stattdessen eine Kurzfassung. Der Fortschritt steht immer auf stderr.
|
||||||
|
|
||||||
|
Exit-Codes: {EXIT_OK} zuverlässiges Ergebnis · {EXIT_ERROR} Fehler (Datei, Abbildung, gespeichertes Ergebnis) ·
|
||||||
|
{EXIT_UNRELIABLE} Ergebnis unzuverlässig · {EXIT_REJECTED} keine Optimierungsaufgabe · {EXIT_USAGE} falscher Aufruf · \
|
||||||
|
{EXIT_INTERRUPTED} abgebrochen
|
||||||
|
|
||||||
|
Optionen ohne Angabe übernehmen die Umgebungsvariable in Klammern bzw. deren Default.
|
||||||
|
Modell je Stufe nur über die Umgebung: LLM_MODEL_UNDERSTANDING, _EXTRACTION, _REVIEW, _REVISION, _SOLVER."""
|
||||||
|
|
||||||
|
|
||||||
|
class _Parser(argparse.ArgumentParser):
|
||||||
|
def error(self, message):
|
||||||
|
self.print_usage(sys.stderr)
|
||||||
|
self.exit(EXIT_USAGE, f"{self.prog}: Fehler: {message}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = _Parser(
|
||||||
|
prog="TextToModel.py", description="Textaufgabe → Optimierungsmodell → Prüfung → Lösung",
|
||||||
|
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
parser.add_argument("--resolve", metavar="ERGEBNIS.JSON",
|
|
||||||
help="gespeichertes Ergebnis erneut rechnen, ohne LLM (z. B. nach Datenänderung)")
|
eingabe = parser.add_argument_group("Eingabe")
|
||||||
parser.add_argument("--von", nargs=2, metavar=("STUFE", "ERGEBNIS.JSON"),
|
eingabe.add_argument("aufgabe", nargs="?", metavar="AUFGABE",
|
||||||
help="gespeichertes Ergebnis ab einer Stufe fortsetzen, statt alles zu wiederholen: "
|
help="Datei mit der Aufgabe (Text/Markdown); '-' = stdin. Fehlt sie und ist stdin nicht "
|
||||||
"'pruefung' (Spezifikation behalten, neu prüfen und korrigieren) oder "
|
"umgeleitet, läuft die Beispielaufgabe")
|
||||||
"'solver' (auch den Prüfbericht behalten, nur Solver-Code neu erzeugen)")
|
eingabe.add_argument("--daten", nargs="+", metavar="DATEI",
|
||||||
parser.add_argument("--daten", nargs="+", metavar="DATEI",
|
help="beigelegte Datendateien (CSV/Excel); ihr Profil geht in die Prompts, die Dateien "
|
||||||
help="beigelegte Datendateien (CSV/Excel); ihr Profil geht in die Prompts, "
|
"ins Arbeitsverzeichnis des Programms (bei --resolve/--von erneut angeben)")
|
||||||
"die Dateien selbst ins Arbeitsverzeichnis des Programms "
|
|
||||||
"(mit --resolve/--von erneut angeben, gespeichert ist nur ihr Profil)")
|
modus = parser.add_argument_group("Modus (höchstens einer; ohne Angabe: kompletter Lauf)")
|
||||||
parser.add_argument("--data", metavar="DATEN.JSON",
|
exklusiv = modus.add_mutually_exclusive_group()
|
||||||
|
exklusiv.add_argument("--no-solve", action="store_true",
|
||||||
|
help="nur verstehen, modellieren und prüfen – kein Solver-Code")
|
||||||
|
exklusiv.add_argument("--von", nargs=2, metavar=("STUFE", "ERGEBNIS.JSON"),
|
||||||
|
help="gespeichertes Ergebnis fortsetzen: 'pruefung' (Spezifikation behalten, neu prüfen "
|
||||||
|
"und korrigieren) oder 'solver' (auch Prüfbericht behalten, nur Solver-Code neu)")
|
||||||
|
exklusiv.add_argument("--resolve", metavar="ERGEBNIS.JSON",
|
||||||
|
help="gespeichertes Ergebnis ohne LLM neu rechnen (z. B. nach Datenänderung)")
|
||||||
|
modus.add_argument("--data", metavar="DATEN.JSON",
|
||||||
help="mit --resolve: Eingabewerte (DATA) des Programms ganz oder teilweise ersetzen")
|
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")
|
ablauf = parser.add_argument_group("Ablauf")
|
||||||
args = parser.parse_args()
|
ablauf.add_argument("--max-revisions", type=int, default=MAX_REVISIONS, metavar="N",
|
||||||
|
help=f"Korrekturrunden je Modellierung (MAX_REVISIONS, {MAX_REVISIONS})")
|
||||||
|
ablauf.add_argument("--max-restarts", type=int, default=MAX_RESTARTS, metavar="N",
|
||||||
|
help=f"Neustarts der Modellierung, wenn sie fehlerhaft bleibt (MAX_RESTARTS, {MAX_RESTARTS})")
|
||||||
|
ablauf.add_argument("--solver-retries", type=int, default=SOLVER_RETRIES, metavar="N",
|
||||||
|
help=f"Reparaturen des Solver-Codes (SOLVER_RETRIES, {SOLVER_RETRIES})")
|
||||||
|
ablauf.add_argument("--understand-retries", type=int, metavar="N",
|
||||||
|
help=f"Nachfassen beim Verstehen, -1 = Stufe aus (UNDERSTAND_RETRIES, {UNDERSTAND_RETRIES})")
|
||||||
|
ablauf.add_argument("--infeasible-recheck", type=int, choices=(0, 1), metavar="{0,1}",
|
||||||
|
help=f"Gegenprobe bei 'unlösbar' (INFEASIBLE_RECHECK, {INFEASIBLE_RECHECK})")
|
||||||
|
|
||||||
|
ki = parser.add_argument_group("LLM")
|
||||||
|
ki.add_argument("-m", "--model", default=DEFAULT_MODEL,
|
||||||
|
help=f"Modell; mit '/' → OpenRouter, sonst Gemini (LLM_MODEL, {DEFAULT_MODEL})")
|
||||||
|
ki.add_argument("--reasoning", choices=("low", "medium", "high"),
|
||||||
|
help="Denkaufwand, nur OpenRouter (LLM_REASONING_EFFORT, Modellvorgabe)")
|
||||||
|
ki.add_argument("--max-tokens", type=int, metavar="N",
|
||||||
|
help=f"Antwortlänge je Aufruf (LLM_MAX_TOKENS, {llm.LLM_MAX_TOKENS})")
|
||||||
|
ki.add_argument("--llm-timeout", type=int, metavar="SEK",
|
||||||
|
help=f"Zeitlimit je Aufruf (LLM_TIMEOUT, {llm.LLM_TIMEOUT})")
|
||||||
|
|
||||||
|
solver = parser.add_argument_group("Solver")
|
||||||
|
solver.add_argument("--solver-timeout", type=int, metavar="SEK",
|
||||||
|
help=f"Zeitlimit je Programmlauf, geht auch als Zeitbudget ans LLM "
|
||||||
|
f"(SOLVER_TIMEOUT, {solver_stage.SOLVER_TIMEOUT})")
|
||||||
|
solver.add_argument("--sandbox", choices=("auto", "bwrap", "aus"),
|
||||||
|
help=f"Isolierung des erzeugten Codes (SOLVER_SANDBOX, {solver_stage.SANDBOX})")
|
||||||
|
|
||||||
|
ausgabe = parser.add_argument_group("Ausgabe")
|
||||||
|
ausgabe.add_argument("-o", "--json", metavar="DATEI", help="Ergebnis-JSON in diese Datei statt nach stdout")
|
||||||
|
ausgabe.add_argument("--report", metavar="DATEI", help="Report mit Formeln und Solver-Code (.md oder .html)")
|
||||||
|
ausgabe.add_argument("--code", metavar="DATEI", help="Solver-Code als eigenständiges Python-Skript")
|
||||||
|
ausgabe.add_argument("-a", "--ausgabe", metavar="PRÄFIX",
|
||||||
|
help="Kurzform für -o PRÄFIX.json --report PRÄFIX.html --code PRÄFIX.py "
|
||||||
|
"(ausdrückliche Angaben haben Vorrang)")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _check_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
|
||||||
|
if args.ausgabe:
|
||||||
|
args.json = args.json or f"{args.ausgabe}.json"
|
||||||
|
args.report = args.report or f"{args.ausgabe}.html"
|
||||||
|
if not args.no_solve:
|
||||||
|
args.code = args.code or f"{args.ausgabe}.py"
|
||||||
if args.report and Path(args.report).suffix.lower() not in (".md", ".html", ".htm"):
|
if args.report and Path(args.report).suffix.lower() not in (".md", ".html", ".htm"):
|
||||||
parser.error("--report braucht die Endung .md oder .html")
|
parser.error("--report braucht die Endung .md oder .html")
|
||||||
if args.code and args.no_solve:
|
if args.code and args.no_solve:
|
||||||
parser.error("--code ist mit --no-solve nicht möglich (es wird kein Solver-Code erzeugt)")
|
parser.error("--code ist mit --no-solve nicht möglich (es wird kein Solver-Code erzeugt)")
|
||||||
if args.data and not args.resolve:
|
if args.data and not args.resolve:
|
||||||
parser.error("--data wirkt nur zusammen mit --resolve")
|
parser.error("--data wirkt nur zusammen mit --resolve")
|
||||||
if args.von:
|
if args.von and args.von[0] not in STAGES:
|
||||||
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]}'")
|
parser.error(f"--von braucht eine Stufe aus {{{', '.join(STAGES)}}}, nicht '{args.von[0]}'")
|
||||||
if args.task_file:
|
if (args.von or args.resolve) and args.aufgabe:
|
||||||
parser.error("--von nimmt die Aufgabe aus dem gespeicherten Ergebnis – keine Aufgabendatei angeben")
|
parser.error("--von/--resolve nehmen die Aufgabe aus dem gespeicherten Ergebnis – keine Aufgabendatei angeben")
|
||||||
if args.no_solve:
|
for name, wert in (("--max-revisions", args.max_revisions), ("--max-restarts", args.max_restarts),
|
||||||
parser.error("--von endet immer mit der Solver-Stufe; --no-solve wäre wirkungslos")
|
("--solver-retries", args.solver_retries)):
|
||||||
|
if wert < 0:
|
||||||
|
parser.error(f"{name} darf nicht negativ sein")
|
||||||
|
for name, wert in (("--max-tokens", args.max_tokens), ("--llm-timeout", args.llm_timeout),
|
||||||
|
("--solver-timeout", args.solver_timeout)):
|
||||||
|
if wert is not None and wert <= 0:
|
||||||
|
parser.error(f"{name} muss positiv sein")
|
||||||
|
eingaben = [args.aufgabe if args.aufgabe != "-" else None, args.von[1] if args.von else None,
|
||||||
|
args.resolve, args.data, *(args.daten or [])]
|
||||||
|
for datei in eingaben:
|
||||||
|
if datei and not Path(datei).is_file():
|
||||||
|
parser.error(f"Datei nicht gefunden: {datei}")
|
||||||
|
|
||||||
if args.resolve:
|
|
||||||
result = resolve_result(args.resolve, args.data, data_files=args.daten or ())
|
def apply_settings(args: argparse.Namespace) -> None:
|
||||||
elif args.von:
|
"""Optionen, die sonst nur als Umgebungsvariable wirken, für diesen Prozess setzen.
|
||||||
result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions,
|
|
||||||
data_files=args.daten or ())
|
Die Module lesen die Umgebung beim Import; deshalb werden ihre Konstanten direkt ersetzt – sie werden
|
||||||
|
zur Aufrufzeit gelesen. Modell je Stufe liest `LLMSession` aus der Umgebung und bleibt dort.
|
||||||
|
"""
|
||||||
|
global UNDERSTAND_RETRIES, INFEASIBLE_RECHECK
|
||||||
|
if args.understand_retries is not None:
|
||||||
|
UNDERSTAND_RETRIES = args.understand_retries
|
||||||
|
if args.infeasible_recheck is not None:
|
||||||
|
INFEASIBLE_RECHECK = args.infeasible_recheck
|
||||||
|
if args.reasoning:
|
||||||
|
llm.REASONING_EFFORT = args.reasoning
|
||||||
|
if args.max_tokens:
|
||||||
|
llm.LLM_MAX_TOKENS = args.max_tokens
|
||||||
|
if args.llm_timeout:
|
||||||
|
llm.LLM_TIMEOUT = args.llm_timeout
|
||||||
|
if args.solver_timeout:
|
||||||
|
solver_stage.SOLVER_TIMEOUT = args.solver_timeout
|
||||||
|
if args.sandbox:
|
||||||
|
solver_stage.SANDBOX = args.sandbox
|
||||||
|
|
||||||
|
|
||||||
|
def summary(result: PipelineResult) -> str:
|
||||||
|
"""Kurzfassung fürs Terminal: Status, Zielwert, Antworten, Zuverlässigkeit."""
|
||||||
|
if result.rejected:
|
||||||
|
return "Abgelehnt – " + "; ".join(result.reliability_issues)
|
||||||
|
zeilen = []
|
||||||
|
run = result.solver_run
|
||||||
|
if run is None:
|
||||||
|
spec = result.specification
|
||||||
|
zeilen.append(f"Modell: {spec.problem_class.value} → {spec.target_stack.value} (kein Solver-Lauf)")
|
||||||
else:
|
else:
|
||||||
try:
|
zeilen.append(f"Status: {run.status.value}" + (f" · Zielwert: {run.objective_value:g}"
|
||||||
result = run_pipeline(
|
if run.objective_value is not None else ""))
|
||||||
read_task(args.task_file), model=args.model,
|
if run.headline:
|
||||||
max_revisions=args.max_revisions, solve_model=not args.no_solve, max_restarts=args.max_restarts,
|
zeilen.append(run.headline)
|
||||||
data_files=args.daten or (), task_dir=Path(args.task_file).parent if args.task_file else None,
|
zeilen += [f" {a.question_id}) {a.text}" for a in run.answers]
|
||||||
)
|
zeilen.append("Zuverlässig." if result.reliable
|
||||||
except FigureError as exc:
|
else "UNZUVERLÄSSIG: " + "; ".join(result.reliability_issues))
|
||||||
parser.error(str(exc))
|
return "\n".join(zeilen)
|
||||||
print(result.model_dump_json(indent=2))
|
|
||||||
|
|
||||||
|
def _write_outputs(args: argparse.Namespace, result: PipelineResult) -> None:
|
||||||
|
daten = result.model_dump_json(indent=2)
|
||||||
|
if args.json:
|
||||||
|
Path(args.json).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(args.json).write_text(daten + "\n", encoding="utf-8")
|
||||||
|
log(f"Ergebnis geschrieben: {args.json}")
|
||||||
|
elif sys.stdout.isatty():
|
||||||
|
print(summary(result))
|
||||||
|
log("(JSON: mit -o DATEI speichern oder stdout umleiten)")
|
||||||
|
else:
|
||||||
|
print(daten)
|
||||||
if args.report:
|
if args.report:
|
||||||
|
Path(args.report).parent.mkdir(parents=True, exist_ok=True)
|
||||||
write_report(result, args.report)
|
write_report(result, args.report)
|
||||||
log(f"Report geschrieben: {args.report}")
|
log(f"Report geschrieben: {args.report}")
|
||||||
if args.code and result.solver_code:
|
if args.code and result.solver_code:
|
||||||
|
Path(args.code).parent.mkdir(parents=True, exist_ok=True)
|
||||||
Path(args.code).write_text(build_solver_script(result), encoding="utf-8")
|
Path(args.code).write_text(build_solver_script(result), encoding="utf-8")
|
||||||
Path(args.code).chmod(0o755)
|
Path(args.code).chmod(0o755)
|
||||||
log(f"Solver-Code geschrieben: {args.code} (ausführen: .venv/bin/python {args.code})")
|
log(f"Solver-Code geschrieben: {args.code} (ausführen: .venv/bin/python {args.code})")
|
||||||
elif args.code:
|
elif args.code:
|
||||||
log(f"Kein Solver-Code geschrieben ({args.code}): Solver wurde nicht gestartet.")
|
log(f"Kein Solver-Code geschrieben ({args.code}): Solver wurde nicht gestartet.")
|
||||||
if result.rejected:
|
|
||||||
sys.exit(3) # für Skripte: keine Optimierungsaufgabe, nichts gerechnet
|
|
||||||
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
|
def main(argv: List[str] | None = None) -> int:
|
||||||
# result.specification.problem_class -> ProblemClass.CP
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
_check_args(parser, args)
|
||||||
|
apply_settings(args)
|
||||||
|
daten = args.daten or ()
|
||||||
|
try:
|
||||||
|
if args.resolve or args.von:
|
||||||
|
try:
|
||||||
|
if args.resolve:
|
||||||
|
result = resolve_result(args.resolve, args.data, data_files=daten)
|
||||||
|
else:
|
||||||
|
result = continue_pipeline(args.von[1], args.von[0], model=args.model,
|
||||||
|
max_revisions=args.max_revisions,
|
||||||
|
solver_retries=args.solver_retries, data_files=daten)
|
||||||
|
except ValueError as exc: # auch pydantic.ValidationError: gespeichertes Ergebnis passt nicht
|
||||||
|
log(f"Fehler in {args.resolve or args.von[1]}: {exc}")
|
||||||
|
return EXIT_ERROR
|
||||||
|
else:
|
||||||
|
task_dir = Path(args.aufgabe).parent if args.aufgabe and args.aufgabe != "-" else None
|
||||||
|
result = run_pipeline(
|
||||||
|
read_task(args.aufgabe), model=args.model, max_revisions=args.max_revisions,
|
||||||
|
solve_model=not args.no_solve, solver_retries=args.solver_retries, max_restarts=args.max_restarts,
|
||||||
|
data_files=daten, task_dir=task_dir,
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("Abgebrochen.")
|
||||||
|
return EXIT_INTERRUPTED
|
||||||
|
except (FigureError, OSError) as exc:
|
||||||
|
log(f"Fehler: {exc}")
|
||||||
|
return EXIT_ERROR
|
||||||
|
_write_outputs(args, result)
|
||||||
|
if result.rejected:
|
||||||
|
return EXIT_REJECTED # keine Optimierungsaufgabe, nichts gerechnet
|
||||||
|
if not result.reliable:
|
||||||
|
return EXIT_UNRELIABLE # Ergebnis vorhanden, aber unzuverlässig
|
||||||
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
|
||||||
|
|
@ -572,8 +572,9 @@ def solver_env(tmp: str) -> dict:
|
||||||
return {**env, "HOME": tmp, "TMPDIR": tmp, "PYTHONDONTWRITEBYTECODE": "1"}
|
return {**env, "HOME": tmp, "TMPDIR": tmp, "PYTHONDONTWRITEBYTECODE": "1"}
|
||||||
|
|
||||||
|
|
||||||
def run_solver_code(code: str, timeout: int = SOLVER_TIMEOUT, data: dict | None = None,
|
def run_solver_code(code: str, timeout: int | None = None, data: dict | None = None,
|
||||||
data_files=()) -> SolverRun:
|
data_files=()) -> SolverRun:
|
||||||
|
timeout = timeout or SOLVER_TIMEOUT # zur Aufrufzeit, damit --solver-timeout wirkt
|
||||||
try:
|
try:
|
||||||
validate_imports(code)
|
validate_imports(code)
|
||||||
except CodeRejected as exc:
|
except CodeRejected as exc:
|
||||||
|
|
@ -815,7 +816,7 @@ def check_text_quality(run, summary: str = "") -> list:
|
||||||
MAX_SCENARIOS = 5 # jedes Szenario kostet einen weiteren Programmlauf (kein LLM-Aufruf)
|
MAX_SCENARIOS = 5 # jedes Szenario kostet einen weiteren Programmlauf (kein LLM-Aufruf)
|
||||||
|
|
||||||
|
|
||||||
def verify_scenarios(run, code: str, timeout: int = SOLVER_TIMEOUT, data_files=()) -> list:
|
def verify_scenarios(run, code: str, timeout: int | None = None, data_files=()) -> list:
|
||||||
"""Jedes Szenario mit übersteuerten Daten NEU rechnen und den gemeldeten Zielwert bestätigen.
|
"""Jedes Szenario mit übersteuerten Daten NEU rechnen und den gemeldeten Zielwert bestätigen.
|
||||||
|
|
||||||
Ein Szenario ist eine Datenvariante desselben Programms – dieselbe Maschinerie wie `--resolve`.
|
Ein Szenario ist eine Datenvariante desselben Programms – dieselbe Maschinerie wie `--resolve`.
|
||||||
|
|
@ -1163,7 +1164,7 @@ def _check_and_run(code: str, timeout: int, spec: OptimizationModelSpecification
|
||||||
|
|
||||||
|
|
||||||
def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
retries: int = 2, timeout: int = SOLVER_TIMEOUT, log=lambda msg: None, understanding=None,
|
retries: int = 2, timeout: int | None = None, log=lambda msg: None, understanding=None,
|
||||||
data_files=(), data_prompt: str = "") -> tuple:
|
data_files=(), data_prompt: str = "") -> tuple:
|
||||||
"""Code erzeugen und ausführen; bei Fehlern bis zu `retries` Reparaturversuche.
|
"""Code erzeugen und ausführen; bei Fehlern bis zu `retries` Reparaturversuche.
|
||||||
|
|
||||||
|
|
|
||||||
124
tests/test_cli.py
Normal file
124
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""CLI `TextToModel.main`: Optionen, Defaults, Ausgabe und Exit-Codes – ohne LLM."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import TextToModel
|
||||||
|
from core import llm
|
||||||
|
from core.model_schema import PipelineResult
|
||||||
|
from solving import solver_stage
|
||||||
|
from test_resolve import gespeichertes_ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def einstellungen_zuruecksetzen(monkeypatch):
|
||||||
|
# main() setzt Modulkonstanten; monkeypatch stellt sie nach jedem Test wieder her
|
||||||
|
for modul, name in ((TextToModel, "UNDERSTAND_RETRIES"), (TextToModel, "INFEASIBLE_RECHECK"),
|
||||||
|
(llm, "REASONING_EFFORT"), (llm, "LLM_MAX_TOKENS"), (llm, "LLM_TIMEOUT"),
|
||||||
|
(solver_stage, "SOLVER_TIMEOUT"), (solver_stage, "SANDBOX")):
|
||||||
|
monkeypatch.setattr(modul, name, getattr(modul, name))
|
||||||
|
|
||||||
|
|
||||||
|
def aufruffehler(argv, capsys) -> str:
|
||||||
|
with pytest.raises(SystemExit) as exc:
|
||||||
|
TextToModel.main(argv)
|
||||||
|
assert exc.value.code == TextToModel.EXIT_USAGE # nicht 2 – das hieße „unzuverlässig“
|
||||||
|
return capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_writes_json_report_and_code_with_prefix(tmp_path):
|
||||||
|
ergebnis = gespeichertes_ergebnis(tmp_path)
|
||||||
|
praefix = tmp_path / "neu" / "lauf" # Ordner existiert noch nicht
|
||||||
|
assert TextToModel.main(["--resolve", str(ergebnis), "-a", str(praefix)]) == TextToModel.EXIT_OK
|
||||||
|
neu = PipelineResult.model_validate_json((tmp_path / "neu" / "lauf.json").read_text(encoding="utf-8"))
|
||||||
|
assert neu.solver_run.objective_value == pytest.approx(9)
|
||||||
|
assert (tmp_path / "neu" / "lauf.html").exists() and (tmp_path / "neu" / "lauf.py").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_output_wins_over_prefix(tmp_path):
|
||||||
|
ergebnis = gespeichertes_ergebnis(tmp_path)
|
||||||
|
TextToModel.main(["--resolve", str(ergebnis), "-a", str(tmp_path / "p"), "--report", str(tmp_path / "r.md")])
|
||||||
|
assert (tmp_path / "r.md").exists() and not (tmp_path / "p.html").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_goes_to_redirected_stdout(tmp_path, capsys):
|
||||||
|
ergebnis = gespeichertes_ergebnis(tmp_path)
|
||||||
|
daten = tmp_path / "daten.json"
|
||||||
|
daten.write_text(json.dumps({"bedarf": 5}), encoding="utf-8")
|
||||||
|
assert TextToModel.main(["--resolve", str(ergebnis), "--data", str(daten)]) == TextToModel.EXIT_OK
|
||||||
|
assert json.loads(capsys.readouterr().out)["solver_run"]["objective_value"] == pytest.approx(12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_terminal_gets_a_summary_instead_of_json(tmp_path, capsys, monkeypatch):
|
||||||
|
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
|
||||||
|
TextToModel.main(["--resolve", str(gespeichertes_ergebnis(tmp_path))])
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Zielwert: 9" in out and "Zuverlässig." in out and not out.lstrip().startswith("{")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unreliable_result_exits_with_2(tmp_path):
|
||||||
|
daten = tmp_path / "daten.json"
|
||||||
|
daten.write_text(json.dumps({"kosten": "keine Zahl"}), encoding="utf-8")
|
||||||
|
code = TextToModel.main(["--resolve", str(gespeichertes_ergebnis(tmp_path)), "--data", str(daten),
|
||||||
|
"-o", str(tmp_path / "x.json")])
|
||||||
|
assert code == TextToModel.EXIT_UNRELIABLE
|
||||||
|
|
||||||
|
|
||||||
|
def test_broken_result_file_is_an_error_not_a_traceback(tmp_path):
|
||||||
|
kaputt = tmp_path / "kaputt.json"
|
||||||
|
kaputt.write_text("{}", encoding="utf-8")
|
||||||
|
assert TextToModel.main(["--resolve", str(kaputt)]) == TextToModel.EXIT_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("argv, meldung", [
|
||||||
|
(["--data", "d.json"], "--data wirkt nur"),
|
||||||
|
(["a.txt", "--report", "r.pdf"], "Endung"),
|
||||||
|
(["a.txt", "--no-solve", "--code", "c.py"], "--no-solve"),
|
||||||
|
(["--von", "extraktion", "e.json"], "Stufe"),
|
||||||
|
(["--von", "solver", "e.json", "--no-solve"], "not allowed"),
|
||||||
|
(["--max-revisions", "-1"], "negativ"),
|
||||||
|
(["--solver-timeout", "0"], "positiv"),
|
||||||
|
(["fehlt.txt"], "Datei nicht gefunden"),
|
||||||
|
])
|
||||||
|
def test_wrong_calls_are_usage_errors(argv, meldung, capsys):
|
||||||
|
assert meldung in aufruffehler(argv, capsys)
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_file_with_resolve_is_rejected(tmp_path, capsys):
|
||||||
|
ergebnis = gespeichertes_ergebnis(tmp_path)
|
||||||
|
assert "keine Aufgabendatei" in aufruffehler([str(ergebnis), "--resolve", str(ergebnis)], capsys)
|
||||||
|
|
||||||
|
|
||||||
|
def test_options_reach_pipeline_and_modules(tmp_path, monkeypatch):
|
||||||
|
aufgabe = tmp_path / "aufgabe.md"
|
||||||
|
aufgabe.write_text("Maximiere x unter x ≤ 3.", encoding="utf-8")
|
||||||
|
gesehen = {}
|
||||||
|
|
||||||
|
def fake_pipeline(task, **kwargs):
|
||||||
|
gesehen.update(kwargs, task=task)
|
||||||
|
return PipelineResult(task=task, rejected=True, reliable=False, reliability_issues=["keine Aufgabe"])
|
||||||
|
|
||||||
|
monkeypatch.setattr(TextToModel, "run_pipeline", fake_pipeline)
|
||||||
|
code = TextToModel.main([str(aufgabe), "-m", "x/y", "--max-revisions", "0", "--solver-retries", "4",
|
||||||
|
"--max-restarts", "0", "--understand-retries", "-1", "--infeasible-recheck", "0",
|
||||||
|
"--reasoning", "low", "--max-tokens", "1000", "--llm-timeout", "30",
|
||||||
|
"--solver-timeout", "90", "--sandbox", "bwrap", "-o", str(tmp_path / "e.json")])
|
||||||
|
assert code == TextToModel.EXIT_REJECTED
|
||||||
|
assert gesehen["task"].startswith("Maximiere") and gesehen["task_dir"] == tmp_path
|
||||||
|
assert (gesehen["model"], gesehen["max_revisions"], gesehen["solver_retries"], gesehen["max_restarts"]) \
|
||||||
|
== ("x/y", 0, 4, 0)
|
||||||
|
assert gesehen["solve_model"] is True
|
||||||
|
assert (TextToModel.UNDERSTAND_RETRIES, TextToModel.INFEASIBLE_RECHECK) == (-1, 0)
|
||||||
|
assert (llm.REASONING_EFFORT, llm.LLM_MAX_TOKENS, llm.LLM_TIMEOUT) == ("low", 1000, 30)
|
||||||
|
assert (solver_stage.SOLVER_TIMEOUT, solver_stage.SANDBOX) == (90, "bwrap")
|
||||||
|
assert "90" in solver_stage.solver_prompt() # Zeitbudget im Prompt folgt der Option
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_options_the_defaults_stay(tmp_path, monkeypatch):
|
||||||
|
vorher = (llm.LLM_MAX_TOKENS, solver_stage.SOLVER_TIMEOUT, solver_stage.SANDBOX, TextToModel.UNDERSTAND_RETRIES)
|
||||||
|
monkeypatch.setattr(TextToModel, "run_pipeline", lambda task, **kw: PipelineResult(task=task, reliable=True))
|
||||||
|
aufgabe = tmp_path / "a.txt"
|
||||||
|
aufgabe.write_text("Minimiere x.", encoding="utf-8")
|
||||||
|
TextToModel.main([str(aufgabe), "-o", str(tmp_path / "e.json")])
|
||||||
|
assert (llm.LLM_MAX_TOKENS, solver_stage.SOLVER_TIMEOUT, solver_stage.SANDBOX,
|
||||||
|
TextToModel.UNDERSTAND_RETRIES) == vorher
|
||||||
Loading…
Reference in a new issue