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 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from core.model_schema import AssumptionReview, CheckIssue, CheckSeverity
|
|
|
|
|
|
from checks.review import select_best
|
2026-09-17 11:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_review(matches=True, verdict="justified", missing=(), units_consistent=True):
|
|
|
|
|
|
return AssumptionReview.model_validate({
|
|
|
|
|
|
"objective_check": {
|
|
|
|
|
|
"task_evidence": "Bestimme die Aufteilung mit der geringsten Volatilität",
|
|
|
|
|
|
"asked_quantity": "Standardabweichung der Portfoliorendite",
|
|
|
|
|
|
"modeled_quantity": "Standardabweichung" if matches else "Varianz",
|
|
|
|
|
|
"matches": matches,
|
|
|
|
|
|
"units_consistent": units_consistent,
|
|
|
|
|
|
"unit_explanation": "Rendite €, Risiko €, Kosten €" if units_consistent else "Rendite €, Risiko dimensionslos, Kosten €",
|
|
|
|
|
|
"explanation": "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
"checks": [{"unclear_point": "-", "verdict": verdict, "task_evidence": "-", "explanation": "-"}],
|
|
|
|
|
|
"missing_assumptions": list(missing),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_clean_review_needs_no_revision():
|
|
|
|
|
|
assert not make_review().needs_revision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_objective_mismatch_triggers_revision():
|
|
|
|
|
|
assert make_review(matches=False).needs_revision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_faulty_assumption_triggers_revision():
|
|
|
|
|
|
assert make_review(verdict="stricter_than_task").needs_revision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_assumption_triggers_revision():
|
|
|
|
|
|
missing = [{"unclear_point": "-", "assumption": "-", "alternative": "-", "impact": "-"}]
|
|
|
|
|
|
assert make_review(missing=missing).needs_revision
|
|
|
|
|
|
|
|
|
|
|
|
|
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 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
# --- Auswahl der besten Korrekturrunde (checks.review.select_best) ---
|
2026-09-17 11:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
ERROR = CheckIssue(severity=CheckSeverity.ERROR, code="CPSAT_WITH_CONTINUOUS_VARS", message="-")
|
|
|
|
|
|
WARNING = CheckIssue(severity=CheckSeverity.WARNING, code="UNDECLARED_SYMBOL", message="-")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_later_hallucinated_round_is_discarded():
|
|
|
|
|
|
# beobachtet in portfolio_qp: Runde 1 sauber, Runde 2 beschreibt ein Fremdproblem mit Check-Error
|
|
|
|
|
|
candidates = [
|
|
|
|
|
|
(None, [], make_review(matches=False)),
|
|
|
|
|
|
(None, [WARNING], make_review()),
|
|
|
|
|
|
(None, [ERROR, WARNING], make_review(matches=False)),
|
|
|
|
|
|
]
|
|
|
|
|
|
assert select_best(candidates) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_clean_later_round_wins_tie():
|
|
|
|
|
|
candidates = [(None, [], make_review()), (None, [WARNING], make_review())]
|
|
|
|
|
|
assert select_best(candidates) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_fewer_errors_preferred_when_all_have_errors():
|
|
|
|
|
|
candidates = [(None, [ERROR], make_review(matches=False)), (None, [ERROR, ERROR], make_review(matches=False))]
|
|
|
|
|
|
assert select_best(candidates) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mixed_units_trigger_revision():
|
|
|
|
|
|
# beobachtet: Rendite/Kosten in Euro, Risiko als dimensionslose Summe w_i^2 sigma_i^2 (Faktor 100.000 zu klein)
|
|
|
|
|
|
assert make_review(units_consistent=False).needs_revision
|
2026-09-17 16:28:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Grund jeder Korrekturrunde im Log (vorher nur „Probleme gefunden“) ---
|
|
|
|
|
|
|
|
|
|
|
|
def test_revision_reasons_name_every_trigger():
|
|
|
|
|
|
from checks.review import needs_work, revision_reasons
|
|
|
|
|
|
from core.model_schema import CheckIssue, CheckSeverity
|
|
|
|
|
|
fehlend = {"unclear_point": "Einkaufspreis nicht beziffert", "assumption": "-", "alternative": "-", "impact": "-"}
|
|
|
|
|
|
review = make_review(matches=False, verdict="weaker_than_task", missing=[fehlend])
|
|
|
|
|
|
issues = [CheckIssue(severity=CheckSeverity.ERROR, code="MIXED_OBJECTIVE_UNITS", message="Risiko dimensionslos"),
|
|
|
|
|
|
CheckIssue(severity=CheckSeverity.WARNING, code="MISSING_DATA", message="kein Wert für p")]
|
|
|
|
|
|
gruende = revision_reasons(review, issues)
|
|
|
|
|
|
assert needs_work(review, issues)
|
|
|
|
|
|
assert any(g.startswith("Zielgröße passt nicht") for g in gruende)
|
|
|
|
|
|
assert any("weaker_than_task" in g for g in gruende)
|
|
|
|
|
|
assert any("Einkaufspreis nicht beziffert" in g for g in gruende)
|
|
|
|
|
|
assert any("MIXED_OBJECTIVE_UNITS" in g for g in gruende)
|
|
|
|
|
|
assert not any("MISSING_DATA" in g for g in gruende) # löst keine Runde aus, gehört nicht in den Grund
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_clean_state_has_no_revision_reasons():
|
|
|
|
|
|
from checks.review import revision_reasons
|
|
|
|
|
|
assert revision_reasons(make_review(), []) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_review_prompt_allows_both_forms_of_decision_independent_costs():
|
|
|
|
|
|
# wagner_whitin_dijkstra: Einkaufspreis ohne Zahl – je 1–2 Korrekturrunden, weil Regel 4 beide Formen beanstanden konnte
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
prompt = (Path(__file__).parent.parent / "checks" / "Pruef-Prompt.txt").read_text(encoding="utf-8")
|
|
|
|
|
|
assert "entscheidungsunabhängige Terme" in prompt and "Verlange keine Umstellung" in prompt
|