From 7459690bf5999cb2380141f4c0820cadab7ed4e3 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Thu, 17 Sep 2026 12:11:51 +0200 Subject: [PATCH] refactor: Pipeline-Stufen in Ordner mit ihren Prompts Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI). Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core, understanding, modeling, checks, solving, reporting, data, benchmark; Tests unter tests/, Aufgaben unter tasks/. understand_task, text_to_model/revise_model und review_assumptions/needs_work/select_best sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a. Co-Authored-By: Claude Opus 5 --- README.md | 38 ++--- TextToModel.py | 134 ++---------------- benchmark/__init__.py | 0 benchmark.py => benchmark/benchmark.py | 12 +- Pruef-Prompt.txt => checks/Pruef-Prompt.txt | 0 checks/__init__.py | 0 model_checks.py => checks/model_checks.py | 2 +- checks/review.py | 50 +++++++ core/__init__.py | 0 llm.py => core/llm.py | 2 +- model_schema.py => core/model_schema.py | 0 data/__init__.py | 0 data_profile.py => data/data_profile.py | 2 +- .../Routing-und-Extraktions-Prompt.txt | 0 modeling/__init__.py | 0 modeling/extraction.py | 68 +++++++++ pyproject.toml | 3 + reporting/__init__.py | 0 charts.py => reporting/charts.py | 0 report.py => reporting/report.py | 8 +- .../Solver-Prompt.txt | 0 solving/__init__.py | 0 script_export.py => solving/script_export.py | 4 +- solver_stage.py => solving/solver_stage.py | 6 +- .../komplexes_Portfolio_Management.md | 0 .../komplexes_Portfolio_Management_Nr2.md | 0 {aufgaben => tasks}/nur_formeln.md | 0 test_charts.py => tests/test_charts.py | 14 +- .../test_data_profile.py | 2 +- .../test_feasibility.py | 6 +- test_llm.py => tests/test_llm.py | 6 +- .../test_model_checks.py | 45 +++--- test_pipeline.py => tests/test_pipeline.py | 44 +++--- .../test_report_math.py | 2 +- test_resolve.py => tests/test_resolve.py | 6 +- .../test_result_tables.py | 30 ++-- test_review.py => tests/test_review.py | 6 +- .../test_script_export.py | 12 +- .../test_solver_stage.py | 64 ++++----- .../Verstehen-Prompt.txt | 0 understanding/__init__.py | 0 understanding/understand.py | 26 ++++ 42 files changed, 314 insertions(+), 278 deletions(-) create mode 100644 benchmark/__init__.py rename benchmark.py => benchmark/benchmark.py (96%) rename Pruef-Prompt.txt => checks/Pruef-Prompt.txt (100%) create mode 100644 checks/__init__.py rename model_checks.py => checks/model_checks.py (99%) create mode 100644 checks/review.py create mode 100644 core/__init__.py rename llm.py => core/llm.py (99%) rename model_schema.py => core/model_schema.py (100%) create mode 100644 data/__init__.py rename data_profile.py => data/data_profile.py (98%) rename Routing-und-Extraktions-Prompt.txt => modeling/Routing-und-Extraktions-Prompt.txt (100%) create mode 100644 modeling/__init__.py create mode 100644 modeling/extraction.py create mode 100644 pyproject.toml create mode 100644 reporting/__init__.py rename charts.py => reporting/charts.py (100%) rename report.py => reporting/report.py (99%) rename Solver-Prompt.txt => solving/Solver-Prompt.txt (100%) create mode 100644 solving/__init__.py rename script_export.py => solving/script_export.py (98%) rename solver_stage.py => solving/solver_stage.py (99%) rename {aufgaben => tasks}/komplexes_Portfolio_Management.md (100%) rename {aufgaben => tasks}/komplexes_Portfolio_Management_Nr2.md (100%) rename {aufgaben => tasks}/nur_formeln.md (100%) rename test_charts.py => tests/test_charts.py (94%) rename test_data_profile.py => tests/test_data_profile.py (96%) rename test_feasibility.py => tests/test_feasibility.py (96%) rename test_llm.py => tests/test_llm.py (97%) rename test_model_checks.py => tests/test_model_checks.py (94%) rename test_pipeline.py => tests/test_pipeline.py (95%) rename test_report_math.py => tests/test_report_math.py (97%) rename test_resolve.py => tests/test_resolve.py (95%) rename test_result_tables.py => tests/test_result_tables.py (90%) rename test_review.py => tests/test_review.py (93%) rename test_script_export.py => tests/test_script_export.py (92%) rename test_solver_stage.py => tests/test_solver_stage.py (94%) rename Verstehen-Prompt.txt => understanding/Verstehen-Prompt.txt (100%) create mode 100644 understanding/__init__.py create mode 100644 understanding/understand.py diff --git a/README.md b/README.md index c26e5d7..9c0ee2e 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report ausgabe/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 report.py ausgabe/ergebnis.json ausgabe/modell.md +.venv/bin/python -m reporting.report ausgabe/ergebnis.json ausgabe/modell.md # exportiertes Solver-Programm erneut ausführen .venv/bin/python ausgabe/solve.py @@ -157,36 +157,36 @@ Trotzdem gilt: **Ergebnisse vor Verwendung prüfen** – der Report zeigt dafür `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 benchmark.py --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3 -.venv/bin/python benchmark.py --cases hard_schichtplan_woche --runs 3 +.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 `aufgaben/`. Hinweis: Drei Fälle wurden nach dieser Messung sprachlich neu gefasst (gleiche Zahlen und Optima). +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 test_solver_stage.py -k cvxpy # einzelner Test +.venv/bin/python -m pytest -q tests/test_solver_stage.py -k cvxpy # einzelner Test ``` ## Projektstruktur -| Datei | Inhalt | +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 Pipeline (`run_pipeline`) | -| `model_schema.py` | Pydantic-Schemas aller Zwischenergebnisse | -| `model_checks.py` | deterministische Konsistenz-Checks | -| `solver_stage.py` | Solver-Code erzeugen, prüfen, ausführen | -| `llm.py` | LLM-Zugriff (OpenRouter/Gemini), Wiederholungen, Kostenerfassung | -| `report.py` | Report als Markdown/HTML (mit Kopier-Symbol), auch als eigenständiges CLI | -| `script_export.py` | dokumentiertes, eigenständiges Solver-Programm (Kopf mit Aufgabe, Modell, Algorithmus) | -| `data_profile.py` | Profil beigelegter CSV-/Excel-Dateien, ohne LLM | -| `charts.py` | Diagramme und Graphenzeichnung als Inline-SVG | -| `benchmark.py`, `benchmark/cases/` | Benchmark und Referenzaufgaben | -| `aufgaben/` | weitere Aufgaben zum Ausprobieren (u. a. mit fehlenden Daten) | -| `*-Prompt.txt` | System-Prompts für Verstehen, Extraktion, Prüfung und Solver-Code | -| `test_*.py` | Offline-Tests | +| `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 diff --git a/TextToModel.py b/TextToModel.py index c8409fb..f98eb07 100644 --- a/TextToModel.py +++ b/TextToModel.py @@ -5,10 +5,10 @@ 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 ( +from checks.model_checks import check_spec, check_understanding, has_errors +from checks.review import needs_work, review_assumptions, select_best +from core.llm import DEFAULT_MODEL, LLMSession +from core.model_schema import ( AssumptionReview, SolverStatus, CheckIssue, @@ -17,14 +17,13 @@ from model_schema import ( 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 +from data import data_profile +from modeling.extraction import revise_model, text_to_model +from reporting.report import write_report +from solving.script_export import build_solver_script +from solving.solver_stage import check_feasibility, check_multistart, check_sensitivity, run_solver_code, solve +from understanding.understand import understand_task -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")) @@ -48,119 +47,6 @@ def log(msg: str) -> None: 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: diff --git a/benchmark/__init__.py b/benchmark/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmark.py b/benchmark/benchmark.py similarity index 96% rename from benchmark.py rename to benchmark/benchmark.py index 1817cc9..7a1a309 100644 --- a/benchmark.py +++ b/benchmark/benchmark.py @@ -15,10 +15,11 @@ from functools import lru_cache from datetime import datetime from pathlib import Path -from llm import DEFAULT_MODEL, is_fatal_error +from core.llm import DEFAULT_MODEL, is_fatal_error from TextToModel import run_pipeline -BENCHMARK_DIR = Path(__file__).parent / "benchmark" +BENCHMARK_DIR = Path(__file__).parent +ROOT = BENCHMARK_DIR.parent CASES_DIR = BENCHMARK_DIR / "cases" RESULTS_DIR = BENCHMARK_DIR / "results" @@ -27,7 +28,8 @@ quota_exhausted = threading.Event() # Dateien, die das LLM-Verhalten steuern: die drei System-Prompts und die Feldbeschreibungen # (Field(description=...) geht als JSON-Schema mit und wirkt wie eine Prompt-Anweisung). -PROMPT_FILES = ("Routing-und-Extraktions-Prompt.txt", "Pruef-Prompt.txt", "Solver-Prompt.txt", "model_schema.py") +PROMPT_FILES = ("modeling/Routing-und-Extraktions-Prompt.txt", "checks/Pruef-Prompt.txt", "solving/Solver-Prompt.txt", + "core/model_schema.py") # Umgebungsvariablen, die das Verhalten ändern – ohne sie ließe sich ein A/B-Lauf nicht zuordnen CONFIG_VARS = ("LLM_MODEL", "LLM_MODEL_EXTRACTION", "LLM_MODEL_REVIEW", "LLM_MODEL_REVISION", "LLM_MODEL_SOLVER", @@ -38,7 +40,7 @@ CONFIG_VARS = ("LLM_MODEL", "LLM_MODEL_EXTRACTION", "LLM_MODEL_REVIEW", "LLM_MOD def prompt_digest(paths=None) -> str: """Kurzer Hash über alle Dateien, die das LLM-Verhalten steuern.""" digest = hashlib.sha256() - for path in paths if paths is not None else [Path(__file__).parent / n for n in PROMPT_FILES]: + for path in paths if paths is not None else [ROOT / n for n in PROMPT_FILES]: digest.update(Path(path).read_bytes()) return digest.hexdigest()[:12] @@ -55,7 +57,7 @@ def prompt_state() -> dict: if gesetzt: state["config"] = gesetzt def git(*args): - return subprocess.run(["git", *args], cwd=Path(__file__).parent, capture_output=True, + return subprocess.run(["git", *args], cwd=ROOT, capture_output=True, text=True, timeout=10).stdout.strip() try: state["commit"] = git("rev-parse", "--short", "HEAD") or "unbekannt" diff --git a/Pruef-Prompt.txt b/checks/Pruef-Prompt.txt similarity index 100% rename from Pruef-Prompt.txt rename to checks/Pruef-Prompt.txt diff --git a/checks/__init__.py b/checks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model_checks.py b/checks/model_checks.py similarity index 99% rename from model_checks.py rename to checks/model_checks.py index a5f859f..f4487b6 100644 --- a/model_checks.py +++ b/checks/model_checks.py @@ -6,7 +6,7 @@ warning -> wird nur berichtet (Heuristiken mit möglichen Fehlalarmen) import re from typing import Iterable, List, Set -from model_schema import ( +from core.model_schema import ( CheckIssue, CheckSeverity, OptimizationModelSpecification, diff --git a/checks/review.py b/checks/review.py new file mode 100644 index 0000000..68e1a21 --- /dev/null +++ b/checks/review.py @@ -0,0 +1,50 @@ +"""Stufe 4 der Pipeline: LLM-Prüfung von Zielgröße und Annahmen; Entscheidung über Korrekturrunden.""" +from pathlib import Path +from typing import List + +from checks.model_checks import has_errors +from core.llm import LLMSession +from core.model_schema import AssumptionReview, CheckIssue, CheckSeverity, OptimizationModelSpecification + +REVIEW_PROMPT_FILE = Path(__file__).parent / "Pruef-Prompt.txt" + + +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", + ) + + +# 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) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llm.py b/core/llm.py similarity index 99% rename from llm.py rename to core/llm.py index 5afc376..cf6def7 100644 --- a/llm.py +++ b/core/llm.py @@ -11,7 +11,7 @@ import time import instructor -from model_schema import CallRecord, LLMUsage +from core.model_schema import CallRecord, LLMUsage DEFAULT_MODEL = os.environ.get("LLM_MODEL", "deepseek/deepseek-v4.1-flash") # Modell je Pipeline-Stufe: ein starkes Modell für Extraktion/Prüfung, ein schnelles für den Solver-Code diff --git a/model_schema.py b/core/model_schema.py similarity index 100% rename from model_schema.py rename to core/model_schema.py diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_profile.py b/data/data_profile.py similarity index 98% rename from data_profile.py rename to data/data_profile.py index 4c21f3a..60db6d6 100644 --- a/data_profile.py +++ b/data/data_profile.py @@ -8,7 +8,7 @@ from pathlib import Path import pandas as pd -from model_schema import ColumnProfile, DataFileProfile +from core.model_schema import ColumnProfile, DataFileProfile SAMPLE_ROWS = 3 # so viele Beispielwerte je Spalte in den Prompt MAX_COLUMNS = 40 # breitere Tabellen werden abgeschnitten, damit der Prompt nicht platzt diff --git a/Routing-und-Extraktions-Prompt.txt b/modeling/Routing-und-Extraktions-Prompt.txt similarity index 100% rename from Routing-und-Extraktions-Prompt.txt rename to modeling/Routing-und-Extraktions-Prompt.txt diff --git a/modeling/__init__.py b/modeling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modeling/extraction.py b/modeling/extraction.py new file mode 100644 index 0000000..d68af97 --- /dev/null +++ b/modeling/extraction.py @@ -0,0 +1,68 @@ +"""Stufen 2 und 5 der Pipeline: Modellspezifikation aus dem Text extrahieren und nach der Prüfung korrigieren.""" +from pathlib import Path +from typing import List + +from core.llm import LLMSession +from core.model_schema import ( + AssumptionReview, + CheckIssue, + CheckSeverity, + OptimizationModelSpecification, + TaskUnderstanding, +) + +ROUTING_PROMPT_FILE = Path(__file__).parent / "Routing-und-Extraktions-Prompt.txt" + + +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 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", + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6115fb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = [".", "tests"] diff --git a/reporting/__init__.py b/reporting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/charts.py b/reporting/charts.py similarity index 100% rename from charts.py rename to reporting/charts.py diff --git a/report.py b/reporting/report.py similarity index 99% rename from report.py rename to reporting/report.py index 35f2878..dc5e69f 100644 --- a/report.py +++ b/reporting/report.py @@ -9,10 +9,10 @@ import re from datetime import datetime from pathlib import Path -import charts -from model_schema import PipelineResult, SolverStatus -from script_export import build_solver_script -from solver_stage import _is_sum_row +from reporting import charts +from core.model_schema import PipelineResult, SolverStatus +from solving.script_export import build_solver_script +from solving.solver_stage import _is_sum_row class Math(str): diff --git a/Solver-Prompt.txt b/solving/Solver-Prompt.txt similarity index 100% rename from Solver-Prompt.txt rename to solving/Solver-Prompt.txt diff --git a/solving/__init__.py b/solving/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script_export.py b/solving/script_export.py similarity index 98% rename from script_export.py rename to solving/script_export.py index 94e1344..c30f1a3 100644 --- a/script_export.py +++ b/solving/script_export.py @@ -13,8 +13,8 @@ Das JSON-Ergebnis enthält weiterhin nur den rohen generierten Code (solver_code import re import textwrap -from model_schema import PipelineResult, ProblemClass, TargetStack -from solver_stage import assemble_script +from core.model_schema import PipelineResult, ProblemClass, TargetStack +from solving.solver_stage import assemble_script WIDTH = 88 diff --git a/solver_stage.py b/solving/solver_stage.py similarity index 99% rename from solver_stage.py rename to solving/solver_stage.py index d992ac8..ec66b4a 100644 --- a/solver_stage.py +++ b/solving/solver_stage.py @@ -15,9 +15,9 @@ import tempfile from pathlib import Path from typing import List, Optional -from llm import LLMSession -from model_checks import numbers_in, unexplained_jargon -from model_schema import ( +from core.llm import LLMSession +from checks.model_checks import numbers_in, unexplained_jargon +from core.model_schema import ( Answer, TargetStack, Scenario, diff --git a/aufgaben/komplexes_Portfolio_Management.md b/tasks/komplexes_Portfolio_Management.md similarity index 100% rename from aufgaben/komplexes_Portfolio_Management.md rename to tasks/komplexes_Portfolio_Management.md diff --git a/aufgaben/komplexes_Portfolio_Management_Nr2.md b/tasks/komplexes_Portfolio_Management_Nr2.md similarity index 100% rename from aufgaben/komplexes_Portfolio_Management_Nr2.md rename to tasks/komplexes_Portfolio_Management_Nr2.md diff --git a/aufgaben/nur_formeln.md b/tasks/nur_formeln.md similarity index 100% rename from aufgaben/nur_formeln.md rename to tasks/nur_formeln.md diff --git a/test_charts.py b/tests/test_charts.py similarity index 94% rename from test_charts.py rename to tests/test_charts.py index 25346fb..536af6e 100644 --- a/test_charts.py +++ b/tests/test_charts.py @@ -3,9 +3,9 @@ import xml.etree.ElementTree as ET import pytest -import charts -from model_schema import PipelineResult, ResultTable, SolverRun, SolverStatus -from report import build_document, to_html, to_markdown +from reporting import charts +from core.model_schema import PipelineResult, ResultTable, SolverRun, SolverStatus +from reporting.report import build_document, to_html, to_markdown from test_model_checks import make_spec from test_result_tables import PORTFOLIO_TABLE from test_review import make_review @@ -102,7 +102,7 @@ def test_no_chart_without_usable_numbers(table): def test_graph_chart_is_valid_svg_and_marks_the_path(): import xml.etree.ElementTree as ET - from charts import graph_chart + from reporting.charts import graph_chart svg = graph_chart(["0", "1", "2", "3"], [("0", "1", 8), ("0", "2", 18), ("0", "3", 31), ("1", "2", 10), ("1", "3", 21), ("2", "3", 12)], path=["0", "1", "3"], title="Graph") @@ -113,15 +113,15 @@ def test_graph_chart_is_valid_svg_and_marks_the_path(): def test_graph_chart_refuses_unknown_nodes_and_huge_graphs(): - from charts import graph_chart + from reporting.charts import graph_chart assert graph_chart(["a"], [("a", "b", 1)]) == "" # Bogen ins Nichts assert graph_chart([str(i) for i in range(20)], [("0", "1", 1)]) == "" # zu groß zum Zeichnen assert graph_chart([], []) == "" def test_graph_section_explains_before_it_shows(): - from model_schema import GraphSolution, SolverRun, SolverStatus - from report import _graph_section + from core.model_schema import GraphSolution, SolverRun, SolverStatus + from reporting.report import _graph_section run = SolverRun(status=SolverStatus.OPTIMAL, objective_value=29, graph=GraphSolution.model_validate({ "nodes": ["0", "1", "3"], "arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}], diff --git a/test_data_profile.py b/tests/test_data_profile.py similarity index 96% rename from test_data_profile.py rename to tests/test_data_profile.py index 5178ae4..f42ac2b 100644 --- a/test_data_profile.py +++ b/tests/test_data_profile.py @@ -1,7 +1,7 @@ """Profil beigelegter Datendateien – deterministisch, ohne LLM.""" import pytest -from data_profile import profile_file, profile_files, to_prompt +from data.data_profile import profile_file, profile_files, to_prompt @pytest.fixture diff --git a/test_feasibility.py b/tests/test_feasibility.py similarity index 96% rename from test_feasibility.py rename to tests/test_feasibility.py index 6090fce..81c7bfa 100644 --- a/test_feasibility.py +++ b/tests/test_feasibility.py @@ -1,9 +1,9 @@ """Deterministische Zulässigkeitsprüfung: Lösung einsetzen, Schranken, Ganzzahligkeit, Namensvertrag.""" import pytest -from model_schema import PipelineResult, Sensitivity, SolverRun, SolverStatus -from report import build_document, to_html -from solver_stage import check_constraint_coverage, check_feasibility, check_sensitivity +from core.model_schema import PipelineResult, Sensitivity, SolverRun, SolverStatus +from reporting.report import build_document, to_html +from solving.solver_stage import check_constraint_coverage, check_feasibility, check_sensitivity from test_model_checks import make_spec from test_review import make_review diff --git a/test_llm.py b/tests/test_llm.py similarity index 97% rename from test_llm.py rename to tests/test_llm.py index df0ee42..fd771b8 100644 --- a/test_llm.py +++ b/tests/test_llm.py @@ -2,9 +2,9 @@ import threading import pytest -import llm -from llm import is_fatal_error, is_transient_error, model_for_step, provider_for -from model_schema import LLMUsage +from core import llm +from core.llm import is_fatal_error, is_transient_error, model_for_step, provider_for +from core.model_schema import LLMUsage class FakeCompletions: diff --git a/test_model_checks.py b/tests/test_model_checks.py similarity index 94% rename from test_model_checks.py rename to tests/test_model_checks.py index 235036d..d60e0ae 100644 --- a/test_model_checks.py +++ b/tests/test_model_checks.py @@ -1,5 +1,5 @@ -from model_checks import check_spec, has_errors -from model_schema import OptimizationModelSpecification +from checks.model_checks import check_spec, has_errors +from core.model_schema import OptimizationModelSpecification def make_spec(**overrides) -> OptimizationModelSpecification: @@ -131,8 +131,8 @@ def test_miqp_without_discrete_vars_is_warning(): def test_every_problem_class_has_allowed_stacks(): - from model_checks import ALLOWED_STACKS - from model_schema import ProblemClass + from checks.model_checks import ALLOWED_STACKS + from core.model_schema import ProblemClass assert set(ALLOWED_STACKS) == set(ProblemClass) @@ -185,7 +185,7 @@ def test_single_term_objective_is_never_mixed(): def test_index_range_is_not_split_into_terms(): # \sum_{i \in I} enthält kein Minus, wohl aber Klammern – der Term darf nicht zerschnitten werden - from model_checks import _additive_terms + from checks.model_checks import _additive_terms assert len(_additive_terms("\\min \\sum_{j \\in J} c_j x_j")) == 1 assert len(_additive_terms("\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w")) == 2 assert len(_additive_terms("\\min z = a x + b y")) == 2 # linke Seite zählt nicht mit @@ -266,7 +266,7 @@ def test_explained_jargon_is_accepted(): def test_everyday_words_are_not_jargon(): - from model_checks import unexplained_jargon + from checks.model_checks import unexplained_jargon assert unexplained_jargon("Menge je Produkt und Lager, Kosten in Euro", "Eine schlichte Zusammenfassung.") == [] @@ -304,13 +304,14 @@ def test_every_checker_marker_in_the_prompts_exists(): """ import re from pathlib import Path - import model_checks - import solver_stage + from checks import model_checks + from solving import solver_stage quelle = Path(model_checks.__file__).read_text() + Path(solver_stage.__file__).read_text() bekannt = set(re.findall(r'code="([A-Z_]+)"', quelle)) | set(re.findall(r"^def (\w+)", quelle, re.M)) marker = set() - for datei in Path(model_checks.__file__).parent.glob("*-Prompt.txt"): + projekt = Path(model_checks.__file__).parent.parent + for datei in projekt.glob("*/*-Prompt.txt"): for treffer in re.findall(r"\[→ ([^\]]+)\]", datei.read_text(encoding="utf-8")): marker |= {teil.strip() for teil in treffer.split(",") if teil.strip() != "…"} # Legende ausnehmen assert marker, "keine Prüfer-Kennungen in den Prompts gefunden" @@ -320,7 +321,7 @@ def test_every_checker_marker_in_the_prompts_exists(): # --- Verstehensstufe: Zahlen und Teilfragen des Aufgabentexts --- def make_understanding(**overrides): - from model_schema import TaskUnderstanding + from core.model_schema import TaskUnderstanding daten = {"is_optimization_task": True, "reason": "-", "facts": [], "ignored_numbers": [], "sub_questions": [{"id": "1", "quote": "-", "kind": "rechnen", "needs_solver": True}]} daten.update(overrides) @@ -328,7 +329,7 @@ def make_understanding(**overrides): def test_numbers_are_normalised_once(): - from model_checks import normalize_number + from checks.model_checks import normalize_number assert normalize_number("1.234,56") == "1234.56" # deutsch: Punkt trennt Tausender assert normalize_number("3.8") == "3.8" # Aufgabennummer, kein Tausendertrenner assert normalize_number("1.234") == "1234" @@ -337,12 +338,12 @@ def test_numbers_are_normalised_once(): def test_numbers_inside_words_are_not_numbers(): - from model_checks import numbers_in + from checks.model_checks import numbers_in assert numbers_in("Aktie A02, Variable x1, Schicht 3") == {"3"} def test_uncaptured_numbers_are_reported(): - from model_checks import check_understanding + from checks.model_checks import check_understanding # beobachtet: 0 von 14 Zahlen des Aufgabentexts landeten in der Spezifikation aufgabe = "Der Deckungsbeitrag ist 12 € je Stück. Mindestens 100 Stück sind abzusetzen." codes = {i.code for i in check_understanding(make_understanding(), aufgabe)} @@ -351,7 +352,7 @@ def test_uncaptured_numbers_are_reported(): def test_numbers_quoted_in_a_fact_count_as_captured(): # eine Tabellenzeile als Zitat deckt alle Zahlen dieser Zeile ab - from model_checks import check_understanding + from checks.model_checks import check_understanding aufgabe = "| A01 | 6,9 % | 0,68 % | 2,04 % |" verstanden = make_understanding(facts=[{ "value": "6,9 %", "unit": "%", "meaning": "Rendite von A01", "quote": "| A01 | 6,9 % | 0,68 % | 2,04 % |"}]) @@ -359,7 +360,7 @@ def test_numbers_quoted_in_a_fact_count_as_captured(): def test_explicitly_ignored_numbers_count_as_captured(): - from model_checks import check_understanding + from checks.model_checks import check_understanding aufgabe = "## Aufgabe 2.7: Der Preis ist 8 €." verstanden = make_understanding( facts=[{"value": "8", "unit": "€", "meaning": "Preis", "quote": "Der Preis ist 8 €."}], @@ -368,7 +369,7 @@ def test_explicitly_ignored_numbers_count_as_captured(): def test_missing_sub_questions_are_reported(): - from model_checks import check_understanding + from checks.model_checks import check_understanding aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!" verstanden = make_understanding(sub_questions=[{"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False}]) meldungen = [i.message for i in check_understanding(verstanden, aufgabe) if i.code == "MISSING_SUB_QUESTIONS"] @@ -376,7 +377,7 @@ def test_missing_sub_questions_are_reported(): def test_complete_sub_questions_are_accepted(): - from model_checks import check_understanding + from checks.model_checks import check_understanding aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!" verstanden = make_understanding(sub_questions=[ {"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False}, @@ -386,7 +387,7 @@ def test_complete_sub_questions_are_accepted(): def test_split_sub_questions_cover_their_parent(): # beobachtet an "Portfolio Nr. 2": Teil 2 als 2a/2b erfasst, gemeldet wurde "es fehlen: 2" - from model_checks import check_understanding + from checks.model_checks import check_understanding aufgabe = "* **Teil 1: Daten**\n* **Teil 2: Optimierung**\n* **Teil 3: Evaluation**" verstanden = make_understanding(sub_questions=[ {"id": i, "quote": "-", "kind": "rechnen", "needs_solver": True} for i in ("Teil 1", "Teil 2a", "Teil 2b", "Teil 3")]) @@ -399,7 +400,7 @@ def test_split_sub_questions_cover_their_parent(): def test_captured_facts_must_reach_the_specification(): # beobachtet: Verstehensstufe erfasste die ganze Kostenmatrix, extracted_values blieb None - from model_checks import check_data_transfer + from checks.model_checks import check_data_transfer verstanden = make_understanding(facts=[ {"value": "8", "unit": "€", "meaning": "Kosten 0->1", "quote": "| 0 | 8 | 18 | 31 |"}]) ohne = make_spec(parameters=[{"symbol": "c_{ij}", "dimension": "Matrix", "unit": "€", "description": "Kosten"}]) @@ -413,7 +414,7 @@ def test_captured_facts_must_reach_the_specification(): def test_data_transfer_tolerates_partially_transferred_values(): # Indexwerte und Zeilenbeschriftungen muessen nicht in extracted_values stehen - kein Fehlalarm - from model_checks import check_data_transfer + from checks.model_checks import check_data_transfer verstanden = make_understanding(facts=[ {"value": "0,1,2", "unit": "Jahr", "meaning": "moegliche Kaufzeitpunkte", "quote": "i = 0,1,2"}, {"value": "8", "unit": "€", "meaning": "Kosten", "quote": "| 0 | 8 |"}]) @@ -423,7 +424,7 @@ def test_data_transfer_tolerates_partially_transferred_values(): def test_data_transfer_check_is_quiet_without_understanding(): - from model_checks import check_data_transfer + from checks.model_checks import check_data_transfer assert check_data_transfer(make_spec(), None) == [] @@ -442,6 +443,6 @@ def test_sum_limits_are_not_an_objective_label(): def test_objective_label_is_still_stripped(): - from model_checks import _objective_without_label + from checks.model_checks import _objective_without_label assert _objective_without_label("Z = 12 x").strip() == "12 x" assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum") diff --git a/test_pipeline.py b/tests/test_pipeline.py similarity index 95% rename from test_pipeline.py rename to tests/test_pipeline.py index 8c19d9a..4555c42 100644 --- a/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1,6 +1,6 @@ """run_pipeline ohne LLM: Neustart bei Fremdproblem und Kennzeichnung unzuverlässiger Ergebnisse.""" -from benchmark import result_correct -from model_schema import ( +from benchmark.benchmark import result_correct +from core.model_schema import ( AssumptionReview, LLMUsage, OptimizationModelSpecification, @@ -114,7 +114,7 @@ def test_no_restart_when_disabled(): def test_report_shows_prominent_warning_for_unreliable_result(): - from report import build_document, to_html, to_markdown + from reporting.report import build_document, to_html, to_markdown session = FakeSession(specs=[HALLUCINATED] * 3, reviews=[MISMATCH] * 3) result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0) doc = build_document(result) @@ -125,7 +125,7 @@ def test_report_shows_prominent_warning_for_unreliable_result(): def test_report_shows_the_rejected_reading_of_each_assumption(): - from report import build_document, to_markdown + from reporting.report import build_document, to_markdown spec = make_spec(open_assumptions=[{ "unclear_point": "„aufeinanderfolgend“", "assumption": "Kalendertage, auch über das Wochenende", @@ -288,8 +288,8 @@ def test_unknown_stage_is_rejected(tmp_path): def test_warnings_reach_the_revision_prompt(): # Warnungen loesen keine Runde aus, muessen aber mitkorrigiert werden, wenn ohnehin eine stattfindet - from model_schema import CheckIssue, CheckSeverity - from TextToModel import revise_model + from core.model_schema import CheckIssue, CheckSeverity + from modeling.extraction import revise_model session = FakeSession([GOOD], [], []) revise_model(session, "Aufgabe", GOOD, OK, issues=[ CheckIssue(severity=CheckSeverity.WARNING, code="UNEXPLAINED_JARGON", message="Fachbegriffe: Fluss"), @@ -303,17 +303,17 @@ def test_warnings_reach_the_revision_prompt(): # --- Benchmark haelt fest, womit gemessen wurde --- def test_benchmark_records_the_prompt_state(): - from benchmark import PROMPT_FILES, prompt_state + from benchmark.benchmark import PROMPT_FILES, prompt_state stand = prompt_state() assert {"prompts", "commit", "dirty"} <= set(stand) assert len(stand["prompts"]) == 12 and stand["commit"] - assert "Routing-und-Extraktions-Prompt.txt" in PROMPT_FILES - assert "model_schema.py" in PROMPT_FILES # Feldbeschreibungen wirken wie Prompt-Anweisungen + assert "modeling/Routing-und-Extraktions-Prompt.txt" in PROMPT_FILES + assert "core/model_schema.py" in PROMPT_FILES # Feldbeschreibungen wirken wie Prompt-Anweisungen def test_deviating_settings_are_recorded(monkeypatch): # Ohne die Umgebung liesse sich ein A/B-Lauf (Stufe abgeschaltet, anderes Modell) nicht zuordnen - import benchmark + from benchmark import benchmark benchmark.prompt_state.cache_clear() monkeypatch.setenv("UNDERSTAND_RETRIES", "-1") monkeypatch.setenv("LLM_MODEL_SOLVER", "billig/modell") @@ -323,7 +323,7 @@ def test_deviating_settings_are_recorded(monkeypatch): def test_prompt_digest_changes_with_the_prompt(tmp_path): - from benchmark import prompt_digest + from benchmark.benchmark import prompt_digest a, b = tmp_path / "eins.txt", tmp_path / "zwei.txt" a.write_text("Regel A"), b.write_text("Regel B") vorher = prompt_digest([a, b]) @@ -335,7 +335,7 @@ def test_prompt_digest_changes_with_the_prompt(tmp_path): def test_unexplained_jargon_triggers_a_revision_but_stays_reliable(): # Erklaeren ist Kernziel: die Warnung ist eine Runde wert, macht das Ergebnis aber nicht unzuverlaessig - from model_checks import check_spec + from checks.model_checks import check_spec unerklaert = make_spec( executive_summary="Der Betrieb entscheidet ueber Mengen.", constraints=[{"identifier": "Flusserhaltung", "latex_formula": "\\sum_j x_j = 1", "description": "-"}]) @@ -357,7 +357,7 @@ def test_clean_model_still_needs_no_revision(): # --- Verstehensstufe im Ablauf --- def test_understanding_runs_first_and_is_kept_in_the_result(): - from model_schema import Fact + from core.model_schema import Fact verstanden = TaskUnderstanding( is_optimization_task=True, reason="Kostenminimierung", ignored_numbers=[], sub_questions=[SubQuestion(id="a", quote="Wie viel?", kind="rechnen", needs_solver=True)], @@ -372,7 +372,7 @@ def test_understanding_runs_first_and_is_kept_in_the_result(): def test_incomplete_understanding_is_asked_again(): # erste Antwort uebersieht die Zahl, zweite erfasst sie - from model_schema import Fact + from core.model_schema import Fact luecke = TaskUnderstanding(is_optimization_task=True, reason="-", facts=[], ignored_numbers=[], sub_questions=[SubQuestion(id="1", quote="-", kind="rechnen", needs_solver=True)]) vollstaendig = luecke.model_copy(update={ @@ -385,8 +385,8 @@ def test_incomplete_understanding_is_asked_again(): def test_report_shows_questions_and_numbers(): - from model_schema import Fact - from report import build_document, to_markdown + from core.model_schema import Fact + from reporting.report import build_document, to_markdown verstanden = TaskUnderstanding( is_optimization_task=True, reason="-", sub_questions=[SubQuestion(id="a", quote="Formulieren Sie das Problem!", kind="formulieren", needs_solver=False), @@ -401,7 +401,7 @@ def test_report_shows_questions_and_numbers(): def test_report_puts_the_answers_before_the_numbers(): - from report import build_document, to_markdown + from reporting.report import build_document, to_markdown code = ''' def main(): """Loest und beantwortet.""" @@ -452,8 +452,8 @@ if __name__ == "__main__": def test_report_shows_data_profile_and_scenarios(tmp_path): - from model_schema import Scenario - from report import build_document, to_markdown + from core.model_schema import Scenario + from reporting.report import build_document, to_markdown datei = tmp_path / "werte.csv" datei.write_text("name,menge\nA,3\nB,4\n") code = ''' @@ -484,8 +484,8 @@ if __name__ == "__main__": # --- Benchmark misst "Aufgabe erfuellt", nicht nur "Zahl richtig" --- def test_benchmark_counts_unanswered_questions_as_not_fulfilled(monkeypatch): - import benchmark - from model_schema import Answer, SubQuestion, TaskUnderstanding + from benchmark import benchmark + from core.model_schema import Answer, SubQuestion, TaskUnderstanding def ergebnis(antworten, warnungen=()): session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)], understandings=[TaskUnderstanding( @@ -509,7 +509,7 @@ def test_benchmark_counts_unanswered_questions_as_not_fulfilled(monkeypatch): def test_benchmark_case_data_files_are_collected(tmp_path, monkeypatch): - import benchmark + from benchmark import benchmark fall = tmp_path / "cases" / "mit_daten" fall.mkdir(parents=True) (fall / "task.txt").write_text("Aufgabe") diff --git a/test_report_math.py b/tests/test_report_math.py similarity index 97% rename from test_report_math.py rename to tests/test_report_math.py index 7cf40f0..d7e4bb7 100644 --- a/test_report_math.py +++ b/tests/test_report_math.py @@ -1,6 +1,6 @@ import pytest -from report import Math, _html_inline, _md_inline, split_math +from reporting.report import Math, _html_inline, _md_inline, split_math def marked(text: str) -> str: diff --git a/test_resolve.py b/tests/test_resolve.py similarity index 95% rename from test_resolve.py rename to tests/test_resolve.py index a74a109..6ef51c6 100644 --- a/test_resolve.py +++ b/tests/test_resolve.py @@ -1,8 +1,8 @@ """Punkt 1: dasselbe Programm mit geänderten Daten neu rechnen – ohne LLM-Aufruf.""" import pytest -from model_schema import SolverStatus -from solver_stage import run_solver_code +from core.model_schema import SolverStatus +from solving.solver_stage import run_solver_code # Programm nach der Vorgabe des Solver-Prompts: alle Zahlen stehen in DATA DATA_CODE = ''' @@ -64,7 +64,7 @@ def test_infeasible_data_is_reported_not_crashing(): def gespeichertes_ergebnis(tmp_path): import json - from model_schema import PipelineResult, SolverCode + from core.model_schema import PipelineResult, SolverCode from test_model_checks import make_spec from test_review import make_review result = PipelineResult(task="Aufgabe", specification=make_spec(), review=make_review(), revisions=0, diff --git a/test_result_tables.py b/tests/test_result_tables.py similarity index 90% rename from test_result_tables.py rename to tests/test_result_tables.py index a05f2df..541d55c 100644 --- a/test_result_tables.py +++ b/tests/test_result_tables.py @@ -1,9 +1,9 @@ """Verständliche Ergebnistabellen: Ausgabe im Programm, Übernahme ins Ergebnis, Summenprüfung, Report.""" import pytest -from model_schema import PipelineResult, SolverRun, SolverStatus -from report import build_document, to_html -from solver_stage import parse_result_tables, run_solver_code +from core.model_schema import PipelineResult, SolverRun, SolverStatus +from reporting.report import build_document, to_html +from solving.solver_stage import parse_result_tables, run_solver_code from test_model_checks import make_spec from test_review import make_review @@ -34,7 +34,7 @@ def test_table_is_printed_and_parsed_without_warnings(): def test_standalone_output_shows_box_table(tmp_path): import subprocess, sys - from solver_stage import assemble_script + from solving.solver_stage import assemble_script path = tmp_path / "solve.py" path.write_text(assemble_script(f"report('optimal', 0.09756, {{}}, tables=[{PORTFOLIO_TABLE!r}])"), encoding="utf-8") out = subprocess.run([sys.executable, str(path)], capture_output=True, text=True, timeout=60).stdout @@ -140,13 +140,13 @@ def test_normal_decomposition_has_no_magnitude_warning(): def test_report_shows_multistart_stability(): - from model_schema import Multistart, SolverRun - from report import build_document, to_markdown + from core.model_schema import Multistart, SolverRun + from reporting.report import build_document, to_markdown from test_pipeline import FakeSession from TextToModel import run_pipeline from test_review import make_review from test_script_export import STRUCTURED_CODE - from model_schema import SolverCode + from core.model_schema import SolverCode session = FakeSession([make_spec()], [make_review()], [SolverCode(code=STRUCTURED_CODE)]) result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0) @@ -158,8 +158,8 @@ def test_report_shows_multistart_stability(): def test_transliterated_umlauts_are_reported(): # beobachtet: "Der guenstigste Ersatzplan", Tabelle "Gewaehlte Nutzungszeitraeume" - from model_schema import ResultTable, SolverRun, SolverStatus - from solver_stage import check_text_quality + from core.model_schema import ResultTable, SolverRun, SolverStatus + from solving.solver_stage import check_text_quality run = SolverRun(status=SolverStatus.OPTIMAL, headline="Der guenstigste Plan kostet 29 €.", tables=[ResultTable(title="Gewaehlte Zeitraeume", columns=["Kauf"], rows=[])]) befunde = check_text_quality(run) @@ -169,8 +169,8 @@ def test_transliterated_umlauts_are_reported(): def test_correct_german_is_not_flagged(): # Gegenprobe: echte Wörter mit ue/oe/ae dürfen keinen Fehlalarm auslösen - from model_schema import ResultTable, SolverRun, SolverStatus - from solver_stage import check_text_quality + from core.model_schema import ResultTable, SolverRun, SolverStatus + from solving.solver_stage import check_text_quality run = SolverRun(status=SolverStatus.OPTIMAL, headline="Die Quelle liefert neue Koeffizienten für tägliche Werte.", tables=[ResultTable(title="Gewählte Zeiträume", columns=["Menge", "Beitrag"], rows=[])]) @@ -179,8 +179,8 @@ def test_correct_german_is_not_flagged(): def test_transliterations_are_repaired_after_being_reported(): # Die Warnung dokumentiert den Prompt-Verstoss, der Report soll trotzdem lesbar sein - from model_schema import Answer, ResultTable, SolverRun, SolverStatus - from solver_stage import check_text_quality, fix_transliterations, repair_texts + from core.model_schema import Answer, ResultTable, SolverRun, SolverStatus + from solving.solver_stage import check_text_quality, fix_transliterations, repair_texts run = SolverRun(status=SolverStatus.OPTIMAL, headline="Der guenstigste Plan kostet 29 €.", answers=[Answer(question_id="a", text="Die Loesung waehlt zwei Aktien.")], tables=[ResultTable(title="Gewaehlte Zeitraeume", columns=["Naechste Periode"], @@ -196,12 +196,12 @@ def test_transliterations_are_repaired_after_being_reported(): def test_repair_leaves_names_and_english_alone(): - from solver_stage import fix_transliterations + from solving.solver_stage import fix_transliterations assert fix_transliterations("Mueller AG, Quelle, value, Koeffizient") == "Mueller AG, Quelle, value, Koeffizient" assert fix_transliterations("") == "" def test_repair_covers_stems_seen_in_the_lot_sizing_case(): # beobachtet in wagner_whitin_dijkstra: "110 Baelle", "Das heisst:" im Antworttext - from solver_stage import fix_transliterations + from solving.solver_stage import fix_transliterations assert fix_transliterations("Das heisst: 110 Baelle, der Ball") == "Das heißt: 110 Bälle, der Ball" diff --git a/test_review.py b/tests/test_review.py similarity index 93% rename from test_review.py rename to tests/test_review.py index 867e9ea..e85b3b1 100644 --- a/test_review.py +++ b/tests/test_review.py @@ -1,5 +1,5 @@ -from model_schema import AssumptionReview, CheckIssue, CheckSeverity -from TextToModel import select_best +from core.model_schema import AssumptionReview, CheckIssue, CheckSeverity +from checks.review import select_best def make_review(matches=True, verdict="justified", missing=(), units_consistent=True): @@ -35,7 +35,7 @@ def test_missing_assumption_triggers_revision(): assert make_review(missing=missing).needs_revision -# --- Auswahl der besten Korrekturrunde (TextToModel.select_best) --- +# --- Auswahl der besten Korrekturrunde (checks.review.select_best) --- ERROR = CheckIssue(severity=CheckSeverity.ERROR, code="CPSAT_WITH_CONTINUOUS_VARS", message="-") WARNING = CheckIssue(severity=CheckSeverity.WARNING, code="UNDECLARED_SYMBOL", message="-") diff --git a/test_script_export.py b/tests/test_script_export.py similarity index 92% rename from test_script_export.py rename to tests/test_script_export.py index b3d6dc1..18716b4 100644 --- a/test_script_export.py +++ b/tests/test_script_export.py @@ -5,10 +5,10 @@ import warnings import pytest -from model_schema import PipelineResult, SolverCode, SolverRun, SolverStatus -from report import build_document, to_html -from script_export import build_solver_script -from solver_stage import CodeRejected, assemble_script, run_solver_code, validate_structure +from core.model_schema import PipelineResult, SolverCode, SolverRun, SolverStatus +from reporting.report import build_document, to_html +from solving.script_export import build_solver_script +from solving.solver_stage import CodeRejected, assemble_script, run_solver_code, validate_structure from test_model_checks import make_spec from test_review import make_review @@ -136,7 +136,7 @@ def test_inline_math_delimiters_are_removed_from_docstring(): def test_every_stack_and_class_has_algorithm_text(): # Synchron-Regel: neuer TargetStack/ProblemClass braucht Texte in script_export - from model_schema import ProblemClass, TargetStack - from script_export import ALGORITHMS, CLASS_FIT + from core.model_schema import ProblemClass, TargetStack + from solving.script_export import ALGORITHMS, CLASS_FIT assert set(ALGORITHMS) == set(TargetStack) assert set(CLASS_FIT) == set(ProblemClass) diff --git a/test_solver_stage.py b/tests/test_solver_stage.py similarity index 94% rename from test_solver_stage.py rename to tests/test_solver_stage.py index de033e6..6c8d1c9 100644 --- a/test_solver_stage.py +++ b/tests/test_solver_stage.py @@ -2,8 +2,8 @@ import sys import pytest -from model_schema import SolverStatus -from solver_stage import CodeRejected, clean_generated_code, run_solver_code, validate_imports +from core.model_schema import SolverStatus +from solving.solver_stage import CodeRejected, clean_generated_code, run_solver_code, validate_imports # min 3x + 2y s.t. x + y >= 4, 0 <= x, y <= 3 -> x=1, y=3, Zielwert 9 HIGHS_CODE = """ @@ -260,7 +260,7 @@ def test_ipopt_multistart_finds_global_minimum_of_nonconvex_problem(): import shutil -from solver_stage import sandbox_command +from solving.solver_stage import sandbox_command ESCAPE_CODE = """ import numpy as np @@ -300,7 +300,7 @@ def test_sandbox_allows_writing_inside_the_run(): def test_sandbox_command_uses_bwrap_when_available(monkeypatch, tmp_path): - import solver_stage + from solving import solver_stage monkeypatch.setattr(solver_stage, "SANDBOX", "auto") monkeypatch.setattr(solver_stage.shutil, "which", lambda name: "/usr/bin/bwrap") befehl = sandbox_command(tmp_path / "solve.py", str(tmp_path)) @@ -311,13 +311,13 @@ def test_sandbox_command_uses_bwrap_when_available(monkeypatch, tmp_path): def test_sandbox_can_be_switched_off(monkeypatch, tmp_path): - import solver_stage + from solving import solver_stage monkeypatch.setattr(solver_stage, "SANDBOX", "aus") assert sandbox_command(tmp_path / "solve.py", str(tmp_path))[0] == sys.executable def test_missing_bwrap_is_reported_when_required(monkeypatch, tmp_path): - import solver_stage + from solving import solver_stage monkeypatch.setattr(solver_stage, "SANDBOX", "bwrap") monkeypatch.setattr(solver_stage.shutil, "which", lambda name: None) with pytest.raises(CodeRejected, match="bubblewrap"): @@ -325,7 +325,7 @@ def test_missing_bwrap_is_reported_when_required(monkeypatch, tmp_path): def test_missing_bwrap_falls_back_when_automatic(monkeypatch, tmp_path): - import solver_stage + from solving import solver_stage monkeypatch.setattr(solver_stage, "SANDBOX", "auto") monkeypatch.setattr(solver_stage.shutil, "which", lambda name: None) assert sandbox_command(tmp_path / "solve.py", str(tmp_path))[0] == sys.executable @@ -333,12 +333,12 @@ def test_missing_bwrap_falls_back_when_automatic(monkeypatch, tmp_path): # --- Mehrstart-Streuung (Ipopt): check_multistart --- -from model_schema import Multistart -from solver_stage import check_multistart +from core.model_schema import Multistart +from solving.solver_stage import check_multistart def multistart_run(**felder): - from model_schema import SolverRun + from core.model_schema import SolverRun werte = {"starts": 5, "converged": 5, "best": 1.125, "worst": 1.125, "mean": 1.125, "stddev": 0.0} werte.update(felder) return SolverRun(status=SolverStatus.FEASIBLE, objective_value=werte["best"], @@ -371,7 +371,7 @@ def test_single_successful_start_cannot_show_stability(): def test_no_multistart_means_no_check(): - from model_schema import SolverRun + from core.model_schema import SolverRun assert check_multistart(SolverRun(status=SolverStatus.OPTIMAL), "minimize") == [] @@ -406,8 +406,8 @@ if __name__ == "__main__": # --- Graphenprobleme (NetworkX/Dijkstra): API aus Solver-Prompt.txt und check_graph --- -from model_schema import GraphSolution -from solver_stage import check_graph +from core.model_schema import GraphSolution +from solving.solver_stage import check_graph DIJKSTRA_CODE = ''' import networkx as nx @@ -459,7 +459,7 @@ def test_dijkstra_program_from_solver_prompt(): def graph_run(**felder): - from model_schema import SolverRun + from core.model_schema import SolverRun werte = {"nodes": ["0", "1", "2"], "arcs": [{"source": "0", "target": "1", "weight": 4}, {"source": "1", "target": "2", "weight": 5}, @@ -482,7 +482,7 @@ def test_path_must_use_declared_arcs(): def test_path_length_must_match_objective(): - from model_schema import SolverRun + from core.model_schema import SolverRun run = graph_run() run.objective_value = 7.0 # 4 + 5 = 9 assert any("weicht vom Zielwert" in w for w in check_graph(run, "minimize")) @@ -504,18 +504,18 @@ def test_falling_labels_contradict_dijkstra(): # --- Abnahme: jede Teilfrage beantwortet, jede Zahl belegt (check_answers) --- -from solver_stage import check_answers +from solving.solver_stage import check_answers def verstaendnis(*fragen): - from model_schema import SubQuestion, TaskUnderstanding + from core.model_schema import SubQuestion, TaskUnderstanding return TaskUnderstanding( is_optimization_task=True, reason="-", facts=[], ignored_numbers=[], sub_questions=[SubQuestion(id=f, quote=f"Frage {f}", kind="rechnen", needs_solver=True) for f in fragen]) def geloest(antworten, **felder): - from model_schema import Answer, SolverRun, VariableValue + from core.model_schema import Answer, SolverRun, VariableValue werte = {"status": SolverStatus.OPTIMAL, "objective_value": 29.0, "variables": [VariableValue(name="x", value=1.0)], "answers": [Answer(question_id=k, text=t) for k, t in antworten]} @@ -545,7 +545,7 @@ def test_invented_numbers_in_the_answer_are_caught(): def test_comparison_numbers_belong_in_a_table(): # beobachtet: die Antwort nannte den Vergleichsweg "kostet 30" - richtig gerechnet, # aber in keinem gemeldeten Feld, also fuer den Leser nicht nachvollziehbar - from model_schema import ResultTable + from core.model_schema import ResultTable run = geloest([("b", "Der beste Weg kostet 29, die Alternative 30.")]) assert any("30" in b for b in check_answers(run, verstaendnis("b"))) mit_tabelle = geloest([("b", "Der beste Weg kostet 29, die Alternative 30.")], @@ -652,13 +652,13 @@ def test_scenarios_are_recomputed_and_confirmed(): run = run_solver_code(SZENARIO_CODE) assert run.objective_value == 30.0 assert [s.name for s in run.scenarios] == ["doppelter Preis", "halbe Menge"] - from solver_stage import verify_scenarios + from solving.solver_stage import verify_scenarios assert verify_scenarios(run, SZENARIO_CODE) == [] # gemeldet = nachgerechnet assert [s.verified_value for s in run.scenarios] == [60.0, 15.0] def test_wrong_scenario_value_is_caught(): - from solver_stage import verify_scenarios + from solving.solver_stage import verify_scenarios run = run_solver_code(SZENARIO_CODE) run.scenarios[0].objective_value = 99.0 # das Programm hat sich verrechnet befunde = verify_scenarios(run, SZENARIO_CODE) @@ -666,8 +666,8 @@ def test_wrong_scenario_value_is_caught(): def test_scenario_without_override_is_reported(): - from model_schema import Scenario - from solver_stage import verify_scenarios + from core.model_schema import Scenario + from solving.solver_stage import verify_scenarios run = run_solver_code(SZENARIO_CODE) run.scenarios = [Scenario(name="leer", description="-", data_override={}, objective_value=1.0)] assert any("keine Datenabweichung" in b for b in verify_scenarios(run, SZENARIO_CODE)) @@ -710,7 +710,7 @@ def test_numbers_from_the_attached_data_are_traceable(): def test_duplicate_question_ids_still_need_one_answer_each(): - from model_schema import SubQuestion, TaskUnderstanding + from core.model_schema import SubQuestion, TaskUnderstanding u = TaskUnderstanding(is_optimization_task=True, reason="-", facts=[], ignored_numbers=[], sub_questions=[ SubQuestion(id="a1", quote="Formulieren Sie …", kind="formulieren", needs_solver=False), SubQuestion(id="a2", quote="Zeichnen Sie …", kind="zeichnen", needs_solver=False)]) @@ -720,8 +720,8 @@ def test_duplicate_question_ids_still_need_one_answer_each(): def test_self_generated_data_must_be_declared(): # beobachtet an "Portfolio Nr. 2": synthetischer Markt mit festem Seed, data_assumptions leer - from model_schema import SolverCode - from solver_stage import check_generated_data + from core.model_schema import SolverCode + from solving.solver_stage import check_generated_data code = "import numpy as np\nrng = np.random.default_rng(42)\nmu = rng.normal(0.1, 0.05, 25)\n" assert check_generated_data(code, SolverCode(code=code, data_assumptions=[])) assert check_generated_data(code, SolverCode(code=code, data_assumptions=["mu, Sigma: synthetisch, Seed 42"])) == [] @@ -729,8 +729,8 @@ def test_self_generated_data_must_be_declared(): def test_fixed_seed_with_complete_data_is_not_generated_data(): # Ipopt-Mehrstart mit festem Zufallsstartwert: alle Werte stehen in der Spezifikation - from model_schema import SolverCode - from solver_stage import check_generated_data + from core.model_schema import SolverCode + from solving.solver_stage import check_generated_data from test_model_checks import make_spec code = "import numpy as np\nrng = np.random.default_rng(42)\nstarts = rng.uniform(0, 1, (5, 2))\n" ipopt = make_spec(problem_class="Nonlinear Programming (NLP)", target_stack="Ipopt (cyipopt)") @@ -739,8 +739,8 @@ def test_fixed_seed_with_complete_data_is_not_generated_data(): def test_prose_instead_of_values_does_not_hide_generated_data(): # Nachmessung "Portfolio Nr. 2": extracted_values war nicht leer, sondern Text ohne Werte - from model_schema import SolverCode - from solver_stage import check_generated_data + from core.model_schema import SolverCode + from solving.solver_stage import check_generated_data from test_model_checks import make_spec spec = make_spec(parameters=[{"symbol": "\\mu_i", "dimension": "Vektor", "unit": "1/Jahr", "description": "Rendite", "extracted_values": "aus Kursdaten berechnet (Teil 1); im Text nicht numerisch vorgegeben"}]) @@ -749,7 +749,7 @@ def test_prose_instead_of_values_does_not_hide_generated_data(): def test_ordinary_programs_are_not_suspected(): - from model_schema import SolverCode - from solver_stage import check_generated_data + from core.model_schema import SolverCode + from solving.solver_stage import check_generated_data code = "import highspy\nh = highspy.Highs()\nDATA = {'kosten': [3, 2]}\n" assert check_generated_data(code, SolverCode(code=code, data_assumptions=[])) == [] diff --git a/Verstehen-Prompt.txt b/understanding/Verstehen-Prompt.txt similarity index 100% rename from Verstehen-Prompt.txt rename to understanding/Verstehen-Prompt.txt diff --git a/understanding/__init__.py b/understanding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/understanding/understand.py b/understanding/understand.py new file mode 100644 index 0000000..1cb1d1c --- /dev/null +++ b/understanding/understand.py @@ -0,0 +1,26 @@ +"""Stufe 1 der Pipeline: erfassen, was im Aufgabentext steht – Teilfragen und Zahlen, noch kein Modell.""" +from pathlib import Path +from typing import List + +from core.llm import LLMSession +from core.model_schema import CheckIssue, TaskUnderstanding + +UNDERSTAND_PROMPT_FILE = Path(__file__).parent / "Verstehen-Prompt.txt" + + +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", + )