feat: Aufgaben mit Abbildungen (docling-Markdown), mit Gegenprobe beim Ablesen
Eingebettete Base64-Bilder und Bilddateien werden aus dem Aufgabentext gelöst und nur der Verstehensstufe als Bild übergeben; sie überträgt jede Abbildung in Text. Eine zweite, unabhängige Lesung wird Zahl für Zahl verglichen (bei Abweichung entscheidet eine dritte, sonst reliable=False). Alle weiteren Stufen rechnen mit der Übertragung, der Report zeigt Original und Übertragung nebeneinander. LLM_MODEL_UNDERSTANDING wählt das Bildmodell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
915c79c1c8
commit
b07dd16143
1 changed files with 484 additions and 36 deletions
|
|
@ -5,7 +5,7 @@ import sys
|
|||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from checks.model_checks import check_spec, check_understanding, has_errors
|
||||
from checks.model_checks import check_figure_readings, 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,7 +22,8 @@ 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
|
||||
from understanding.figures import FigureError, split_figures, with_transcriptions
|
||||
from understanding.understand import read_figures, understand_task
|
||||
|
||||
MAX_REVISIONS = int(os.environ.get("MAX_REVISIONS", "2"))
|
||||
SOLVER_RETRIES = int(os.environ.get("SOLVER_RETRIES", "2"))
|
||||
|
|
@ -47,19 +48,19 @@ def log(msg: str) -> None:
|
|||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
def _understand(session, task: str, label: str, data_prompt: str = "") -> tuple:
|
||||
def _understand(session, task: str, label: str, data_prompt: str = "", figures=()) -> tuple:
|
||||
"""Verstehensstufe samt Prüfung; ein Nachfassen, wenn Zahlen oder Teilfragen fehlen."""
|
||||
if UNDERSTAND_RETRIES < 0:
|
||||
return None, []
|
||||
log(f"[{label}] Lese die Aufgabe (Teilfragen, Zahlen) ...")
|
||||
understanding = understand_task(session, task, data_prompt=data_prompt)
|
||||
log(f"[{label}] Lese die Aufgabe (Teilfragen, Zahlen" + (", Abbildungen" if figures else "") + ") ...")
|
||||
understanding = understand_task(session, task, data_prompt=data_prompt, figures=figures)
|
||||
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)
|
||||
understanding = understand_task(session, task, issues, data_prompt, figures=figures)
|
||||
issues = check_understanding(understanding, task)
|
||||
if understanding.sub_questions:
|
||||
log(f" {len(understanding.sub_questions)} Teilfrage(n): "
|
||||
|
|
@ -68,6 +69,26 @@ def _understand(session, task: str, label: str, data_prompt: str = "") -> tuple:
|
|||
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) ...")
|
||||
zweite = 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])
|
||||
dritte = 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:
|
||||
|
|
@ -217,9 +238,13 @@ 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=(),
|
||||
session: LLMSession | None = None, data_files=(), task_dir=None,
|
||||
) -> 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:
|
||||
|
|
@ -227,12 +252,21 @@ 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)
|
||||
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)
|
||||
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, understanding=understanding, data_files=profile, rejected=True,
|
||||
result = PipelineResult(task=task, figures=figures, understanding=understanding, data_files=profile, rejected=True,
|
||||
reliable=False, reliability_issues=[grund], checks=verstehen_issues)
|
||||
_log_usage(session, result)
|
||||
return result
|
||||
|
|
@ -249,7 +283,8 @@ 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)
|
||||
problems = _unresolved(best) + [f"Abbildung: {i.message}" for i in verstehen_issues
|
||||
if i.code == "FIGURE_READINGS_DIFFER"]
|
||||
|
||||
for issue in issues:
|
||||
log(f" {issue.severity.value.upper()} [{issue.code}] {issue.message}")
|
||||
|
|
@ -261,7 +296,7 @@ def run_pipeline(
|
|||
log("Prüfung ohne Beanstandung.")
|
||||
|
||||
result = PipelineResult(
|
||||
task=task, understanding=understanding, data_files=profile, specification=spec, review=review, revisions=best["revisions"],
|
||||
task=task, figures=figures, 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,
|
||||
)
|
||||
|
|
@ -402,11 +437,14 @@ if __name__ == "__main__":
|
|||
elif args.von:
|
||||
result = continue_pipeline(args.von[1], args.von[0], model=args.model, max_revisions=args.max_revisions)
|
||||
else:
|
||||
result = run_pipeline(
|
||||
read_task(args.task_file), model=args.model,
|
||||
max_revisions=args.max_revisions, solve_model=not args.no_solve, max_restarts=args.max_restarts,
|
||||
data_files=args.daten or (),
|
||||
)
|
||||
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))
|
||||
print(result.model_dump_json(indent=2))
|
||||
if args.report:
|
||||
write_report(result, args.report)
|
||||
|
|
|
|||
Loading…
Reference in a new issue