"""Aufgaben mit Abbildungen (docling-Markdown): abtrennen, als Bild an die Verstehensstufe, doppelt ablesen.""" import base64 import pytest from checks.model_checks import check_figure_readings, numbers_in from core.model_schema import (FigureReading, FigureTranscription, SolverCode, SubQuestion, TaskUnderstanding) from reporting.report import build_document, to_html, to_markdown from TextToModel import run_pipeline from understanding.figures import MAX_FIGURES, FigureError, split_figures, with_transcriptions from understanding.understand import understand_task # 1×1-PNG – Inhalt egal, es geht um den Weg durch die Pipeline PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" AUFGABE = f"Gesucht sind die minimalen Fahrtkosten von Knoten 1 nach Knoten 4.\n\n![Image](data:image/png;base64,{PNG})\n" # Graph aus test_Image.md GRAPH = FigureTranscription(id="Abbildung 1", shows="gerichteter Graph mit 4 Knoten", transcription="Pfeil 1 → 2: 3\nPfeil 1 → 3: 1\nPfeil 3 → 2: 1\nPfeil 2 → 4: 3\nPfeil 3 → 4: 5") VERLESEN = GRAPH.model_copy(update={"transcription": GRAPH.transcription.replace("2 → 4: 3", "2 → 4: 5")}) # --- Abtrennen ohne LLM --- def test_embedded_image_is_replaced_by_a_placeholder(): text, figuren = split_figures(AUFGABE) assert "[Abbildung 1]" in text and "base64" not in text assert len(figuren) == 1 and figuren[0].mime == "image/png" and figuren[0].data == PNG assert numbers_in(text) == {"1", "4"} # keine Zahlen aus dem Base64-Zeichensalat def test_referenced_image_file_is_loaded_relative_to_the_task(tmp_path): (tmp_path / "bilder").mkdir() (tmp_path / "bilder" / "graph.png").write_bytes(base64.b64decode(PNG)) text, figuren = split_figures("Siehe ![Netz](bilder/graph.png) und ![](fehlt.png) und ![Web](https://x.de/a.png)", tmp_path) assert "[Abbildung 1: Netz]" in text assert "![](fehlt.png)" in text and "https://x.de/a.png" in text # nicht vorhanden bzw. keine Datei: bleibt assert len(figuren) == 1 and figuren[0].data == PNG and figuren[0].source.endswith("graph.png") def test_limits_and_broken_base64_are_errors_not_silent_cuts(): zu_viele = "\n".join(f"![Image](data:image/png;base64,{PNG})" for _ in range(MAX_FIGURES + 1)) with pytest.raises(FigureError, match="höchstens"): split_figures(zu_viele) with pytest.raises(FigureError, match="Base64"): split_figures("![Image](data:image/png;base64,abc)") def test_task_without_images_is_unchanged(): assert split_figures("Minimiere 3x + 2y.") == ("Minimiere 3x + 2y.", []) def test_transcription_is_appended_with_uncertain_parts(): unsicher = GRAPH.model_copy(update={"uncertain": ["Richtung des Pfeils zwischen 2 und 3"]}) text = with_transcriptions("Aufgabe [Abbildung 1]", [unsicher]) assert "Pfeil 3 → 4: 5" in text and "Unsicher gelesen: Richtung des Pfeils" in text # --- Nachricht an das Bildmodell --- class Aufzeichner: def __init__(self, antwort): self.antwort, self.nachrichten = antwort, [] def create(self, response_model, messages, step="unbenannt"): self.nachrichten.append((step, messages)); return self.antwort def test_understanding_sends_images_as_image_parts(): text, figuren = split_figures(AUFGABE) session = Aufzeichner("ok") understand_task(session, text, figures=figuren) inhalt = session.nachrichten[0][1][-1]["content"] assert isinstance(inhalt, list) and inhalt[0]["type"] == "text" and "1 ABBILDUNG" in inhalt[0]["text"] assert inhalt[-1] == {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{PNG}"}} def test_understanding_without_images_stays_plain_text(): session = Aufzeichner("ok") understand_task(session, "Minimiere 3x.") assert isinstance(session.nachrichten[0][1][-1]["content"], str) # --- Doppelt ablesen --- def test_identical_readings_have_no_finding(): assert check_figure_readings([GRAPH], [GRAPH.model_copy()]) == [] def test_misread_value_is_found_with_both_numbers(): befunde = check_figure_readings([GRAPH], [VERLESEN]) assert len(befunde) == 1 and befunde[0].code == "FIGURE_READINGS_DIFFER" assert "nur in Lesung 1: 3" in befunde[0].message and "nur in Lesung 2: 5" in befunde[0].message def test_figure_missing_in_one_reading_is_found(): assert "nur in einer" in check_figure_readings([GRAPH], [])[0].message # --- Durch die ganze Pipeline (ohne LLM) --- def _verstaendnis(figur): return TaskUnderstanding(is_optimization_task=True, reason="-", facts=[], ignored_numbers=[], sub_questions=[SubQuestion(id="1", quote="-", kind="rechnen", needs_solver=True)], figures=[figur]) def _lauf(erste, lesungen): from test_pipeline import GOOD, OK, FakeSession from test_script_export import STRUCTURED_CODE session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)], understandings=[_verstaendnis(erste)], readings=[FigureReading(figures=[f]) for f in lesungen]) return run_pipeline(AUFGABE, session=session, max_revisions=0, max_restarts=0), session def test_only_the_understanding_sees_the_image_later_stages_get_the_transcription(): result, session = _lauf(GRAPH, [GRAPH]) bilder = [schritt for schritt, inhalt in session.contents if isinstance(inhalt, list)] assert set(bilder) == {"Verstehen", "Abbildungen"} spaeter = [inhalt for schritt, inhalt in session.contents if schritt not in ("Verstehen", "Abbildungen")] assert spaeter and all("data:image" not in str(i) for i in spaeter) assert any("Pfeil 3 → 4: 5" in str(i) for i in spaeter) assert result.reliable and len(result.figures) == 1 and "Pfeil 2 → 4: 3" in result.task def test_two_of_three_agreeing_readings_win(): result, _ = _lauf(VERLESEN, [GRAPH, GRAPH]) # erste Lesung verlesen, zweite und dritte stimmen überein assert result.reliable and "Pfeil 2 → 4: 3" in result.task def test_persistently_differing_readings_make_the_result_unreliable(): dritte = GRAPH.model_copy(update={"transcription": GRAPH.transcription.replace("1 → 3: 1", "1 → 3: 7")}) result, _ = _lauf(GRAPH, [VERLESEN, dritte]) assert not result.reliable assert any(p.startswith("Abbildung:") and "uneinheitlich" in p for p in result.reliability_issues) def test_report_shows_original_and_transcription(): result, _ = _lauf(GRAPH.model_copy(update={"uncertain": ["Pfeil 3 → 2 schwer lesbar"]}), [GRAPH]) html = to_html(build_document(result)) assert f'