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:
Dieter Schlüter 2026-09-17 13:28:43 +02:00
commit 0d9b5d718f
2 changed files with 88 additions and 40 deletions

View file

@ -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.

View file

@ -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