fix: Umlaute im Code erlaubt, Lesertexte still berichtigt
Ursache der Umschreibungen ist kein Encoding-Problem: das LLM schreibt im erzeugten Python-Code ASCII (Portfolio Nr. 2: Code-Strings 0 echte gegen 60 umschriebene Umlaute, Spezifikation 128 echte). Die Prompt-Regel wurde ignoriert, die Warnung stand in fast jedem Lauf. - Solver-Prompt: Umschreibungen in Bezeichnern, DATA-Schluesseln, Restriktionsnamen und Kommentaren erlaubt; echte Umlaute nur in Lesertexten, Regel direkt bei den report()-Argumenten. - check_text_quality meldet keine Umlaute mehr (Fachbegriffe bleiben). - umlaut_map baut je Lauf ein Woerterbuch aus Aufgabe und Spezifikation; repair_texts wendet es vor UMLAUT_STEMS an. ASCII-Formen aus dem Aufgabentext (Namen) bleiben, Restriktions-/Variablennamen unberuehrt. An vier gespeicherten Laeufen: alle Umschreibungen in Lesertexten berichtigt, 11 Ersetzungen, keine falsch. Prompt-Kennung 694c3eb3d59a. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8697400dc2
commit
0d9b5d718f
3 changed files with 88 additions and 40 deletions
|
|
@ -59,6 +59,7 @@ Geldbeträge und Rundung:
|
|||
|
||||
Ergebnisausgabe:
|
||||
- Die Funktion `report(status, objective_value, solution, tables, headline, constraints, variables, sensitivity, relaxation_bound, multistart, graph, answers, scenarios)` ist bereits vordefiniert (sie gibt das Ergebnis lesbar und maschinenlesbar aus). Definiere sie NICHT selbst und importiere nichts dafür.
|
||||
- Schreibweise: Bezeichner, `DATA`-Schlüssel, Restriktionsnamen und Kommentare dürfen Umlaute umschreiben (`Kapazitaet_M1`) – das wird nicht beanstandet. **Lesertexte** dagegen erscheinen im Report und stehen in echtem Deutsch mit ä, ö, ü, ß: `headline`, `answers`, Tabellen (`title`, `columns`, Textzellen, `note`), `scenarios` (`name`, `description`) und `graph` (`node_meaning`, `arc_meaning`). Die Datei ist UTF-8. [→ repair_texts]
|
||||
- Rufe `report(...)` genau einmal auf, innerhalb von `main()`:
|
||||
- status: "optimal", "feasible", "infeasible", "unbounded" oder "error"
|
||||
- objective_value: Wert der Zielfunktion der Spezifikation (im ursprünglichen Sinn/Maßstab) als float, oder None
|
||||
|
|
@ -122,5 +123,4 @@ Ergebnisausgabe:
|
|||
3. "Bindende Restriktionen": welche Grenzen die Lösung ausreizt, mit Spalten ["Restriktion", "Wert", "Grenze", "bindend"] und Werten aus der Lösung (z. B. "Höchstgewicht A02" | "30,00 %" | "30,00 %" | "ja"). Nur Tatsachen aus der Lösung, keine Bewertung; ohne objective_column.
|
||||
Zahlen in der objective_column immer im Maßstab von objective_value (nicht in Prozent umrechnen); Datenspalten dagegen in lesbaren Einheiten als Text ("1,63 %", "0,74 %", "36 h").
|
||||
- Weitere print-Ausgaben sind erlaubt, aber nicht nötig.
|
||||
- Alle Texte auf Deutsch mit echten Umlauten (ä, ö, ü, ß), nie 'ae'/'oe'/'ue'; die Dateien sind UTF-8. [→ check_text_quality]
|
||||
- Liefere im Feld code nur den reinen Python-Code ohne Markdown-Codeblock.
|
||||
|
|
|
|||
|
|
@ -586,38 +586,61 @@ UMLAUT_STEMS = {
|
|||
"beruecksicht": "berücksicht", "erfuell": "erfüll", "gemaess": "gemäß", "taeglich": "täglich",
|
||||
"jaehrl": "jährl", "kuerzest": "kürzest", "laenge": "länge", "staerk": "stärk", "hoeh": "höh",
|
||||
"fruehest": "frühest", "spaetest": "spätest", "baell": "bäll", "heisst": "heißt",
|
||||
"betraeg": "beträg", "ausfuehr": "ausführ", "abzueg": "abzüg", "enthaelt": "enthält", "tatsaechl": "tatsächl",
|
||||
}
|
||||
TRANSLITERATIONS = re.compile(r"\b\w*(?:" + "|".join(UMLAUT_STEMS) + r")\w*\b", re.IGNORECASE)
|
||||
_STEM_PATTERN = re.compile("|".join(UMLAUT_STEMS), re.IGNORECASE)
|
||||
|
||||
|
||||
def fix_transliterations(text: str) -> str:
|
||||
"""'guenstigste' -> 'günstigste'. Die Warnung bleibt; der Report soll trotzdem lesbar sein.
|
||||
_UMLAUT_WORD = re.compile(r"\b\w*[äöüÄÖÜß]\w*\b")
|
||||
_ASCII = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", "ß": "ss"})
|
||||
|
||||
Der Solver-Prompt verlangt echte Umlaute, das LLM hält sich nicht daran (in fast jedem Lauf
|
||||
mindestens einmal beobachtet). Eine Warnung, die nichts bewirkt, hilft dem Leser nicht.
|
||||
|
||||
def umlaut_map(aufgabe: str, *weitere: str) -> dict:
|
||||
"""Wörterbuch aus dem Lauf selbst: Umschreibung -> Original, z. B. 'volatilitaet' -> 'volatilität'.
|
||||
|
||||
Das LLM schreibt im erzeugten Python-Code ASCII (beobachtet: Code-Strings 0 echte gegen 60
|
||||
umschriebene Umlaute, dieselbe Spezifikation 128 echte). Eine feste Stammliste läuft jeder neuen
|
||||
Aufgabe hinterher; die Wörter, die im Report vorkommen, stehen aber mit Umlaut in Aufgabe und
|
||||
Spezifikation. Steht die ASCII-Form im Aufgabentext selbst (Namen wie „Mueller"), wird sie nicht
|
||||
ersetzt. Geprüft wird dafür nur die Aufgabe: die Spezifikation enthält ASCII-Bezeichner des LLM
|
||||
(beobachtet: Restriktion `Kardinalitaet` verhinderte die Korrektur von „Kardinalitaet“ im Text).
|
||||
"""
|
||||
def ersetze(treffer):
|
||||
richtig = UMLAUT_STEMS[treffer.group(0).lower()]
|
||||
return richtig.capitalize() if treffer.group(0)[0].isupper() else richtig
|
||||
quelle = " ".join(t or "" for t in (aufgabe, *weitere))
|
||||
vorhanden = {w.lower() for w in re.findall(r"\w+", aufgabe or "")}
|
||||
zuordnung = {}
|
||||
for wort in {m.group(0) for m in _UMLAUT_WORD.finditer(quelle)}:
|
||||
umschrieben = wort.translate(_ASCII).lower()
|
||||
if len(wort) >= 4 and umschrieben not in vorhanden:
|
||||
zuordnung[umschrieben] = wort.lower()
|
||||
return zuordnung
|
||||
|
||||
return _STEM_PATTERN.sub(ersetze, text or "")
|
||||
|
||||
def fix_transliterations(text: str, extra: dict | None = None) -> str:
|
||||
"""'guenstigste' -> 'günstigste': erst Wörter aus dem Lauf (`umlaut_map`), dann die feste Stammliste.
|
||||
|
||||
Nur für Lesertexte. Bezeichner und Restriktionsnamen bleiben ASCII – der Namensabgleich hängt daran.
|
||||
"""
|
||||
def gross_wie(vorlage, richtig):
|
||||
return richtig[:1].upper() + richtig[1:] if vorlage[:1].isupper() else richtig
|
||||
|
||||
text = text or ""
|
||||
if extra:
|
||||
muster = re.compile(r"\b(?:" + "|".join(sorted(map(re.escape, extra), key=len, reverse=True)) + r")\b",
|
||||
re.IGNORECASE)
|
||||
text = muster.sub(lambda t: gross_wie(t.group(0), extra[t.group(0).lower()]), text)
|
||||
return _STEM_PATTERN.sub(lambda t: gross_wie(t.group(0), UMLAUT_STEMS[t.group(0).lower()]), text)
|
||||
|
||||
|
||||
def check_text_quality(run, summary: str = "") -> list:
|
||||
"""Texte für Menschen prüfen (ohne LLM): umschriebene Umlaute und unerklärte Fachbegriffe.
|
||||
"""Texte für Menschen prüfen (ohne LLM): unerklärte Fachbegriffe in Antwortsatz und Tabellen.
|
||||
|
||||
Der Report ist ein Dokument für Entscheider; 'guenstigste' statt 'günstigste' gehört dort nicht hin,
|
||||
und ein Fachbegriff in einer Tabellenspalte muss vorher erklärt worden sein.
|
||||
Umschriebene Umlaute werden nicht mehr gemeldet, sondern still berichtigt (`repair_texts`):
|
||||
die Warnung stand in fast jedem Lauf und bewirkte nichts – im Code sind Umschreibungen erlaubt.
|
||||
"""
|
||||
texte = [("headline", run.headline or "")]
|
||||
for table in run.tables:
|
||||
texte.append((f"Tabelle '{table.title}'", " ".join([table.title, *map(str, table.columns), table.note or ""])))
|
||||
befunde = []
|
||||
for wo, text in texte:
|
||||
woerter = sorted({m.group(0) for m in TRANSLITERATIONS.finditer(text)})
|
||||
if woerter:
|
||||
befunde.append(f"{wo}: Umlaute umschrieben statt ausgeschrieben ({', '.join(woerter)}).")
|
||||
if summary:
|
||||
offen = unexplained_jargon(" ".join(text for _, text in texte), summary)
|
||||
if offen:
|
||||
|
|
@ -711,23 +734,23 @@ def check_generated_data(code: str, solver_code, spec=None) -> list:
|
|||
"Ergebnis nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht."]
|
||||
|
||||
|
||||
def repair_texts(run) -> None:
|
||||
"""Umschriebene Umlaute in allen Texten für Menschen berichtigen (nach der Prüfung)."""
|
||||
run.headline = fix_transliterations(run.headline) if run.headline else run.headline
|
||||
def repair_texts(run, extra: dict | None = None) -> None:
|
||||
"""Umschriebene Umlaute in allen Texten für Menschen berichtigen; `extra` aus `umlaut_map`."""
|
||||
run.headline = fix_transliterations(run.headline, extra) if run.headline else run.headline
|
||||
for antwort in run.answers:
|
||||
antwort.text = fix_transliterations(antwort.text)
|
||||
antwort.text = fix_transliterations(antwort.text, extra)
|
||||
for tabelle in run.tables:
|
||||
tabelle.title = fix_transliterations(tabelle.title)
|
||||
tabelle.columns = [fix_transliterations(c) for c in tabelle.columns]
|
||||
tabelle.note = fix_transliterations(tabelle.note) if tabelle.note else tabelle.note
|
||||
tabelle.rows = [[fix_transliterations(z) if isinstance(z, str) else z for z in zeile]
|
||||
tabelle.title = fix_transliterations(tabelle.title, extra)
|
||||
tabelle.columns = [fix_transliterations(c, extra) for c in tabelle.columns]
|
||||
tabelle.note = fix_transliterations(tabelle.note, extra) if tabelle.note else tabelle.note
|
||||
tabelle.rows = [[fix_transliterations(z, extra) if isinstance(z, str) else z for z in zeile]
|
||||
for zeile in tabelle.rows]
|
||||
for szenario in run.scenarios:
|
||||
szenario.name = fix_transliterations(szenario.name)
|
||||
szenario.description = fix_transliterations(szenario.description)
|
||||
szenario.name = fix_transliterations(szenario.name, extra)
|
||||
szenario.description = fix_transliterations(szenario.description, extra)
|
||||
if run.graph is not None:
|
||||
run.graph.node_meaning = fix_transliterations(run.graph.node_meaning)
|
||||
run.graph.arc_meaning = fix_transliterations(run.graph.arc_meaning)
|
||||
run.graph.node_meaning = fix_transliterations(run.graph.node_meaning, extra)
|
||||
run.graph.arc_meaning = fix_transliterations(run.graph.arc_meaning, extra)
|
||||
|
||||
|
||||
def check_answers(run, understanding, task: str = "", data_summary: str = "") -> list:
|
||||
|
|
@ -821,7 +844,9 @@ def _check_and_run(code: str, timeout: int, spec: OptimizationModelSpecification
|
|||
run.warnings += check_multistart(run, spec.objective.sense.value)
|
||||
run.warnings += check_graph(run, spec.objective.sense.value)
|
||||
run.warnings += check_text_quality(run, spec.executive_summary)
|
||||
repair_texts(run) # erst melden, dann beheben: der Report soll lesbar sein
|
||||
# Umlaute still berichtigen – mit den Wörtern, die in Aufgabe und Spezifikation mit Umlaut stehen
|
||||
repair_texts(run, umlaut_map(task, spec.model_dump_json(),
|
||||
understanding.model_dump_json() if understanding else ""))
|
||||
run.warnings += check_answers(run, understanding, task, data_summary)
|
||||
run.warnings += verify_scenarios(run, code, timeout, data_files)
|
||||
return run
|
||||
|
|
|
|||
|
|
@ -156,15 +156,15 @@ def test_report_shows_multistart_stability():
|
|||
assert "nicht nachweislich das globale Optimum" in markdown
|
||||
|
||||
|
||||
def test_transliterated_umlauts_are_reported():
|
||||
# beobachtet: "Der guenstigste Ersatzplan", Tabelle "Gewaehlte Nutzungszeitraeume"
|
||||
def test_transliterated_umlauts_are_no_longer_reported():
|
||||
# Umschreibungen im Code sind erlaubt; Lesertexte werden still berichtigt statt gemeldet
|
||||
from core.model_schema import ResultTable, SolverRun, SolverStatus
|
||||
from solving.solver_stage import check_text_quality
|
||||
run = SolverRun(status=SolverStatus.OPTIMAL, headline="Der guenstigste Plan kostet 29 €.",
|
||||
tables=[ResultTable(title="Gewaehlte Zeitraeume", columns=["Kauf"], rows=[])])
|
||||
befunde = check_text_quality(run)
|
||||
assert any("guenstigste" in b for b in befunde)
|
||||
assert any("Gewaehlte" in b and "Zeitraeume" in b for b in befunde)
|
||||
assert check_text_quality(run) == []
|
||||
befunde = check_text_quality(run, summary="Es wird ein Plan gewählt.")
|
||||
assert not any("Umlaut" in b for b in befunde)
|
||||
|
||||
|
||||
def test_correct_german_is_not_flagged():
|
||||
|
|
@ -177,16 +177,14 @@ def test_correct_german_is_not_flagged():
|
|||
assert check_text_quality(run) == []
|
||||
|
||||
|
||||
def test_transliterations_are_repaired_after_being_reported():
|
||||
# Die Warnung dokumentiert den Prompt-Verstoss, der Report soll trotzdem lesbar sein
|
||||
def test_transliterations_are_repaired_silently():
|
||||
from core.model_schema import Answer, ResultTable, SolverRun, SolverStatus
|
||||
from solving.solver_stage import check_text_quality, fix_transliterations, repair_texts
|
||||
from solving.solver_stage import repair_texts
|
||||
run = SolverRun(status=SolverStatus.OPTIMAL, headline="Der guenstigste Plan kostet 29 €.",
|
||||
answers=[Answer(question_id="a", text="Die Loesung waehlt zwei Aktien.")],
|
||||
tables=[ResultTable(title="Gewaehlte Zeitraeume", columns=["Naechste Periode"],
|
||||
rows=[["gewaehlt", 3]], note="Gemaess Aufgabe.")])
|
||||
assert check_text_quality(run) # erst melden
|
||||
repair_texts(run) # dann beheben
|
||||
repair_texts(run)
|
||||
assert run.headline == "Der günstigste Plan kostet 29 €."
|
||||
assert run.answers[0].text == "Die Lösung wählt zwei Aktien."
|
||||
assert run.tables[0].title == "Gewählte Zeiträume"
|
||||
|
|
@ -205,3 +203,28 @@ def test_repair_covers_stems_seen_in_the_lot_sizing_case():
|
|||
# beobachtet in wagner_whitin_dijkstra: "110 Baelle", "Das heisst:" im Antworttext
|
||||
from solving.solver_stage import fix_transliterations
|
||||
assert fix_transliterations("Das heisst: 110 Baelle, der Ball") == "Das heißt: 110 Bälle, der Ball"
|
||||
|
||||
|
||||
def test_words_from_the_run_repair_what_the_fixed_list_misses():
|
||||
# beobachtet in "Portfolio Nr. 2": Volatilitaet, Kardinalitaet, vollstaendigen, binaere im Report
|
||||
from core.model_schema import Answer, ConstraintValue, SolverRun, SolverStatus
|
||||
from solving.solver_stage import repair_texts, umlaut_map
|
||||
aufgabe = "Datenlieferant: Mueller GmbH."
|
||||
spezifikation = ("Minimiere die Volatilität unter der Kardinalität k; nur Titel mit vollständigen Reihen, "
|
||||
"binäre Auswahl. Restriktion: Kardinalitaet") # ASCII-Bezeichner des LLM
|
||||
run = SolverRun(status=SolverStatus.OPTIMAL,
|
||||
headline="Volatilitaet 12 %, Kardinalitaet eingehalten.",
|
||||
answers=[Answer(question_id="1", text="Nur Titel mit vollstaendigen Reihen, binaere Variablen; Quelle: Mueller GmbH.")],
|
||||
constraints=[ConstraintValue(name="Kardinalitaet", lhs=20, sense="==", rhs=20)])
|
||||
repair_texts(run, umlaut_map(aufgabe, spezifikation))
|
||||
assert run.headline == "Volatilität 12 %, Kardinalität eingehalten."
|
||||
assert run.answers[0].text == "Nur Titel mit vollständigen Reihen, binäre Variablen; Quelle: Mueller GmbH."
|
||||
assert run.constraints[0].name == "Kardinalitaet" # Bezeichner bleibt: Namensabgleich hängt daran
|
||||
|
||||
|
||||
def test_ascii_form_present_in_the_task_is_not_replaced():
|
||||
# steht die ASCII-Form selbst im Text (z. B. ein Name), ist sie echt
|
||||
from solving.solver_stage import fix_transliterations, umlaut_map
|
||||
zuordnung = umlaut_map("Herr Müller und die Firma Mueller liefern.")
|
||||
assert "mueller" not in zuordnung
|
||||
assert fix_transliterations("Firma Mueller", zuordnung) == "Firma Mueller"
|
||||
|
|
|
|||
Loading…
Reference in a new issue