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>
This commit is contained in:
Dieter Schlüter 2026-09-17 12:11:51 +02:00
commit 7459690bf5

View file

@ -1,89 +0,0 @@
"""Profil beigelegter Datendateien – Zeilen, Spalten, Typen, Spannweiten, Lücken.
Bewusst **ohne LLM**: Das Fremdprojekt MM-Agent lässt eine Datenbeschreibung vom Modell schreiben
(`DataDescription.summary`). Gerechnet ist billiger, reproduzierbar und nicht halluzinierbar – und
das Profil ist damit selbst eine geprüfte Tatsache, auf die sich die späteren Stufen berufen dürfen.
"""
from pathlib import Path
import pandas as pd
from model_schema import ColumnProfile, DataFileProfile
SAMPLE_ROWS = 3 # so viele Beispielwerte je Spalte in den Prompt
MAX_COLUMNS = 40 # breitere Tabellen werden abgeschnitten, damit der Prompt nicht platzt
def _read(path: Path) -> pd.DataFrame:
"""CSV (mit Trennzeichen-Erkennung) oder Excel einlesen."""
if path.suffix.lower() in (".xlsx", ".xls"):
return pd.read_excel(path)
# sep=None + engine="python" erkennt Komma, Semikolon und Tabulator selbst
return pd.read_csv(path, sep=None, engine="python")
def _as_numbers(spalte: pd.Series) -> pd.Series:
"""Zahlen einer Spalte, auch bei deutscher Schreibweise ('1.234,56' als Text eingelesen)."""
zahlen = pd.to_numeric(spalte, errors="coerce")
vorhanden = spalte.notna().sum()
if vorhanden and zahlen.notna().sum() < vorhanden and pd.api.types.is_string_dtype(spalte):
deutsch = pd.to_numeric(
spalte.astype(str).str.replace(".", "", regex=False).str.replace(",", ".", regex=False),
errors="coerce")
if deutsch.notna().sum() > zahlen.notna().sum():
return deutsch
return zahlen
def _column(name: str, spalte: pd.Series) -> ColumnProfile:
zahlen = _as_numbers(spalte)
# Eine Spalte gilt als Zahlenspalte, wenn die große Mehrheit der belegten Werte Zahlen sind
vorhanden = max(int(spalte.notna().sum()), 1)
numerisch = zahlen.notna().sum() >= 0.8 * vorhanden and not pd.api.types.is_bool_dtype(spalte)
beispiele = [str(v) for v in spalte.dropna().unique()[:SAMPLE_ROWS]]
return ColumnProfile(
name=str(name),
dtype="Zahl" if numerisch else "Text",
missing=int(spalte.isna().sum()),
minimum=float(zahlen.min()) if numerisch and zahlen.notna().any() else None,
maximum=float(zahlen.max()) if numerisch and zahlen.notna().any() else None,
mean=float(zahlen.mean()) if numerisch and zahlen.notna().any() else None,
samples=beispiele,
)
def profile_file(path) -> DataFileProfile:
"""Eine Datei einlesen und ihr Profil bestimmen; unlesbare Dateien melden den Fehler."""
path = Path(path)
try:
tabelle = _read(path)
except Exception as exc: # kaputte Datei darf den Lauf nicht abbrechen
return DataFileProfile(name=path.name, rows=0, columns=[], error=f"{type(exc).__name__}: {exc}")
spalten = [_column(name, tabelle[name]) for name in list(tabelle.columns)[:MAX_COLUMNS]]
return DataFileProfile(name=path.name, rows=int(len(tabelle)), columns=spalten,
truncated_columns=max(0, len(tabelle.columns) - MAX_COLUMNS))
def profile_files(paths) -> list:
return [profile_file(p) for p in paths or []]
def to_prompt(profiles) -> str:
"""Das Profil als knapper Text für die Prompts – Spaltennamen sind das Wichtigste."""
if not profiles:
return ""
teile = ["BEIGELEGTE DATEN (deterministisch ermittelt, nicht geschätzt):"]
for p in profiles:
if p.error:
teile.append(f"- {p.name}: NICHT LESBAR ({p.error})")
continue
teile.append(f"- {p.name}: {p.rows} Zeilen, {len(p.columns)} Spalten"
+ (f" (weitere {p.truncated_columns} Spalten nicht gezeigt)" if p.truncated_columns else ""))
for s in p.columns:
spanne = (f", von {s.minimum:g} bis {s.maximum:g}, Mittel {s.mean:g}"
if s.minimum is not None else "")
luecken = f", {s.missing} fehlend" if s.missing else ""
teile.append(f" {s.name} ({s.dtype}{spanne}{luecken}): {', '.join(s.samples)}")
teile.append("Lies diese Dateien im Programm über ihren Dateinamen (sie liegen im Arbeitsverzeichnis). "
"Jede abgeleitete Kennzahl wird aus den Daten berechnet, nie geschätzt.")
return "\n".join(teile)