diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index b4797d3..b7062f5 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -11,7 +11,7 @@ Diese Anleitung beschreibt Schritt für Schritt, wie man das Programm installier 5. [Aufrufvarianten](#5-aufrufvarianten) 6. [Was das Programm ausgibt](#6-was-das-programm-ausgibt) 7. [Das Ergebnis richtig lesen](#7-das-ergebnis-richtig-lesen) -8. [Aufgaben mit Datendateien und Abbildungen](#8-aufgaben-mit-datendateien-und-abbildungen) +8. [Aufgaben mit Datendateien](#8-aufgaben-mit-datendateien) 9. [Weiterarbeiten ohne neue LLM-Kosten](#9-weiterarbeiten-ohne-neue-llm-kosten) 10. [Kosten und Laufzeit steuern](#10-kosten-und-laufzeit-steuern) 11. [Alle Einstellungen im Überblick](#11-alle-einstellungen-im-überblick) @@ -279,7 +279,7 @@ Der Report beginnt mit dem Ergebnis und erklärt, bevor er Tabellen zeigt. Empfo --- -## 8. Aufgaben mit Datendateien und Abbildungen +## 8. Aufgaben mit Datendateien Für größere Datenmengen (Kurse, Bedarfe, Entfernungen) legt man Dateien bei: @@ -302,36 +302,6 @@ cp kurse.csv entfernungen.xlsx output/ --- -### Aufgaben mit Abbildungen (z. B. aus docling) - -Stehen Daten nur in einer Abbildung – ein Graph mit bewerteten Pfeilen, eine Tabelle als Bild –, kann die Aufgabe die Abbildung als Markdown-Bild enthalten. Beide Formen, die docling erzeugt, werden erkannt: - -- **eingebettet** (`ImageRefMode.EMBEDDED`, Standard): `` -- **als Datei** (`ImageRefMode.REFERENCED`): `` – der Pfad gilt relativ zur Aufgabendatei - -```bash -# Dokument mit docling nach Markdown wandeln (Bilder eingebettet), dann lösen -docling aufgabe.pdf --to md --image-export-mode embedded --output output/ -.venv/bin/python TextToModel.py output/aufgabe.md --report output/aufgabe.html > output/aufgabe.json -``` - -Was dabei passiert: - -1. Die Bilder werden aus dem Text gelöst; im Text steht dort `[Abbildung 1]`. -2. **Nur die Verstehensstufe** bekommt die Bilder und überträgt jede Abbildung vollständig in Text (jede Zahl, jeder Pfeil mit Richtung und Bewertung, unsicher Gelesenes wird benannt). -3. **Gegenprobe:** Ein zweiter, unabhängiger Aufruf überträgt die Abbildungen noch einmal. Weichen die Zahlen ab, entscheidet eine dritte Lesung; stimmen keine zwei überein, gilt das Ergebnis als unzuverlässig. -4. Alle weiteren Stufen rechnen mit der Übertragung – das Bild selbst wird nicht erneut gesendet. -5. Der Report zeigt im Abschnitt „Aufgabe“ **Original und Übertragung nebeneinander**. Bitte vergleichen: Ein verlesener Wert ändert das ganze Ergebnis, und eine vertauschte Pfeilrichtung bei gleichen Zahlen fällt der Gegenprobe nicht auf. - -Voraussetzungen und Grenzen: - -- Das Modell der Verstehensstufe muss Bilder lesen können. Das Standardmodell `~deepseek/deepseek-flash-latest` (derzeit DeepSeek V4.1 Flash) kann das; sonst ein Bildmodell über `LLM_MODEL_UNDERSTANDING` wählen (z. B. `qwen/qwen3.8-flash`). -- Höchstens 6 Abbildungen, je 5 MB. -- Mehrkosten: ein bis zwei zusätzliche kleine Aufrufe für die Gegenprobe. -- Verweise auf Webadressen werden nicht geladen (das Programm lädt nichts aus dem Internet). - ---- - ## 9. Weiterarbeiten ohne neue LLM-Kosten ### Andere Zahlen, gleiches Modell: `--resolve` @@ -401,7 +371,7 @@ Wo das Geld hingeht: rund zwei Drittel der Kosten sind **erzeugter Output**, dav **Prompt-Cache:** Alle Aufrufe eines Laufs gehen an denselben OpenRouter-Anbieter, damit dessen Cache wiederholt gelesene Prompt-Teile billiger abrechnen kann. Der Cache-Anteil steht in der Abschlusszeile. -**Standardmodell ist der Alias `~deepseek/deepseek-flash-latest`** – er zeigt immer auf das neueste DeepSeek-Flash-Modell (September 2026: V4.1 Flash). Das hält die Pipeline aktuell, kann Ergebnisse aber unbemerkt verändern, sobald DeepSeek ein neues Flash-Modell veröffentlicht. **Für Benchmarks und Vergleiche feste Modellnamen verwenden** (z. B. `--models deepseek/deepseek-v4.1-flash`). +**Feste Modellnamen verwenden** (z. B. `deepseek/deepseek-v4.1-flash`), keine `…-latest`-Aliase – sonst ändern sich Ergebnisse unbemerkt. --- @@ -429,8 +399,7 @@ Wo das Geld hingeht: rund zwei Drittel der Kosten sind **erzeugter Output**, dav |---|---|---| | `OPENROUTER_API_KEY` | Schlüssel für OpenRouter | – | | `GEMINI_API_KEY` | Schlüssel für Gemini | – | -| `LLM_MODEL` | Modell, falls `--model` fehlt | `~deepseek/deepseek-flash-latest` | -| `LLM_MODEL_UNDERSTANDING` | Modell für Verstehen und Abbildungs-Gegenprobe (muss bei Abbildungen Bilder lesen können) | Session-Modell | +| `LLM_MODEL` | Modell, falls `--model` fehlt | `deepseek/deepseek-v4.1-flash` | | `LLM_MODEL_EXTRACTION` | Modell nur für die Extraktion | Session-Modell | | `LLM_MODEL_REVIEW` | Modell nur für die Prüfung | Session-Modell | | `LLM_MODEL_REVISION` | Modell nur für die Korrektur | Session-Modell | diff --git a/README.md b/README.md index 597e57f..640b564 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Exit-Codes: `0` Ergebnis zuverlässig, `2` Ergebnis vorhanden, aber unzuverläss |---|---|---| | `--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-flash-latest` (Alias, derzeit V4.1 Flash) | +| `--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 | @@ -116,7 +116,7 @@ Exit-Codes: `0` Ergebnis zuverlässig, `2` Ergebnis vorhanden, aber unzuverläss | `LLM_TIMEOUT` | Timeout je LLM-Aufruf (s) | 420 | | `LLM_REASONING_EFFORT` | Denkaufwand des Modells über OpenRouter: `low`, `medium` oder `high` | Modellvorgabe | | `SOLVER_SANDBOX` | Isolierung des erzeugten Codes: `auto` (bubblewrap, wenn vorhanden), `bwrap` (Pflicht), `aus` | `auto` | -| `LLM_MODEL_UNDERSTANDING`, `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` | +| `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 @@ -152,7 +152,6 @@ Das LLM kann Fehler machen – deshalb gibt es mehrere Sicherungen: - **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. -- **Abbildungen werden gelesen und gegengeprüft:** Enthält die Aufgabe Bilder (docling-Markdown, eingebettet oder als Datei), überträgt die Verstehensstufe sie in Text; eine zweite, unabhängige Lesung wird Zahl für Zahl verglichen. Alle weiteren Stufen rechnen mit der Übertragung, der Report zeigt Original und Übertragung nebeneinander. - **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. Hat ein Szenario kein Ergebnis und ist nicht als unlösbar belegt (beobachtet: Zeitlimit abgelaufen, gemeldet als „keine Lösung“), folgt eine Reparatur und notfalls die Markierung „unzuverlässig“. - **Keine erfundenen Einheiten:** Eine Einheit hinter einer Zahl im Antworttext muss im Aufgabentext oder in beigelegten Daten vorkommen (nach Größenart: „Tausend Euro“ deckt „€“); neutrale Geld- und Mengeneinheiten (GE, ME) nur, wenn der Text von Kosten bzw. Mengen spricht. Sonst folgt eine Reparatur. diff --git a/TextToModel.py b/TextToModel.py index 151d7c9..32ecd88 100644 --- a/TextToModel.py +++ b/TextToModel.py @@ -5,7 +5,7 @@ import sys from pathlib import Path from typing import List -from checks.model_checks import check_figure_readings, check_spec, check_understanding, has_errors +from checks.model_checks import check_spec, check_understanding, has_errors from checks.review import needs_work, review_assumptions, revision_reasons, select_best from core.llm import DEFAULT_MODEL, LLMSession from core.model_schema import ( @@ -22,8 +22,7 @@ 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.figures import FigureError, normalize_figure_ids, split_figures, with_transcriptions -from understanding.understand import read_figures, understand_task +from understanding.understand import understand_task MAX_REVISIONS = int(os.environ.get("MAX_REVISIONS", "2")) SOLVER_RETRIES = int(os.environ.get("SOLVER_RETRIES", "2")) @@ -48,19 +47,19 @@ def log(msg: str) -> None: print(msg, file=sys.stderr) -def _understand(session, task: str, label: str, data_prompt: str = "", figures=()) -> tuple: +def _understand(session, task: str, label: str, data_prompt: str = "") -> tuple: """Verstehensstufe samt Prüfung; ein Nachfassen, wenn Zahlen oder Teilfragen fehlen.""" if UNDERSTAND_RETRIES < 0: return None, [] - log(f"[{label}] Lese die Aufgabe (Teilfragen, Zahlen" + (", Abbildungen" if figures else "") + ") ...") - understanding = understand_task(session, task, data_prompt=data_prompt, figures=figures) + log(f"[{label}] Lese die Aufgabe (Teilfragen, Zahlen) ...") + understanding = understand_task(session, task, data_prompt=data_prompt) issues = check_understanding(understanding, task) for versuch in range(UNDERSTAND_RETRIES): if not issues or not understanding.is_optimization_task: break # eine Nicht-Aufgabe wird abgelehnt, nicht vervollständigt log(f"[{label}] Verständnis unvollständig – fasse nach ({versuch + 1}/{UNDERSTAND_RETRIES}): " + "; ".join(i.code for i in issues)) - understanding = understand_task(session, task, issues, data_prompt, figures=figures) + understanding = understand_task(session, task, issues, data_prompt) issues = check_understanding(understanding, task) if understanding.sub_questions: log(f" {len(understanding.sub_questions)} Teilfrage(n): " @@ -69,30 +68,6 @@ def _understand(session, task: str, label: str, data_prompt: str = "", figures=( return understanding, issues -def _checked_figure_readings(session, task: str, figures, first, label: str) -> tuple: - """Gegenprobe der Abbildungen: zweite, unabhängige Lesung; bei Abweichung entscheidet eine dritte. - - Stimmen zwei von drei Lesungen Zahl für Zahl überein, gilt diese Übertragung. Sonst bleibt die erste, - und der Befund macht das Ergebnis unzuverlässig – die Daten der Aufgabe stünden auf ungeprüftem Ablesen. - """ - log(f"[{label}] Lese {len(figures)} Abbildung(en) ein zweites Mal (Gegenprobe) ...") - first = normalize_figure_ids(first) - zweite = normalize_figure_ids(read_figures(session, task, figures).figures) - befunde = check_figure_readings(first, zweite) - if not befunde: - return first, [] - log(f"[{label}] Abbildungen uneinheitlich gelesen – dritte Lesung: " + "; ".join(i.message for i in befunde)[:300]) - for nummer, lesung in ((1, first), (2, zweite)): - for f in lesung: - log(f" Lesung {nummer}, {f.id}: " + f.transcription.replace("\n", " | ")[:300]) - dritte = normalize_figure_ids(read_figures(session, task, figures).figures) - if not check_figure_readings(first, dritte): - return first, [] - if not check_figure_readings(zweite, dritte): - return dritte, [] - return first, befunde - - def _model_attempt(session, task: str, max_revisions: int, label: str, hint: str = "", spec: OptimizationModelSpecification | None = None, understanding: TaskUnderstanding | None = None, data_prompt: str = "") -> dict: @@ -242,13 +217,9 @@ def _log_usage(session, result: PipelineResult) -> None: def run_pipeline( task: str, model: str = DEFAULT_MODEL, max_revisions: int = MAX_REVISIONS, solve_model: bool = True, solver_retries: int = SOLVER_RETRIES, max_restarts: int = MAX_RESTARTS, - session: LLMSession | None = None, data_files=(), task_dir=None, + session: LLMSession | None = None, data_files=(), ) -> PipelineResult: session = session or LLMSession(model) - # Abbildungen (docling: Base64 oder Bilddatei) gehen nur an die Verstehensstufe; ab dann zählt ihre Übertragung - task, figures = split_figures(task, task_dir) - if figures: - log(f"{len(figures)} Abbildung(en) in der Aufgabe – werden in der Verstehensstufe in Text übertragen.") profile = data_profile.profile_files(data_files) data_prompt = data_profile.to_prompt(profile) for p in profile: @@ -256,21 +227,12 @@ def run_pipeline( for step, step_model in session.overrides().items(): log(f"[{model}] Stufe '{step}' läuft mit abweichendem Modell: {step_model}") - understanding, verstehen_issues = _understand(session, task, model, data_prompt, figures) - if figures and understanding is not None: - if understanding.is_optimization_task: - understanding.figures, figur_befunde = _checked_figure_readings( - session, task, figures, understanding.figures, model) - verstehen_issues = verstehen_issues + figur_befunde - for figur in understanding.figures: - if figur.uncertain: - log(f" {figur.id} – unsicher gelesen: " + "; ".join(figur.uncertain)) - task = with_transcriptions(task, understanding.figures) + understanding, verstehen_issues = _understand(session, task, model, data_prompt) if not understanding.is_optimization_task: # Keine Entscheidung verlangt: jedes Modell wäre erfunden, jede Zahl ohne Frage dahinter grund = f"Keine Optimierungsaufgabe: {understanding.reason}" log(f"ABGELEHNT – {grund} Es wird weder modelliert noch gerechnet.") - result = PipelineResult(task=task, figures=figures, understanding=understanding, data_files=profile, rejected=True, + result = PipelineResult(task=task, understanding=understanding, data_files=profile, rejected=True, reliable=False, reliability_issues=[grund], checks=verstehen_issues) _log_usage(session, result) return result @@ -287,8 +249,7 @@ def run_pipeline( best = attempts[select_best([(a["spec"], a["issues"], a["review"]) for a in attempts])] spec, issues, review = best["spec"], best["issues"] + verstehen_issues, best["review"] - problems = _unresolved(best) + [f"Abbildung: {i.message}" for i in verstehen_issues - if i.code == "FIGURE_READINGS_DIFFER"] + problems = _unresolved(best) for issue in issues: log(f" {issue.severity.value.upper()} [{issue.code}] {issue.message}") @@ -300,7 +261,7 @@ def run_pipeline( log("Prüfung ohne Beanstandung.") result = PipelineResult( - task=task, figures=figures, understanding=understanding, data_files=profile, specification=spec, review=review, revisions=best["revisions"], + task=task, understanding=understanding, data_files=profile, specification=spec, review=review, revisions=best["revisions"], selected_revision=best["selected"], restarts=len(attempts) - 1, checks=issues, reliable=not problems, reliability_issues=problems, ) @@ -441,14 +402,11 @@ if __name__ == "__main__": elif args.von: result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions) else: - try: - result = run_pipeline( - read_task(args.task_file), model=args.model, - max_revisions=args.max_revisions, solve_model=not args.no_solve, max_restarts=args.max_restarts, - data_files=args.daten or (), task_dir=Path(args.task_file).parent if args.task_file else None, - ) - except FigureError as exc: - parser.error(str(exc)) + result = run_pipeline( + read_task(args.task_file), model=args.model, + max_revisions=args.max_revisions, solve_model=not args.no_solve, max_restarts=args.max_restarts, + data_files=args.daten or (), + ) print(result.model_dump_json(indent=2)) if args.report: write_report(result, args.report) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index f70b1c1..5a77f8c 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -26,10 +26,9 @@ RESULTS_DIR = BENCHMARK_DIR / "results" # Nach dem ersten fatalen API-Fehler (Kontingent/Guthaben/Key) keine weiteren Läufe starten quota_exhausted = threading.Event() -# Dateien, die das LLM-Verhalten steuern: die System-Prompts und die Feldbeschreibungen +# Dateien, die das LLM-Verhalten steuern: die vier System-Prompts und die Feldbeschreibungen # (Field(description=...) geht als JSON-Schema mit und wirkt wie eine Prompt-Anweisung). -PROMPT_FILES = ("understanding/Verstehen-Prompt.txt", "understanding/Abbildungs-Prompt.txt", - "modeling/Routing-und-Extraktions-Prompt.txt", "checks/Pruef-Prompt.txt", +PROMPT_FILES = ("understanding/Verstehen-Prompt.txt", "modeling/Routing-und-Extraktions-Prompt.txt", "checks/Pruef-Prompt.txt", "solving/Solver-Prompt.txt", "core/model_schema.py") diff --git a/checks/model_checks.py b/checks/model_checks.py index 083eac5..5cdf171 100644 --- a/checks/model_checks.py +++ b/checks/model_checks.py @@ -4,7 +4,6 @@ error -> löst in run_pipeline eine Korrekturrunde aus warning -> wird nur berichtet (Heuristiken mit möglichen Fehlalarmen) """ import re -from collections import Counter from typing import Iterable, List, Set from core.model_schema import ( @@ -206,56 +205,6 @@ def check_understanding(understanding, task: str) -> List[CheckIssue]: return issues -def _number_rows(text: str) -> Counter: - """Je Datenzeile das Tupel ihrer Zahlen: „Pfeil 1 → 2: 3“ und „(1,2): 3“ ergeben beide (1, 2, 3). - - Zeilen trennt der Zeilenumbruch oder ein Semikolon; Zeilen ohne Zahl zählen nicht. So ist der Vergleich - unabhängig von der Schreibweise, bemerkt aber eine Bewertung am falschen Pfeil. - """ - zeilen = Counter() - # „(1,2)“ ist ein Pfeil, keine Dezimalzahl 1,2 – ganzzahlige Paare in Klammern vorher trennen - text = re.sub(r"\((\d+)\s*,\s*(\d+)\)", r"(\1 \2)", text or "") - for zeile in re.split(r"[\n;]", text): - tupel = [] - for roh in NUMBER_PATTERN.findall(zeile): - try: - tupel.append(normalize_number(roh)) - except ValueError: - continue - if tupel: - zeilen[tuple(tupel)] += 1 - return zeilen - - -def check_figure_readings(first, second) -> List[CheckIssue]: - """Zwei unabhängige Übertragungen derselben Abbildungen vergleichen – Zahl für Zahl, ohne LLM. - - Ein Bildmodell kann eine Pfeilbewertung dem falschen Pfeil zuordnen oder eine Ziffer verlesen; eine einzige - Lesung wäre dann ungeprüft die Datengrundlage jeder weiteren Stufe. Verglichen werden die Datenzeilen als - Zahlentupel (`_number_rows`). Erster Ansatz – alle Zahlen des Freitexts – schlug im ersten echten Lauf - fälschlich an: eine Lesung zählte zusätzlich „Knoten: 1, 2, 3, 4“ auf (test_Image.md, 17.09.2026). - Eine vertauschte Pfeilrichtung (2 → 3 statt 3 → 2) fällt auf; eine in BEIDEN Lesungen gleich falsche nicht. - """ - from understanding.figures import canonical_figure_id - issues = [] - erste = {canonical_figure_id(f.id): f for f in first} - zweite = {canonical_figure_id(f.id): f for f in second} - for kennung in sorted(set(erste) | set(zweite)): - if kennung not in erste or kennung not in zweite: - issues.append(CheckIssue(severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER", - message=f"{kennung} wurde nur in einer von zwei Lesungen übertragen.")) - continue - a = _number_rows(erste[kennung].transcription) - b = _number_rows(zweite[kennung].transcription) - if a != b: - zeilen = lambda z: ", ".join("(" + " ".join(t) + ")" for t in sorted(z.elements())) or "–" - issues.append(CheckIssue( - severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER", - message=f"{kennung} uneinheitlich gelesen – Datenzeilen nur in Lesung 1: {zeilen(a - b)}; " - f"nur in Lesung 2: {zeilen(b - a)}.")) - return issues - - def _strip_scripts(text: str) -> str: """Entfernt Indizes/Exponenten: x_{i,j} -> x, C^{prod}_{p} -> C, x_1 -> x.""" text = text.replace(r"\_", "_") diff --git a/core/llm.py b/core/llm.py index b80e33c..074165d 100644 --- a/core/llm.py +++ b/core/llm.py @@ -14,14 +14,10 @@ import instructor from core.model_schema import CallRecord, LLMUsage -# Alias auf das jeweils neueste DeepSeek-Flash-Modell (September 2026: V4.1 Flash, nimmt auch Bilder an). -# Für vergleichbare Benchmarks ein festes Modell mit --models angeben. -DEFAULT_MODEL = os.environ.get("LLM_MODEL", "~deepseek/deepseek-flash-latest") +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 # (oder umgekehrt, je nach Messung). Leer = das Modell der Session (LLM_MODEL bzw. --model). STEP_MODEL_ENV = { - "Verstehen": "LLM_MODEL_UNDERSTANDING", # muss Bilder lesen können, wenn die Aufgabe Abbildungen hat - "Abbildungen": "LLM_MODEL_UNDERSTANDING", "Extraktion": "LLM_MODEL_EXTRACTION", "Prüfung": "LLM_MODEL_REVIEW", "Korrektur": "LLM_MODEL_REVISION", diff --git a/core/model_schema.py b/core/model_schema.py index 5ecf0d9..14b4b68 100644 --- a/core/model_schema.py +++ b/core/model_schema.py @@ -118,32 +118,6 @@ class IgnoredNumber(BaseModel): "'Teil der Einheit (in 1000 Stunden)', 'Seitenzahl'") -class FigureInput(BaseModel): - """Eine Abbildung der Aufgabe (eingebettet als Base64 oder als Bilddatei) – Bilddaten, kein LLM-Feld.""" - id: str - alt: str = "" - mime: str - data: str = Field(description="Bildinhalt als Base64") - source: str = Field(default="eingebettet", description="'eingebettet' oder der Pfad der Bilddatei") - - -class FigureTranscription(BaseModel): - """Eine Abbildung, vollständig in Text übertragen – danach arbeitet die Pipeline nur noch mit diesem Text.""" - id: str = Field(description="Kennung wie im Text, z. B. 'Abbildung 1'") - shows: str = Field(description="Was die Abbildung zeigt, in einem Satz (z. B. 'gerichteter Graph mit 4 Knoten')") - transcription: str = Field( - description="Nur die Daten, eine Zeile je Datenpunkt, jede Zahl genau einmal (Graph: je Pfeil 'A → B: Wert', alle Pfeile; " - "Tabelle: 'Zeile / Spalte: Wert'). Keine Knotenaufzählung, keine Lagebeschreibung – das gehört nach shows.") - uncertain: List[str] = Field( - default_factory=list, - description="Stellen, die nicht sicher lesbar sind (verdeckt, unscharf, mehrdeutig); leer = alles sicher gelesen") - - -class FigureReading(BaseModel): - """Unabhängige zweite Übertragung der Abbildungen – Gegenprobe zur Verstehensstufe.""" - figures: List[FigureTranscription] - - class TaskUnderstanding(BaseModel): """Was in der Aufgabe steht – vor jeder Modellierung: Fragen, Zahlen, Machbarkeit.""" is_optimization_task: bool = Field( @@ -155,9 +129,6 @@ class TaskUnderstanding(BaseModel): facts: List[Fact] = Field(description="Alle Zahlenwerte des Textes, die Eingabegrößen des Modells sind") ignored_numbers: List[IgnoredNumber] = Field( default_factory=list, description="Zahlen im Text, die keine Eingabegrößen sind, mit Begründung") - figures: List[FigureTranscription] = Field( - default_factory=list, - description="Nur wenn der Aufgabe Abbildungen beiliegen: je Abbildung eine vollständige Übertragung; sonst leer") class ObjectiveDefinition(BaseModel): @@ -459,8 +430,6 @@ class LLMUsage(BaseModel): class PipelineResult(BaseModel): task: str = "" - figures: List[FigureInput] = Field( - default_factory=list, description="Abbildungen der Aufgabe; ihre Übertragung steht im Aufgabentext (task)") data_files: List[DataFileProfile] = Field( default_factory=list, description="Profile der beigelegten Datendateien (--daten)") understanding: Optional[TaskUnderstanding] = Field( diff --git a/reporting/report.py b/reporting/report.py index 912d577..507f136 100644 --- a/reporting/report.py +++ b/reporting/report.py @@ -301,28 +301,6 @@ def _fmt_number(value) -> str: return str(value) -def _figure_blocks(result: PipelineResult) -> list: - """Abbildungen der Aufgabe: Original neben der Übertragung, mit der die Pipeline gerechnet hat.""" - if not result.figures: - return [] - from understanding.figures import canonical_figure_id - uebertragen = {canonical_figure_id(f.id): f for f in (result.understanding.figures if result.understanding else [])} - doc = [("h3", "Abbildungen"), - ("p", "Die Pipeline rechnet nicht mit dem Bild, sondern mit seiner Übertragung in Text. " - "Bitte Original und Übertragung vergleichen – ein verlesener Wert ändert das ganze Ergebnis.")] - for figur in result.figures: - doc.append(("h4", figur.id + (f" – {figur.alt}" if figur.alt and figur.alt.lower() != "image" else ""))) - doc.append(("img", f"data:{figur.mime};base64,{figur.data}", figur.id)) - t = uebertragen.get(figur.id) - if t is None: - doc.append(("warn", f"{figur.id} wurde nicht übertragen – ihre Inhalte fehlen im Modell.")) - continue - doc += [("p", f"Übertragung: {t.shows}"), ("pre", t.transcription.strip())] - if t.uncertain: - doc.append(("warn", f"{figur.id} – unsicher gelesen: " + "; ".join(t.uncertain))) - return doc - - def _rejected_document(result: PipelineResult) -> list: """Abgelehnte Aufgabe: Grund und Aufgabentext – kein Modell, keine Zahlen.""" grund = result.understanding.reason if result.understanding else "; ".join(result.reliability_issues) @@ -333,7 +311,7 @@ def _rejected_document(result: PipelineResult) -> list: doc.append(("list", [f"Erstellt: {datetime.now():%d.%m.%Y %H:%M}", f"Modell: {result.usage.model}", f"LLM: {result.usage.calls} Aufrufe{cost}"])) if result.task.strip(): - doc += [("h2", "Aufgabe"), ("pre", result.task.strip())] + _figure_blocks(result) + doc += [("h2", "Aufgabe"), ("pre", result.task.strip())] return doc @@ -364,7 +342,7 @@ def build_document(result: PipelineResult, model: str = "") -> list: doc += _result_section(result) if result.task.strip(): - doc += [("h2", "Aufgabe"), ("pre", result.task.strip())] + _figure_blocks(result) + doc += [("h2", "Aufgabe"), ("pre", result.task.strip())] if result.data_files: doc += [("h2", "Beigelegte Daten")] @@ -490,8 +468,6 @@ def to_markdown(doc: list) -> str: out.append(f"**{_md_inline(block[1])}**") elif kind == "svg": pass # Diagramme nur im HTML-Report - elif kind == "img": - out.append(f"![{block[2]}]({block[1]})") elif kind == "list": out.append("\n".join(f"- {_md_inline(item)}" for item in block[1])) elif kind == "math": @@ -615,9 +591,6 @@ def to_html(doc: list) -> str: out.append(f'
{_html_inline(block[1])}
') elif kind == "svg": out.append(f'