fix: Nebenpfade behalten Kontext – Gegenprobe, --von, --resolve, UNDERSTAND_RETRIES=-1
- UNDERSTAND_RETRIES=-1 (A/B-Arm der Verstehensstufe) stürzte seit dem Ablehnungstor ab - Unlösbar-Gegenprobe bekommt Verständnis, Datenprofil und Datendateien; ihre Lösung durchläuft dieselbe Abnahme (_assess_run) - --von pruefung behält Verständnis, Abbildungen und Datenprofil - --resolve überschreibt frühere Befunde nicht mehr (nur Zulässigkeit neu) - --daten wirkt auch mit --resolve und --von - Prüfung und Korrektur sehen das Datenprofil - Reparaturmeldung nennt alle Befundarten zugleich - Test der Balkenhöhe prüft die Höhe tatsächlich; unbenutzte Imports entfernt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8bd3c2e7e1
commit
3c4d2fb92c
5 changed files with 182 additions and 59 deletions
|
|
@ -1,4 +1,5 @@
|
|||
"""Inline-SVG-Diagramme: gültiges SVG, korrekte Geometrie, Einbindung in den Report."""
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
|
@ -33,8 +34,12 @@ def test_bars_are_proportional_and_capped_in_thickness():
|
|||
widths = [float(d.split("H")[1].split(" ")[0].split(",")[0]) for d in
|
||||
[p.get("d") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]]
|
||||
assert widths[0] > widths[1]
|
||||
heights = {float(p.get("d").split("V")[1].split(" ")[0]) for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")}
|
||||
assert charts.BAR_HEIGHT <= 24
|
||||
# Balkenhöhe = unterer Rand (letztes „x,y“ vor „H… Z“) minus oberer Rand (Startpunkt „M x,y“)
|
||||
heights = set()
|
||||
for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path"):
|
||||
ys = [float(y) for y in re.findall(r"[\d.]+,([\d.]+)", p.get("d"))]
|
||||
heights.add(max(ys) - min(ys))
|
||||
assert heights == {float(charts.BAR_HEIGHT)} and charts.BAR_HEIGHT <= 24
|
||||
|
||||
|
||||
def test_negative_values_use_the_other_colour_role():
|
||||
|
|
|
|||
|
|
@ -457,7 +457,6 @@ if __name__ == "__main__":
|
|||
|
||||
|
||||
def test_report_shows_data_profile_and_scenarios(tmp_path):
|
||||
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")
|
||||
|
|
@ -609,3 +608,84 @@ def test_persistent_scenario_without_result_is_unreliable():
|
|||
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
|
||||
assert not result.reliable
|
||||
assert any(p.startswith("Szenario ohne Ergebnis") for p in result.reliability_issues)
|
||||
|
||||
|
||||
# --- Nebenpfade: Verstehensstufe aus, Gegenprobe und --von mit vollem Kontext ---
|
||||
|
||||
def test_pipeline_runs_with_understanding_stage_disabled(monkeypatch):
|
||||
# UNDERSTAND_RETRIES=-1 war der A/B-Arm der Verstehensstufe; das Ablehnungstor griff dann auf None zu
|
||||
import TextToModel
|
||||
monkeypatch.setattr(TextToModel, "UNDERSTAND_RETRIES", -1)
|
||||
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)])
|
||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||
assert result.understanding is None and not result.rejected
|
||||
assert session.steps[0] == "Extraktion"
|
||||
assert result.solver_run.status == SolverStatus.OPTIMAL
|
||||
|
||||
|
||||
def test_infeasible_recheck_keeps_understanding_and_data_files(tmp_path, monkeypatch):
|
||||
# Ohne Datendateien fand das Programm der Gegenprobe seine CSV nicht, ohne Verständnis fehlten die Antworten
|
||||
import TextToModel
|
||||
aufrufe = []
|
||||
echtes_solve = TextToModel.solve
|
||||
|
||||
def protokoll(*args, **kwargs):
|
||||
aufrufe.append(kwargs)
|
||||
return echtes_solve(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(TextToModel, "solve", protokoll)
|
||||
csv = tmp_path / "werte.csv"
|
||||
csv.write_text("a,b\n1,2\n", encoding="utf-8")
|
||||
session = FakeSession(specs=[GOOD, GOOD], reviews=[OK, OK],
|
||||
codes=[SolverCode(code=INFEASIBLE_CODE), SolverCode(code=STRUCTURED_CODE)])
|
||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, data_files=[str(csv)])
|
||||
assert result.restarts == 1 and len(aufrufe) == 2
|
||||
gegenprobe = aufrufe[1]
|
||||
assert gegenprobe["understanding"] is not None
|
||||
assert list(gegenprobe["data_files"]) == [str(csv)] and "werte.csv" in gegenprobe["data_prompt"]
|
||||
assert "werte.csv" in session.tasks[1] # auch die neue Extraktion sieht das Datenprofil
|
||||
|
||||
|
||||
def test_continue_from_review_keeps_understanding(tmp_path):
|
||||
from TextToModel import continue_pipeline
|
||||
path = stored_result(tmp_path)
|
||||
session = FakeSession([], [OK], [SolverCode(code=STRUCTURED_CODE)])
|
||||
result = continue_pipeline(str(path), "pruefung", session=session)
|
||||
assert result.understanding is not None
|
||||
assert [q.id for q in result.understanding.sub_questions] == ["1"]
|
||||
assert "ZU BEANTWORTENDE TEILFRAGEN" in session.letzter_prompt # der Solver bekommt die Teilfragen
|
||||
|
||||
|
||||
def test_review_and_revision_see_the_data_profile():
|
||||
from checks.review import review_assumptions
|
||||
from modeling.extraction import revise_model
|
||||
session = FakeSession([GOOD], [OK], [])
|
||||
review_assumptions(session, "Aufgabe", GOOD, "BEIGELEGTE DATEN: kurse.csv")
|
||||
assert "kurse.csv" in session.letzter_prompt
|
||||
revise_model(session, "Aufgabe", GOOD, OK, data_prompt="BEIGELEGTE DATEN: kurse.csv")
|
||||
assert "kurse.csv" in session.letzter_prompt
|
||||
|
||||
|
||||
def test_repair_message_names_every_kind_of_finding():
|
||||
# Vorher erfuhr das LLM nur von der ersten Befundart und verbrauchte für die zweite einen eigenen Versuch
|
||||
from solving.solver_stage import solve
|
||||
code = '''
|
||||
import numpy as np
|
||||
|
||||
|
||||
def main():
|
||||
"""Verletzt die Restriktion und erzeugt Zahlen per Zufall, ohne sie auszuweisen."""
|
||||
np.random.seed(0)
|
||||
report("optimal", 9.0, {"x": 1.0},
|
||||
constraints=[{"name": "Bedarf", "lhs": 1.0, "sense": ">=", "rhs": 4.0}],
|
||||
variables=[{"name": "x", "value": 1.0, "var_type": "continuous", "lb": 0, "ub": None}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
session = FakeSession([], [], [SolverCode(code=code), SolverCode(code=code)])
|
||||
solve(session, "Aufgabe", GOOD, retries=1)
|
||||
assert session.steps == ["Solver-Code", "Solver-Reparatur"]
|
||||
assert "hält das Modell nicht ein" in session.letzter_prompt
|
||||
assert "Eingabedaten nicht ausgewiesen" in session.letzter_prompt
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ def test_infeasible_data_is_reported_not_crashing():
|
|||
# --- CLI-Modus: gespeichertes Ergebnis neu rechnen ---
|
||||
|
||||
def gespeichertes_ergebnis(tmp_path):
|
||||
import json
|
||||
from core.model_schema import PipelineResult, SolverCode
|
||||
from test_model_checks import make_spec
|
||||
from test_review import make_review
|
||||
|
|
@ -96,3 +95,17 @@ def test_resolve_keeps_specification_and_checks_feasibility(tmp_path):
|
|||
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)))
|
||||
assert neu.specification.problem_class.value.startswith("Linear")
|
||||
assert neu.solver_run.feasibility == []
|
||||
|
||||
|
||||
def test_resolve_keeps_earlier_reliability_issues(tmp_path):
|
||||
# Neurechnen prüft nur die Zulässigkeit neu – eine falsche Zielgröße bleibt ein Befund
|
||||
from core.model_schema import PipelineResult
|
||||
from TextToModel import resolve_result
|
||||
pfad = gespeichertes_ergebnis(tmp_path)
|
||||
alt = PipelineResult.model_validate_json(pfad.read_text(encoding="utf-8"))
|
||||
alt.reliable = False
|
||||
alt.reliability_issues = ["Zielfunktion passt nicht zur Frage", "Zulässigkeitsprüfung: alter Befund"]
|
||||
pfad.write_text(alt.model_dump_json(), encoding="utf-8")
|
||||
neu = resolve_result(str(pfad))
|
||||
assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt
|
||||
assert not neu.reliable
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ def test_normal_decomposition_has_no_magnitude_warning():
|
|||
|
||||
|
||||
def test_report_shows_multistart_stability():
|
||||
from core.model_schema import Multistart, SolverRun
|
||||
from core.model_schema import Multistart
|
||||
from reporting.report import build_document, to_markdown
|
||||
from test_pipeline import FakeSession
|
||||
from TextToModel import run_pipeline
|
||||
|
|
|
|||
|
|
@ -482,7 +482,6 @@ def test_path_must_use_declared_arcs():
|
|||
|
||||
|
||||
def test_path_length_must_match_objective():
|
||||
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"))
|
||||
|
|
@ -757,7 +756,6 @@ def test_ordinary_programs_are_not_suspected():
|
|||
|
||||
def test_scenario_failing_without_error_text_gets_a_reason():
|
||||
# beobachtet (Portfolio Nr. 2): Szenario ohne Lösung im Zeitlimit, Warnung endete mit leerem Grund
|
||||
from core.model_schema import Scenario
|
||||
from solving.solver_stage import verify_scenarios
|
||||
code = SZENARIO_CODE.replace(' basis = loesen(DATA)\n',
|
||||
' basis = loesen(DATA)\n if DATA["preis"] > 15:\n'
|
||||
|
|
|
|||
Loading…
Reference in a new issue