text_to_solver_program/README.md

203 lines
16 KiB
Markdown
Raw Normal View History

# Math Model Generator & Solver
Übersetzt eine **natürlichsprachige Optimierungsaufgabe** (Deutsch) mit einem LLM in ein formales mathematisches Modell, prüft es, korrigiert Fehler und **löst es tatsächlich** mit einem passenden Python-Solver. Ergebnis ist ein nachvollziehbarer Report mit Formeln, Prüfbericht, Lösung und dem erzeugten Python-Code.
```text
Aufgabentext (+ optional beigelegte Daten: --daten kurse.csv)
│ ohne LLM: Profil je Datei – Zeilen, Spalten, Spannweiten, Lücken
▼
1. Verstehen ───────── LLM: Teilfragen a)/b)/…, alle Zahlen des Textes mit Fundstelle,
│ übergangene Zahlen mit Grund; ist es überhaupt eine Aufgabe?
│ ohne LLM geprüft: keine Zahl, keine Teilfrage übersehen
▼
2. Extraktion ──────── LLM: Problemklasse, Solver-Wahl, Indizes, Parameter,
│ Variablen, Zielfunktion, Restriktionen (LaTeX), Annahmen
▼
3. Konsistenz-Checks ─ ohne LLM: Solver passt zur Klasse? Einheiten? undeklarierte
│ Symbole? kamen die erfassten Zahlen im Modell an?
▼
4. LLM-Prüfung ─────── Zielfunktion = gefragte Größe? Annahmen textkonform?
│ ▲
▼ │ Korrekturschleife (max. 2 Runden, beste Runde gewinnt)
5. Korrektur ┘
│
▼
6. Solver ──────────── LLM erzeugt Python-Code, Ausführung in der Sandbox, Reparatur
│ bei Fehlern; Szenarien = Datenvarianten, ohne LLM nachgerechnet
▼
7. Abnahme ─────────── ohne LLM: jede Teilfrage beantwortet? jede Zahl der Antwort
│ belegt? Lösung zulässig? Tabellensummen richtig?
▼
Report (Markdown/HTML) + JSON + eigenständiges Solver-Skript
```
## Unterstützte Problemklassen
| Klasse | Solver | Typische Aufgaben |
|---|---|---|
| Lineare Programmierung (LP) | SciPy `linprog` | Mischungs-, einfache Produktionsprobleme |
| Gemischt-ganzzahlig (MILP) | HiGHS (`highspy`) | Zuordnung, Rucksack, Standortwahl, Rüst-/Fixkosten |
| Constraint Programming (CP) | Google OR-Tools CP-SAT | Schicht- und Personalplanung, logische Bedingungen |
| Quadratisch / Kegel (QP, SOCP) | CVXPY | Portfoliooptimierung (Varianz, Volatilität) mit kontinuierlichen Anteilen |
| Gemischt-ganzzahlig quadratisch (MIQP) | SCIP (`pyscipopt`) | Portfolio mit Kardinalität (mind./höchstens k Titel), Mindestgewicht nur für gewählte Titel, fixe Transaktionskosten |
| Nichtlinear, stetig (NLP) | Ipopt (`cyipopt`) | nichtlineare Ausbeute- oder Kostenfunktionen, Kurvenanpassung; mehrere Startpunkte, Streuung wird berichtet |
| Kürzeste Wege (Graph) | NetworkX (Dijkstra) | Wegewahl im Netz, Ersatz- und Erneuerungsprobleme; der Report zeichnet den Graphen und zeigt den Markierungsverlauf |
## Installation
Voraussetzungen: Python ≥ 3.10, Linux. Systempakete:
- `bubblewrap` für die Sandbox des erzeugten Codes (Debian/Ubuntu: `sudo apt install bubblewrap`, siehe Sicherheitshinweis)
- die Ipopt-Bibliothek für `cyipopt` (Debian/Ubuntu: `sudo apt install coinor-libipopt-dev pkg-config`), sofern pip kein fertiges Paket findet
```bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt
```
API-Key als Umgebungsvariable setzen (nicht in Dateien speichern):
| Anbieter | Variable | Modellnamen |
|---|---|---|
| OpenRouter (Standard) | `OPENROUTER_API_KEY` | mit `/`, z. B. `deepseek/deepseek-v4.1-flash` |
| Google Gemini | `GEMINI_API_KEY` | ohne `/`, z. B. `gemini-2.5-flash` |
Der Anbieter wird am Modellnamen erkannt. Das kostenlose Gemini-Kontingent reicht nur für wenige Läufe pro Tag.
## Benutzung
```bash
# Aufgabe aus Datei lösen, Report und Solver-Code speichern
.venv/bin/python TextToModel.py aufgabe.txt --report ausgabe/modell.html --code ausgabe/solve.py > ausgabe/ergebnis.json
# Aufgabe über stdin
echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report ausgabe/modell.md
# nur modellieren und prüfen, nicht lösen; anderes Modell
.venv/bin/python TextToModel.py aufgabe.txt --no-solve --model deepseek/deepseek-v4-pro-0813
# Report nachträglich aus gespeichertem Ergebnis erzeugen (keine LLM-Kosten)
.venv/bin/python -m reporting.report ausgabe/ergebnis.json ausgabe/modell.md
# exportiertes Solver-Programm erneut ausführen
.venv/bin/python ausgabe/solve.py
# Variante rechnen: geänderte Eingabedaten, gleiches Programm, KEIN LLM-Aufruf
.venv/bin/python TextToModel.py --resolve ausgabe/ergebnis.json --data neue_daten.json > ausgabe/variante.json
# Aufgabe mit beigelegten Daten (CSV/Excel); das Profil geht in die Prompts, die Datei ins Programm
.venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --report ausgabe/modell.html
# nur die Solver-Stufe wiederholen: Modell und Prüfbericht bleiben, spart die Aufrufe davor
.venv/bin/python TextToModel.py --von solver ausgabe/ergebnis.json > ausgabe/neu.json
```
Der Ordner `ausgabe/` ist in `.gitignore` eingetragen. Ohne Eingabe wird eine eingebaute Beispielaufgabe verwendet. Fortschritt und Kosten erscheinen auf stderr, das Ergebnis-JSON auf stdout.
| Option / Variable | Bedeutung | Standard |
|---|---|---|
| `--report DATEI` | Report als `.md` (z. B. VS Code-Vorschau: Strg+Umschalt+V) oder `.html` (Browser, Formeln via MathJax) | – |
| `--code DATEI` | Solver-Programm als eigenständiges, dokumentiertes Python-Skript speichern | – |
| `--model` / `LLM_MODEL` | LLM-Modell | `deepseek/deepseek-v4.1-flash` |
| `--daten DATEI …` | beigelegte CSV-/Excel-Dateien; ihr Profil (Zeilen, Spalten, Spannweiten, Lücken) geht in die Prompts, die Dateien ins Arbeitsverzeichnis des Programms | – |
| `--von STUFE ERGEBNIS.JSON` | gespeichertes Ergebnis fortsetzen statt alles zu wiederholen: `solver` (nur Solver-Code neu) oder `pruefung` (ab Prüfung/Korrektur) | – |
| `--max-revisions` / `MAX_REVISIONS` | Korrekturrunden | 2 |
| `--no-solve` | keinen Solver-Code erzeugen | aus |
| `SOLVER_RETRIES` | Reparaturversuche für fehlerhaften Solver-Code | 2 |
| `INFEASIBLE_RECHECK` | Gegenprobe mit unabhängiger Modellierung bei „unlösbar“ (0 schaltet ab) | 1 |
| `LLM_MAX_TOKENS` | Obergrenze je Antwort (OpenRouter) | 32000 |
| `--max-restarts` / `MAX_RESTARTS` | komplette Neustarts, wenn das Modell nach allen Korrekturrunden fehlerhaft ist | 1 |
| `OPENROUTER_MIN_THROUGHPUT` | bevorzugte Mindestgeschwindigkeit des OpenRouter-Anbieters (Tokens/s); Präferenz, kein harter Filter | 75 |
| `LLM_TIMEOUT` | Timeout je LLM-Aufruf (s) | 420 |
| `SOLVER_SANDBOX` | Isolierung des erzeugten Codes: `auto` (bubblewrap, wenn vorhanden), `bwrap` (Pflicht), `aus` | `auto` |
| `LLM_MODEL_EXTRACTION`, `LLM_MODEL_REVIEW`, `LLM_MODEL_REVISION`, `LLM_MODEL_SOLVER` | Modell einzelner Pipeline-Stufen, z. B. ein schnelles für den Solver-Code; verschiedene Anbieter sind erlaubt | Modell aus `--model` |
### Solver-Programme
Das erzeugte Programm (`--code` sowie im HTML-/Markdown-Report) ist **separat verwendbar** und aufgebaut als:
1. `#!/usr/bin/env python3`
2. Docstring mit **Aufgabentext**, **mathematischem Modell** (Indizes, Parameter, Variablen, Zielfunktion, Restriktionen – mit Erläuterungen und Annahmen) und dem **Algorithmus** samt Begründung, warum er hier passt
3. kommentierter Python-Code: Imports, Eingabedaten als Konstanten, Modellfunktion(en), `main()`
4. `if __name__ == "__main__": main()`
Die Ausgabe ist lesbar – Status, Zielwert, **verständliche Ergebnistabellen** (z. B. je Aktie: Anteil, Rendite, Kosten, Beitrag zum Ziel, Summenzeile) und die Variablenwerte – und endet mit einer maschinenlesbaren `RESULT_JSON`-Zeile. Die Tabellen berechnet das Programm aus der tatsächlichen Lösung; die Pipeline prüft automatisch, dass die Beiträge sich zum Zielwert summieren, und warnt sonst. Der Report beginnt mit dem **Ergebnis**: ein Antwortsatz, die Kennzahlen, die Ergebnistabellen samt **Diagramm** (Balken je Objekt, Wasserfall für die Zielwert-Zerlegung aus Ertrag, Risiko und Kosten) und eine Tabelle der **bindenden Restriktionen**. Darunter folgen die Rohwerte nach Variablen gruppiert, danach Aufgabe, Modell, Prüfbericht und Programm. Die Diagramme sind reines SVG ohne externe Bibliothek und funktionieren offline, hell wie dunkel. Im HTML-Report lässt sich das Programm mit einem Klick auf das Kopier-Symbol in die Zwischenablage übernehmen. Das JSON-Ergebnis enthält weiterhin nur den rohen generierten Code.
**Mit Datenanhängen (`--daten`):** Das erzeugte Programm liest die Dateien über ihren Dateinamen aus dem Arbeitsverzeichnis. Ein mit `--code` exportiertes Programm braucht die Datendateien deshalb **im selben Verzeichnis** – sonst endet es mit `FileNotFoundError`. Kopiere sie mit:
```bash
.venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --code ausgabe/solve.py
cp kurse.csv ausgabe/ && (cd ausgabe && ../.venv/bin/python solve.py)
```
## Wie verlässlich ist das?
Das LLM kann Fehler machen – deshalb gibt es mehrere Sicherungen:
- **Zielgrößen-Prüfung:** erkennt z. B., wenn die Varianz statt der gefragten Volatilität minimiert wird.
- **Annahmen-Prüfung:** Unklarheiten im Text müssen als Annahme ausgewiesen werden und werden gegen den Text geprüft (z. B. „wenn A, dann B“ ist keine Äquivalenz).
- **Zulässigkeitsprüfung ohne LLM:** Die gefundene Lösung wird in alle Restriktionen eingesetzt, Schranken und Ganzzahligkeit geprüft und abgeglichen, ob jede Restriktion des Modells im Programm vorkommt. Verletzungen lösen eine Reparatur aus – Solver-Toleranzen (beobachtet: 8 Cent über dem Budget) fallen damit auf.
- **Unlösbare Aufgaben** werden als `infeasible` gemeldet, nicht durch Weglassen von Bedingungen „passend gemacht“ – aber auch nicht ungeprüft geglaubt: Eine zweite, unabhängige Modellierung bestätigt oder widerlegt den Status.
- **Beste Korrekturrunde:** Verschlechtert eine Korrektur das Modell, wird die frühere Fassung verwendet.
- **Sensitivität:** Für bindende Restriktionen rechnet das Programm mit verschobener rechter Seite neu; der Report zeigt, was eine Lockerung je Einheit bringt, und bei ganzzahligen Modellen den Preis der Ganzzahligkeit. Unplausible Richtungen werden gemeldet.
- **Neustart bei Fremdproblem:** Gelegentlich modelliert das LLM ein anderes als das gestellte Problem. Passt die Zielgröße auch nach allen Korrekturrunden nicht, wird die Modellierung komplett neu gestartet.
- **Jede Teilfrage beantwortet:** Der Aufgabentext wird vor dem Modellieren in seine Teilfragen zerlegt (a, b, …); das Programm liefert je Teilfrage einen Antwortsatz. Fehlt eine Antwort, gilt die Aufgabe als nicht erfüllt – auch wenn der Zielwert stimmt.
- **Jede Zahl der Antwort belegt:** Zahlen im Antworttext müssen aus dem Ergebnis oder der Aufgabe stammen (Rundungen und Einheitenumrechnungen werden akzeptiert). Ein Vergleichswert, der in keiner Tabelle steht, fällt auf.
- **Jede Zahl der Aufgabe erfasst:** Zahlen des Aufgabentexts müssen als Fakt mit Fundstelle oder mit Begründung als irrelevant ausgewiesen sein – sonst betreten sie die Pipeline erst im Solver-Code, hinter allen Prüfungen.
- **Selbst erzeugte Daten sind gekennzeichnet:** Erzeugt das Programm Eingabedaten (Zufallszahlen, synthetischer Markt, verkleinertes Universum), muss das in `data_assumptions` stehen, nicht nur im Fließtext – sonst folgt eine Reparatur und notfalls die Markierung „unzuverlässig“.
- **Szenarien nachgerechnet:** Vergleicht die Aufgabe mehrere Konfigurationen, wird jede durch erneutes Ausführen des Programms bestätigt – ohne LLM.
- **Sandbox:** Der erzeugte Code läuft ohne Netzzugang, mit nur lesbarem Dateisystem und beschreibbar allein dem Arbeitsverzeichnis des Laufs.
- **Kein falsches „optimal“:** Bleibt das Modell fehlerhaft, wird das Ergebnis als **unzuverlässig** markiert – bei falscher Zielgröße ohne Solver-Lauf, mit Warnkasten im Report und Exit-Code 2.
Trotzdem gilt: **Ergebnisse vor Verwendung prüfen** – der Report zeigt dafür Modell, Annahmen, Prüfbericht und Code.
### Benchmark
`benchmark/cases/` enthält 19 Aufgaben mit **unabhängig verifiziertem Optimum** (Brute Force bzw. zweiter Solver) – von kleinen LPs bis zu Aufgaben mit eingebauten Fallen (Rüstkosten/Mindestlos, Wenn-dann-Regel als Implikation, Volatilität statt Varianz, nichtkonvexes NLP mit globalem Optimum, beweisbar unlösbare Mischung, Losgrößen als kürzester Weg). Gemessen wird der **Zielwert** und, strenger, ob die **Aufgabe erfüllt** ist: jede Teilfrage beantwortet, jede Zahl der Antwort belegt. Dateien im Fallverzeichnis außer `task.txt`/`expected.json` werden als Datenanhang übergeben.
```bash
.venv/bin/python -m benchmark.benchmark --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3
.venv/bin/python -m benchmark.benchmark --cases hard_schichtplan_woche --runs 3
```
Letzter Stand (September 2026, DeepSeek V4.1-Flash über OpenRouter, je 2 Läufe): **38 von 38 Läufen** über alle 19 Fälle mit richtigem Zielwert und erfüllter Aufgabe, etwa 0,05–0,15 $ und 3–10 Minuten je Lauf. Die Fallmenge ist damit gesättigt; die Grenzen der Pipeline zeigen sich eher an schlecht gestellten Aufgaben (fehlende Daten, mehrdeutige Angaben) – Beispiele dafür liegen in `tasks/`. Hinweis: Drei Fälle wurden nach dieser Messung sprachlich neu gefasst (gleiche Zahlen und Optima).
## Tests
```bash
.venv/bin/python -m pytest -q # offline, ohne API-Kosten
.venv/bin/python -m pytest -q tests/test_solver_stage.py -k cvxpy # einzelner Test
```
## Projektstruktur
Im Wurzelverzeichnis liegt nur das Hauptprogramm; jede Pipeline-Stufe liegt mit ihrem Code und ihrem Prompt in einem eigenen Ordner.
| Ordner / Datei | Inhalt |
|---|---|
| `TextToModel.py` | CLI und Ablauf der Pipeline (`run_pipeline`, Neustart, Gegenprobe, Fortsetzen, Neurechnen) |
| `core/` | `model_schema.py` (Pydantic-Schemas aller Zwischenergebnisse), `llm.py` (LLM-Zugriff über OpenRouter/Gemini, Wiederholungen, Kostenerfassung) |
| `understanding/` | Stufe Verstehen: Teilfragen und Zahlen des Aufgabentexts erfassen (`understand.py`, `Verstehen-Prompt.txt`) |
| `modeling/` | Stufen Extraktion und Korrektur: Modellspezifikation erzeugen und überarbeiten (`extraction.py`, `Routing-und-Extraktions-Prompt.txt`) |
| `checks/` | Prüfungen: deterministische Konsistenz-Checks (`model_checks.py`) und LLM-Prüfung von Zielgröße und Annahmen (`review.py`, `Pruef-Prompt.txt`) |
| `solving/` | Solver-Code erzeugen, prüfen, in der Sandbox ausführen (`solver_stage.py`, `Solver-Prompt.txt`); eigenständiges Solver-Programm (`script_export.py`) |
| `reporting/` | Report als Markdown/HTML, auch als CLI (`report.py`); Diagramme und Graphenzeichnung als Inline-SVG (`charts.py`) |
| `data/` | Profil beigelegter CSV-/Excel-Dateien, ohne LLM (`data_profile.py`) |
| `benchmark/` | Benchmark (`benchmark.py`) und Referenzaufgaben (`cases/`) |
| `tasks/` | weitere Aufgaben zum Ausprobieren (u. a. mit fehlenden Daten) |
| `tests/` | Offline-Tests (`pytest`, Konfiguration in `pyproject.toml`) |
## Sicherheitshinweis
Der Solver-Code wird vom LLM erzeugt. Er läuft in einer **bubblewrap-Sandbox**: kein Netzzugang, das Dateisystem nur lesbar, beschreibbar allein das Temp-Verzeichnis des jeweiligen Laufs, `HOME` zeigt ebenfalls dorthin. Davor greifen weiterhin eine Import-Whitelist (nur Mathe- und Solver-Bibliotheken), das Verbot von `open`/`eval`/`exec` und ein Timeout.
Voraussetzung ist ein installiertes `bubblewrap` (Debian/Ubuntu: `sudo apt install bubblewrap`). Fehlt es, läuft der Code **ungeschützt mit deinen Benutzerrechten** – dann nur eigene, vertrauenswürdige Aufgabentexte verwenden. Mit `SOLVER_SANDBOX=bwrap` wird die Sandbox zur Pflicht: ohne bubblewrap wird der Lauf abgelehnt statt ungeschützt ausgeführt.
```bash
.venv/bin/python -m pytest -q -k sandbox # prüft, dass Schreiben außerhalb des Laufs scheitert
```
## Lizenz
MIT – siehe [`LICENSE`](LICENSE).