Kurs
GPT-5.2 „ ” ist ein starkes Modell für die Erstellung von Workflows und mehrstufigen Pipelines, die Zuverlässigkeit, Struktur und weniger Halluzinationen brauchen. OpenAI hat Verbesserungen bei der Arbeit mit langen Kontexten und eine noch bessere Leistung bei Aufgaben im Tabellenkalkulationsstil gezeigt.
In diesem Tutorial erstellen wir eine Streamlit-App, die sich wie ein Junior-Analyst verhält:
- Lade eine CSV-Datei hoch und schreib ein Ziel auf (zum Beispiel: „6-Folien-Update mit Fokus auf Wachstum, Kundenbindung, Unit Economics und Runway“)
- Erstelle einen schema-gesperrten JSON-Plan
- Mach eine Excel-Datei mit Rohdaten, abgeleiteten Kennzahlen und Diagrammen.
- Zum Schluss machst du eine passende PowerPoint-Präsentation mit den richtigen Quellenangaben.
Der Punkt ist, dass GPT-5.2 nicht direkt „Folien erstellt“. Es erstellt einen genauen Plan, den dein Code genau ausführt.
Was ist GPT-5.2?
GPT-5.2 ist das neueste Modell der GPT-5-Serie von OpenAI mit Verbesserungen für End-to-End-Aufgaben wie das Erstellen von Tabellenkalkulationen, das Erstellen von Präsentationen, das Schreiben und Debuggen von Code, das Verstehen langer Kontexte und die Verwendung von Tools.
Hier sind die GPT-5.2-Eigenschaften, die für dieses Projekt am wichtigsten sind:
- Langzeit-Kontext-Argumentation: GPT-5.2 Thinking gilt als top bei der Bewertung von Schlussfolgerungen in langen Kontexten (OpenAI MRCRv2) und ist super praktisch, um bei sehr langen Dokumenten und Eingaben mit mehreren Dateien zusammenhängend zu arbeiten.
- Mehrstufige Ausführungs: Dieses Modell ist super für komplexe Projekte mit vielen Schritten, einschließlich besserer Tool-Aufrufe und langfristiger Verhaltensweisen, was direkt zu unserer schemalockierten Planung und deterministischen Rendering-Schleife passt.
- Verbesserte Fakten: GPT-5.2 Thinking macht weniger Fehler als GPT-5.1 Thinking, was hier echt praktisch ist, weil es die Wahrscheinlichkeit verringert, dass Texte/Zahlen erzeugt werden, die von unseren angegebenen Excel-Bereichen abweichen könnten.
- Kompromisse bei der Latenz: In der API kannst du mit GPT-5.2 Instant für schnelles Feedback arbeiten und dann zu GPT-5.2 Thinking oder Pro wechseln, wenn du eine starke End-to-End-Planung und hochwertigere Artefakte willst. Denk aber daran, dass komplexere Generierungen Minuten dauern können.
GPT-5.2 Beispielprojekt: Erstelle einen Generator für strukturierte Artefakte
In diesem Abschnitt erstellen wir einen Generator für strukturierte Artefakte (Excel und PowerPoint) mit dem GPT 5.2-Modell, das in eine Streamlit-App eingebunden ist. Auf hoher Ebene macht die fertige Streamlit-App Folgendes:
-
Es liest CSV-Dateien und erstellt eine kanonische Arbeitsmappe.
-
Dann ruft es GPT-5.2 über OpenAI oder OpenRouter mit strukturierten Ausgaben auf (
response_format: json_schema). -
Überprüft den zurückgegebenen Plan anhand unseres Schemas.
-
Rendert Diagramme in Excel und passt sie an unsere Markenpalette an.
-
Am Ende macht es eine PowerPoint-Präsentation mit eingebauten Diagrammen und Zitaten.

Lass es uns Schritt für Schritt aufbauen.
Schritt 1: Voraussetzungen und Einrichtung
Bevor wir mit GPT-5.2 Excel-Arbeitsmappen und PowerPoint-Präsentationen erstellen können, müssen wir erst mal eine lokale Umgebung einrichten. Wir fangen damit an, die Kernbibliotheken zu installieren und API-Schlüssel als Umgebungsvariablen festzulegen.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
Wir werden Streamlit, um die interaktive Web-Benutzeroberfläche zu erstellen, Pandas zum Laden und Vorbereiten der CSV-Datei und Pydantic zum Validieren des schemageschützten JSON-Plans von GPT-5.2. Wir nutzen auch python-dotenv, um Schlüssel wie OPENROUTER_API_KEY/OPENAI_API_KEY sicher aus einer .env -Datei zu laden. Die Anfragen Bibliothek ruft den OpenRouter/OpenAI-kompatiblen Endpunkt auf, während openpyxl die Excel-Arbeitsmappe und -Diagramme erstellt und python-pptx erstellt die PowerPoint-Präsentation mit nativen (bearbeitbaren) Diagrammen.
Als Nächstes legen wir unseren/unsere API-Schlüssel fest:
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Beachte, dass du auf GPT 5.2 über verschiedene Dienste zugreifen kannst, darunter die offizielle API von OpenAI sowie Dienste wie OpenRouter und andere.
Es wird empfohlen, eine Datei „ .env “ zu verwenden, um die API-Schlüssel lokal zu speichern:
Speicher in der Datei „ .env “ Folgendes:
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
Lade dann diese API-Schlüssel mit dem Befehl load_dotenv :
from dotenv import load_dotenv
load_dotenv()
Jetzt ist unsere Umgebung bereit, sich bei der OpenAI-API anzumelden.
Schritt 2: Das Schema des Plans festlegen
Als Nächstes machen wir einen Schema-Locked-Artefakt-Plan, der aus einem strengen JSON-Entwurf besteht, der GPT-5.2 genau sagt, welche Diagramme erstellt und wie die Folien aufgebaut werden sollen, damit die Ausgabe des Modells mit Structured Outputs über response_format: { type: "json_schema", strict: true } statt mit Freitext entschlüsselt und überprüft werden kann.
import os
import io
import re
import json
import math
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Literal, Optional, Tuple
import requests
import pandas as pd
import numpy as np
import streamlit as st
import openpyxl
from dotenv import load_dotenv
from pydantic import BaseModel, Field, ValidationError
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter
from openpyxl.chart import LineChart, BarChart, Reference
from openpyxl.chart.series import SeriesLabel
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
ChartKind = Literal["line", "bar"]
def safe_float(x) -> float:
try:
if x is None:
return 0.0
if isinstance(x, (int, float)):
return float(x)
return float(str(x).replace(",", "").replace("$", ""))
except Exception:
return 0.0
def fmt_money(v: float) -> str:
sign = "-" if v < 0 else ""
v = abs(v)
if v >= 1_000_000:
return f"{sign}${v/1_000_000:.2f}M"
if v >= 1_000:
return f"{sign}${v/1_000:.1f}K"
return f"{sign}${v:.0f}"
def fmt_pct(v: float) -> str:
if v is None or (isinstance(v, float) and (math.isnan(v) or math.isinf(v))):
return "n/a"
return f"{v*100:.1f}%"
class ChartSeriesSpec(BaseModel):
name: str
metric: str
class ChartSpec(BaseModel):
id: str
kind: ChartKind
title: str
x_metric: str
series: List[ChartSeriesSpec]
anchor: str
class SlideBullet(BaseModel):
text: str
citations: List[str] = Field(min_length=1, max_length=4)
class SlideSpec(BaseModel):
title: str
chart_id: Optional[str] = None
bullets: List[SlideBullet] = Field(min_length=2, max_length=4)
class ArtifactPlan(BaseModel):
charts: List[ChartSpec] = Field(min_length=3, max_length=6)
slides: List[SlideSpec] = Field(min_length=4, max_length=10)
Der obige Codeausschnitt zeigt, wie man den Plan mit Pydantic-Modellen macht, die drei wichtige Sachen erledigen:
-
Planstruktur:
ChartKind = Literal["line", "bar"]Beschränkt die Diagrammtypen auf eine kleine Aufzählung, „ChartSpec“ erfasst den gesamten Diagrammvertrag und „SlideSpec“ legt das Layout der Folie fest, während eine Folie optional über „chart_id“ mit einem Diagramm verknüpft werden kann. -
Automatische Qualitätssicherung: Die Vorgaben wie „
Field()“ sorgen dafür, dass wir immer genug Diagramme/Folien haben, jede Folie genügend Aufzählungspunkte enthält und jeder Aufzählungspunkt Quellenangaben hat. -
Planung und Umsetzung: GPT-5.2 macht nur den Entwurf, also Diagramme, Folien und Zitate, als strukturiertes JSON, während der Rest vom Code die Ausführung, die Excel-Formatierung, die Diagrammdarstellung und das Deck-Layout übernimmt.
Mit diesem Schema wird GPT-5.2 zu einem Planer, der einen validierten JSON-Entwurf erstellt.
Schritt 3: Ruf GPT-5.2 mit strukturierten Ausgaben an
Nachdem das Planschema definiert ist, rufstdu als Nächstes GPT-5.2 im „Schema-Modus” auf, damit es einen JSON-Entwurf zurückgibt, den du überprüfen und ausführen kannst. Anstatt zu hoffen, dass das Modell „sich benimmt“, sagen wir der API, dass sie direkt in dein JSON-Schema entschlüsseln soll, indem wir response_format: { type: "json_schema", ... strict: true } verwenden. Genau dafür sind strukturierte Ausgaben da.
def chat_json_schema(
model: str,
messages: list,
json_schema: dict,
api_key: str,
temperature: float = 0.2,
max_tokens: int = 6000,
timeout_s: int = 120,
api_provider: Optional[str] = None,
) -> dict:
if api_provider is None:
if "/" in model:
api_provider = "openrouter"
elif os.getenv("OPENAI_API_KEY") and api_key == os.getenv("OPENAI_API_KEY"):
api_provider = "openai"
elif os.getenv("OPENROUTER_API_KEY") and api_key == os.getenv("OPENROUTER_API_KEY"):
api_provider = "openrouter"
else:
api_provider = "openrouter"
if api_provider == "openai":
url = "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
if "/" in model:
model = model.split("/", 1)[1]
else:
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
if os.getenv("OPENROUTER_SITE_URL"):
headers["HTTP-Referer"] = os.getenv("OPENROUTER_SITE_URL")
if os.getenv("OPENROUTER_APP_NAME"):
headers["X-Title"] = os.getenv("OPENROUTER_APP_NAME")
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "artifact_plan",
"strict": True,
"schema": json_schema,
},
},
}
r = requests.post(url, headers=headers, data=json.dumps(payload), timeout=timeout_s)
r.raise_for_status()
data = r.json()
if "error" in data:
provider_name = "OpenAI" if api_provider == "openai" else "OpenRouter"
raise RuntimeError(f"{provider_name} error: {data['error']}")
finish_reason = data["choices"][0].get("finish_reason")
if finish_reason == "length":
raise RuntimeError("Model output truncated (max_tokens). Increase max_tokens or reduce schema size.")
content = data["choices"][0]["message"]["content"]
if isinstance(content, dict):
return content
try:
return json.loads(content)
except json.JSONDecodeError as e:
preview = (content or "")[:800]
raise RuntimeError(f"Failed to parse JSON. Preview:\n{preview}") from e
Die Funktion „ chat_json_schema() ” ist das einzige Gateway, das Eingabeaufforderungen in einen validierten Plan verwandelt:
-
Anbieter-Routing (OpenRouter vs. OpenAI): Wenn der Modellname einen Namespace hat (openai/gpt-5.2), wird standardmäßig OpenRouter verwendet, weil das das Format ist, das OpenRouter für das Modell-Routing benutzt. Wenn du explizit api_provider="openai" festlegst, sendest du stattdessen dieselbe Chat Completions-Nutzlast an den Endpunkt von OpenAI. Du kannst OpenRouter einfach komplett überspringen.
-
Endpunkte festlegen: Für OpenAI rufst du den Endpunkt „
https://api.openai.com/v1/chat/completions“ mit der Standard-Bearer-Authentifizierung auf. Für OpenRouter rufst du den Endpunkt „https://openrouter.ai/api/v1/chat/completions“ auf und fügst optional Attributions-Header wie HTTP-Referer und X-Title hinzu, die OpenRouter als empfohlene Metadaten dokumentiert. -
Schema-Entschlüsselung über
response_format: Der wichtigste Payload-Block ist „response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }“, der das Modell anweist, eine Ausgabe zu erzeugen, die auf unser JSON-Schema beschränkt ist.
Als Nächstes checken wir den JSON-Plan des Modells mit Pydantic und machen erst weiter mit dem Rendern der Excel-Diagramme und dem Erstellen der PowerPoint-Präsentation, wenn die Grenzwertprüfungen okay sind.
Schritt 4: Hilfsfunktionen
Bevor wir irgendwas rendern, brauchen wir eine kleine Ebene, die alle hochgeladenen CSV-Dateien sicher analysierbar und konsistent referenzierbar macht. Diese Hilfsfunktionen machen vier Sachen: Sie laden und bereinigen die CSV-Datei, erstellen eine abgeleitete Tabelle, berechnen einen kompakten Satz von Fakten für die Eingabeaufforderung und machen die Excel-Ausgabe mit automatisch angepassten Spalten lesbar.
Schritt 4.1: CSV laden und überprüfen
GPT 5.2 kann Diagramme und Folien nicht zuverlässig planen, wenn die Eingabetabelle leer ist oder numerische Felder enthält, die als Zeichenfolgen gespeichert sind. Diese Funktion macht den Datensatz einheitlich, damit man bei den nächsten Schritten nicht raten muss.
def load_and_validate_csv(csv_path: str) -> pd.DataFrame:
df = pd.read_csv(csv_path)
if len(df) == 0:
raise ValueError("CSV is empty")
if len(df.columns) == 0:
raise ValueError("CSV has no columns")
date_cols = [c for c in df.columns if any(x in c.lower() for x in ['date', 'time', 'month', 'year', 'period', 'week'])]
if date_cols:
date_col = date_cols[0]
df[date_col] = df[date_col].astype(str)
for col in df.columns:
if col not in date_cols:
try:
df[col] = pd.to_numeric(df[col])
if df[col].dtype in ['float64', 'int64']:
df[col] = df[col].fillna(0)
except (ValueError, TypeError):
pass
return df
Die obige Funktion:
- Liest die Datei mit in DataFrame
pd.read_csv. - Erkennt Spalten anhand von Namensheuristiken (Datum, Monat, Woche, Zeitraum) und wandelt sie in eine Zeichenfolge um. Dadurch bleiben Beschriftungen in Diagrammen und Folientexten stabil.
- Schließlich werden Spalten, die keine Datumsangaben enthalten, mit „
pd.to_numeric“ in Zahlen umgewandelt, und fehlende Zahlenwerte werden mit Null aufgefüllt, um später NaN-Kaskaden zu vermeiden.
Am Ende dieses Schritts hast du einen DataFrame, der nicht leer ist und numerische Spalten hat.
Schritt 4.2: Berechne das Modell-DataFrame
Rohe CSV-Dateien sind chaotisch und nicht einheitlich über verschiedene Domänen hinweg. Das Modellblatt ist wie eine Art Normalisierungsschicht, wo wir abgeleitete Metriken hinzufügen, die für die Diagrammerstellung nützlich sind, ohne dass der Benutzer sie manuell berechnen muss.
def compute_model_df(raw_df: pd.DataFrame) -> pd.DataFrame:
df = raw_df.copy()
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
if "new_mrr" in df.columns and "churned_mrr" in df.columns:
df["net_new_mrr"] = df["new_mrr"] - df["churned_mrr"]
revenue_cols = [c for c in df.columns if any(x in c.lower() for x in ['revenue', 'mrr', 'income', 'sales'])]
cost_cols = [c for c in df.columns if any(x in c.lower() for x in ['cost', 'cogs', 'expense'])]
if revenue_cols and cost_cols:
revenue = df[revenue_cols[0]]
cost = df[cost_cols[0]]
df["gross_margin"] = ((revenue - cost) / revenue.replace(0, pd.NA)).fillna(0)
for col in numeric_cols[:5]:
if col not in df.columns:
continue
df[f"{col}_change"] = df[col].pct_change().fillna(0)
return df
Die Funktion „ compute_model_df() “ macht zwei ziemlich nützliche Sachen:
-
Es sucht nach einer Spalte mit Primärausgabewerten und einer Spalte mit Sekundäreingabewerten und berechnet ein einfaches Verhältnis, um zu ermitteln, wie viel vom Primärsignal nach Berücksichtigung des Sekundärsignals übrig bleibt.
-
Für ein paar numerische Spalten berechnet es „
pct_change()“ (Veränderung gegenüber vorheriger Zeile), was standardmäßig die prozentuale Veränderung gegenüber der vorherigen Zeile angibt. Das ist eine einfache Basis, um Veränderungen in Zeitreihenmetriken zu erfassen.
Schritt 4.3: Fakten berechnen
Auch bei strukturierten Ausgaben wollen wir GPT-5.2 eine kurze Zusammenfassung der Veränderungen in diesem Zeitraum geben. Diese Fakten dienen auch als Vorlage, die das Modell in Folienaufzählungen wiederverwenden kann.
def compute_facts(model_df: pd.DataFrame) -> Dict[str, str]:
def delta(a: float, b: float) -> float:
return 0.0 if a == 0 else (b - a) / a
if len(model_df) == 0:
return {"error": "No data"}
first = model_df.iloc[0]
last = model_df.iloc[-1]
date_cols = [c for c in model_df.columns if any(x in c.lower() for x in ['date', 'time', 'month', 'year', 'period', 'week'])]
period_col = date_cols[0] if date_cols else model_df.columns[0]
facts = {
"period": f"{first[period_col]} → {last[period_col]}",
"row_count": len(model_df),
"column_count": len(model_df.columns),
}
numeric_cols = model_df.select_dtypes(include=[np.number]).columns.tolist()
for i, col in enumerate(numeric_cols[:5]):
if col in first.index and col in last.index:
start_val = safe_float(first[col])
end_val = safe_float(last[col])
avg_val = safe_float(model_df[col].mean())
facts[f"{col}_start"] = fmt_money(start_val) if start_val >= 100 else f"{start_val:.2f}"
facts[f"{col}_end"] = fmt_money(end_val) if end_val >= 100 else f"{end_val:.2f}"
facts[f"{col}_change_pct"] = fmt_pct(delta(start_val, end_val))
facts[f"{col}_avg"] = fmt_money(avg_val) if avg_val >= 100 else f"{avg_val:.2f}"
return facts
Zuerst wählen wir die erste und letzte Zeile aus, um das Zeitfenster festzulegen. Dann suchen wir uns eine Spalte aus (Datum, Monat, Woche, Zeitraum), damit die Berichte für uns lesbar sind.
Für eine kleine Gruppe von numerischen Spalten machen wir eine Momentaufnahme von Anfang, Ende, Durchschnitt und der gesamten prozentualen Veränderung vom Anfang bis zum Ende. Dadurch entsteht ein leichtes Signalwörterbuch, das dem Modell hilft, Folienpunkte zu schreiben.
Schritt 4.4: Excel-Spalten automatisch anpassen
Die Standardbreiten der Excel-Spalten lassen die erstellten Arbeitsmappen unordentlich aussehen. Autosizing ist ein einfacher Schliff, der die Benutzerfreundlichkeit sofort verbessert.
def autosize_columns(ws, max_width: int = 45):
for col in ws.columns:
max_len = 0
col_letter = get_column_letter(col[0].column)
for cell in col:
if cell.value is None:
continue
max_len = max(max_len, len(str(cell.value)))
ws.column_dimensions[col_letter].width = min(max_len + 2, max_width)
Die Funktion „ autosize_columns() ” checkt jede Spalte, sucht die längste Zeichenfolge und setzt „ ws.column_dimensions[col_letter].width ”. Dann zeigt die OpenPyXL-Bibliothek „ column_dimensions “ und seine Eigenschaft „ width “ an, um die Spaltenanzeigegröße in Excel zu steuern.
Als Nächstes packen wir die bereinigte Tabelle und die Fakten in die GPT-5.2-Planungsaufforderung, checken den zurückgegebenen Plan und erstellen dann Diagramme und Folien.
Schritt 5: Erstell die Arbeitsmappe
Mit der bereinigten Tabelle und den leicht verständlichen Zusammenfassungen aus dem vorherigen Schritt erstellen wir jetzt mit openpyxl eine deterministische Excel-Arbeitsmappe.
def build_workbook_base(raw_df: pd.DataFrame, model_df: pd.DataFrame, out_xlsx: str) -> Tuple[str, int]:
wb = Workbook()
wb.remove(wb.active)
header_fill = PatternFill("solid", fgColor="F2F2F2")
header_font = Font(bold=True)
ws_raw = wb.create_sheet("Raw")
for j, col in enumerate(raw_df.columns, start=1):
c = ws_raw.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")
for i, row in enumerate(raw_df.itertuples(index=False), start=2):
for j, val in enumerate(row, start=1):
ws_raw.cell(row=i, column=j, value=val)
ws_raw.freeze_panes = "A2"
autosize_columns(ws_raw)
row_end = 1 + len(raw_df)
ws_model = wb.create_sheet("Model")
for j, col in enumerate(model_df.columns, start=1):
c = ws_model.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")
for i, row in enumerate(model_df.itertuples(index=False), start=2):
for j, val in enumerate(row, start=1):
ws_model.cell(row=i, column=j, value=val)
ws_model.freeze_panes = "A2"
autosize_columns(ws_model)
col_index = {name: idx + 1 for idx, name in enumerate(model_df.columns)}
money_keywords = ['mrr', 'revenue', 'income', 'sales', 'cost', 'expense', 'cash', 'balance', 'burn', 'price', 'amount', 'fee']
pct_keywords = ['margin', 'rate', 'ratio', 'pct', 'percent', 'change', 'growth']
for col_name, col_idx in col_index.items():
col_lower = col_name.lower()
is_money = any(kw in col_lower for kw in money_keywords)
is_pct = any(kw in col_lower for kw in pct_keywords) or 'margin' in col_lower
is_large_number = False
if col_name in model_df.columns:
sample_vals = model_df[col_name].dropna()
if len(sample_vals) > 0 and pd.api.types.is_numeric_dtype(sample_vals):
max_val = abs(sample_vals.max())
is_large_number = max_val > 1000
for r in range(2, row_end + 1):
if is_money or is_large_number:
ws_model.cell(row=r, column=col_idx).number_format = "$#,##0"
elif is_pct:
ws_model.cell(row=r, column=col_idx).number_format = "0.0%"
elif pd.api.types.is_numeric_dtype(model_df[col_name]):
ws_model.cell(row=r, column=col_idx).number_format = "0.0"
ws_charts = wb.create_sheet("Charts")
ws_charts["A1"] = "Charts"
ws_charts["A1"].font = Font(bold=True)
wb.save(out_xlsx)
return out_xlsx, row_end
Hier ist, was dieser Arbeitsbuch-Generator macht:
-
Mach das Grundgerüst der Arbeitsmappe fertig: Die Funktion „
Workbook()” macht ein neues Arbeitsmappenobjekt, und „wb.remove(wb.active)” löscht das Standardblatt, damit wir die Reihenfolge der Blätter komplett kontrollieren können. Neue Arbeitsblätter werden mit den Befehlen „wb.create_sheet("Raw")“, „wb.create_sheet("Model")“ und „wb.create_sheet("Charts")“ hinzugefügt. -
Schreib das Rohblatt: Wir erstellen eine Kopfzeile mit Font, PatternFill und Alignment und fügen dann jede Zeile aus raw_df hinzu. Die Funktion „.
autosize_columns(ws_raw)“ macht die Lesbarkeit besser, ohne dass du die Größe manuell anpassen musst. -
Berechne die Grenzen: Wir nutzen
row_end = 1 + len(raw_df), das uns eine stabile letzte Zeile gibt, damit wir sicher Bereiche wieA2:A{row_end}für Diagrammkategorien und Zitate erstellen können. -
Wende die Zahlenformat-Heuristik an: Als Nächstes legen wir die „
cell.number_format” anhand der Schlüsselwörter für Spaltennamen und des Datentyps fest, damit Excel die Werte als Währung, Prozentangaben oder einfache Zahlen anzeigt. -
Mach ein Diagrammblatt: Das
wb.create_sheet("Charts") gibt uns eine stabile Arbeitsfläche für Diagrammanker. Das passt zum openpyxl-Diagramm-Workflow, wo wir ein LineChart oder BarChart erstellen, Bereiche überReferencebinden, Kategorien mitset_categoriesfestlegen und es dann mitws.add_chart()platzieren.
Nach diesem Schritt hast du eine einheitliche Excel-Datei, auf die in der nächsten Phase anhand genauer Zellbereiche sicher verwiesen werden kann.
Schritt 6: Diagramme rendern
Jetzt, wo die Struktur der Arbeitsmappe fertig ist, können wir die Diagramme im Blatt „Charts“ rendern und ein einheitliches Markenimage anwenden. Ich hab die Farbpalette von DataCamp mit folgenden Farben genommen:
- Primärfarbe: #01ef63
- Sekundärfarbe: #203147
def add_charts_to_workbook(xlsx_path: str, charts: List[ChartSpec], metric_to_range: Dict[str, str]):
wb = load_workbook(xlsx_path)
ws_model = wb["Model"]
ws_charts = wb["Charts"]
header_map = {ws_model.cell(1, c).value: c for c in range(1, ws_model.max_column + 1)}
def ref_from_metric(metric: str, row_start: int, row_end: int) -> Reference:
if metric not in header_map:
raise ValueError(f"Unknown metric in Model sheet: {metric}")
col = header_map[metric]
return Reference(ws_model, min_col=col, min_row=row_start, max_col=col, max_row=row_end)
for ch in charts:
chart = LineChart() if ch.kind == "line" else BarChart()
chart.title = ch.title
x_metric = ch.x_metric
if x_metric not in header_map:
date_keywords = ['date', 'time', 'month', 'year', 'period', 'week']
date_cols = [col for col in header_map.keys() if col and any(kw in str(col).lower() for kw in date_keywords)]
if date_cols:
x_metric = date_cols[0]
else:
x_metric = list(header_map.keys())[0] if header_map else "month"
x_ref = ref_from_metric(x_metric, 2, ws_model.max_row)
chart.set_categories(x_ref)
chart_colors = ["01EF63", "203147"]
for i, s in enumerate(ch.series):
y_ref = ref_from_metric(s.metric, 2, ws_model.max_row)
chart.add_data(y_ref, titles_from_data=False)
chart.series[-1].title = SeriesLabel(v=s.name)
series = chart.series[-1]
color_hex = chart_colors[i % len(chart_colors)]
try:
if ch.kind == "line":
series.graphicalProperties.line.solidFill = color_hex
series.graphicalProperties.line.width = 30000
else:
series.graphicalProperties.solidFill = color_hex
except AttributeError:
pass
anchor = ch.anchor.strip()
if "!" in anchor:
anchor = anchor.split("!")[-1]
if anchor and anchor.isalpha():
anchor = f"{anchor}2"
if not re.match(r'^[A-Z]+[0-9]+
Die Funktion „ add_charts_to_workbook() “ ist die Engine für die Diagrammdarstellung, die unseren Schemaplan in Excel-Visualisierungen mit Branding verwandelt.
-
Lade die Arbeitsmappe: Wir fangen damit an, die erstellte Excel-Datei mit „
load_workbook(xlsx_path)“ zu öffnen. Als Nächstes erstellen wir eine „header_map“ aus der ersten Zeile, wobei wir jeden Spaltennamen dem entsprechenden Excel-Spaltenindex zuordnen. -
Zellbereichsreferenzen erstellen: Die Funktion „
Reference()“ legt den genauen Bereich fest, der dargestellt werden soll, und „chart.set_categories(x_ref)“ verbindet die Kategorien der X-Achse. Normalerweise macht man es so, dass man Referenzobjekte erstellt, Daten hinzufügt und dann Kategorien festlegt. -
Den Diagrammtyp instanziieren: Für jede Diagrammspezifikation erstellen wir entweder eine „
LineChart()“ oder eine „BarChart()“ und setzen „chart.title“. -
Füge die Markenpalette hinzu: Für jede Serie in der Spezifikation binden wir den Y-Bereich mit „
chart.add_data()“ und setzen dann die Anzeige-Beschriftung mit „SeriesLabel()“.
Schließlich platziert die Funktion „ ws_charts.add_chart() “ jedes Diagramm in einer bestimmten Zelle oben links.
Schritt 7: PowerPoint rendern
In diesem Schritt wird der Plan zu einer echten Präsentation. Wir machen eine PowerPoint-Präsentation, gestalten Titel und Aufzählungspunkte, fügen Diagramme ein, die wir aus derselben Tabelle wie in Excel erstellt haben, und fügen eine Fußzeile hinzu, die die Zellbereichsverweise des Plans beibehält.
def build_pptx(out_pptx: str, model_df: pd.DataFrame, plan: ArtifactPlan):
prs = Presentation()
blank = prs.slide_layouts[6]
chart_by_id = {c.id: c for c in plan.charts}
def add_title(slide, title: str):
box = slide.shapes.add_textbox(Inches(0.5), Inches(0.2), Inches(9.0), Inches(0.6))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = title
p.font.size = Pt(32)
p.font.bold = True
def add_bullets(slide, bullets: List[SlideBullet], left, top, width, height):
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.word_wrap = True
tf.clear()
for i, b in enumerate(bullets):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.text = b.text
p.font.size = Pt(18)
p.level = 0
p.space_before = Pt(4)
p.space_after = Pt(8)
def add_sources_footer(slide, bullets: List[SlideBullet]):
sources = []
seen = set()
for b in bullets:
for c in b.citations:
if c not in seen:
seen.add(c)
sources.append(c)
if not sources:
return
text = "Sources: " + "; ".join(sources)
box = slide.shapes.add_textbox(Inches(0.5), Inches(6.8), Inches(9.0), Inches(0.4))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(9)
p.font.italic = True
def add_chart(slide, chart_spec: ChartSpec, left, top, width, height):
date_cols = [c for c in model_df.columns if any(x in c.lower() for x in ['date', 'time', 'month', 'year', 'period', 'week'])]
x_col = date_cols[0] if date_cols else model_df.columns[0]
categories = model_df[x_col].astype(str).tolist()
chart_data = CategoryChartData()
chart_data.categories = categories
chart_colors = ["01EF63", "203147"]
for i, s in enumerate(chart_spec.series):
values = model_df[s.metric].apply(safe_float).tolist()
chart_data.add_series(s.name, values)
chart_type = XL_CHART_TYPE.LINE if chart_spec.kind == "line" else XL_CHART_TYPE.COLUMN_CLUSTERED
graphic_frame = slide.shapes.add_chart(chart_type, left, top, width, height, chart_data)
chart = graphic_frame.chart
chart.has_title = True
chart.chart_title.text_frame.text = chart_spec.title
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = False
from pptx.dml.color import RGBColor
for i, series in enumerate(chart.series):
color_hex = chart_colors[i % len(chart_colors)]
color_rgb = RGBColor(
int(color_hex[0:2], 16),
int(color_hex[2:4], 16),
int(color_hex[4:6], 16)
)
fill = series.format.fill
fill.solid()
fill.fore_color.rgb = color_rgb
if chart_spec.kind == "line":
line = series.format.line
line.color.rgb = color_rgb
line.width = Pt(3)
for s in plan.slides:
slide = prs.slides.add_slide(blank)
add_title(slide, s.title)
has_chart = bool(s.chart_id and s.chart_id in chart_by_id)
if has_chart:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),
)
add_chart(
slide,
chart_by_id[s.chart_id],
left=Inches(5.2),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),
)
else:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(9.0),
height=Inches(5.5),
)
add_sources_footer(slide, s.bullets)
prs.save(out_pptx)
def build_allowed_ranges(model_df: pd.DataFrame, row_end: int) -> Dict[str, str]:
ranges = {}
for i, col in enumerate(model_df.columns, start=1):
col_letter = get_column_letter(i)
ranges[col] = f"Model!{col_letter}2:{col_letter}{row_end}"
return ranges
def validate_plan(plan: ArtifactPlan, allowed_ranges: Dict[str, str]) -> None:
allowed_set = set(allowed_ranges.values())
chart_ids = {c.id for c in plan.charts}
if len(chart_ids) != len(plan.charts):
raise ValueError("Duplicate chart ids in plan.")
for s in plan.slides:
if s.chart_id and s.chart_id not in chart_ids:
raise ValueError(f"Slide references unknown chart_id: {s.chart_id}")
for s in plan.slides:
for b in s.bullets:
for c in b.citations:
if c not in allowed_set:
raise ValueError(f"Citation not in allowed set: {c}")
allowed_metrics = set(allowed_ranges.keys())
for c in plan.charts:
if c.x_metric not in allowed_metrics:
pass
for s in c.series:
if s.metric not in allowed_metrics:
raise ValueError(f"Unknown series metric: {s.metric}")
Schauen wir uns den obigen Code Schritt für Schritt an:
-
Erstell Deck- und Index-Diagramme:
prs = Presentation()startet eine neue PPTX-Datei, währendblank = prs.slide_layouts[6]uns eine leere Leinwand für die genaue Platzierung gibt. -
Überschriften und Aufzählungszeichen: Die Funktion „
add_title()“ schreibt in ein Textfeld „text_frame“, und „add_bullets()“ fügt pro Aufzählungszeichen einen Absatz hinzu, während Schriftgröße und Abstand über die Absatzformatierung gesteuert werden. -
Quellenangaben Fußzeile: Die Funktion „
add_sources_footer()“ entfernt doppelte Zitate aus den Aufzählungspunkten und macht eine kompakte Zeile mit dem Namen „Sources:“ unten, damit die Rückverfolgbarkeit erhalten bleibt, ohne die Folie zu überladen. -
Diagramme einbetten:
add_chart()erstellt einCategoryChartData()-Objekt mit einer zeitähnlichen X-Spalte und den geplanten Serienwerten und fügt es dann überslide.shapes.add_chart()ein, was einGraphicFramezurückgibt. -
Markenstil: Zum Schluss wandeln wir Hexadezimal in Dezimal um (
RGBColor) und wenden das pro Serie an. -
Layoutregeln und Leitplanken: Wenn eine Folie eine gültige
chart_idhat, nehmen wir ein zweispaltiges Layout, sonst nehmen wir Aufzählungszeichen in voller Breite. Die Funktion „build_allowed_ranges()“ erstellt die Liste der zulässigen Bereiche, die zitiert werden dürfen, und „validate_plan()“ sorgt dafür, dass Diagramm-IDs eindeutig sind, Referenzen gültig sind, Zitate erlaubt sind und Metriken gültig sind, damit das Modell keine Felder erfinden kann.
Schritt 8: Erstelle den Plan mit dem LLM
Jetzt haben wir saubere tabellarische Daten, die wir in einen Schema-Erstellungsplan (Diagramme, Folien und Zitate) umwandeln, den der Renderer ausführen kann.
def generate_plan_with_llm(
api_key: str,
model: str,
goal: str,
allowed_ranges: Dict[str, str],
facts: Dict[str, str],
num_slides: int = 6,
num_charts: int = 4,
) -> ArtifactPlan:
plan_schema = {
"type": "object",
"properties": {
"charts": {
"type": "array",
"minItems": 3,
"maxItems": 6,
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"kind": {"type": "string", "enum": ["line", "bar"]},
"title": {"type": "string"},
"x_metric": {"type": "string"},
"series": {
"type": "array",
"minItems": 1,
"maxItems": 3,
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"metric": {"type": "string"},
},
"required": ["name", "metric"],
"additionalProperties": False,
},
},
"anchor": {"type": "string"},
},
"required": ["id", "kind", "title", "x_metric", "series", "anchor"],
"additionalProperties": False,
},
},
"slides": {
"type": "array",
"minItems": 4,
"maxItems": 10,
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"chart_id": {"type": ["string", "null"]},
"bullets": {
"type": "array",
"minItems": 2,
"maxItems": 4,
"items": {
"type": "object",
"properties": {
"text": {"type": "string"},
"citations": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {"type": "string"},
},
},
"required": ["text", "citations"],
"additionalProperties": False,
},
},
},
"required": ["title", "chart_id", "bullets"],
"additionalProperties": False,
},
},
},
"required": ["charts", "slides"],
"additionalProperties": False,
}
system = (
"You produce investor-grade slides with traceability.\n"
"Output MUST match the JSON schema exactly.\n"
"Rules:\n"
"1) Use ONLY the allowed citation ranges (exact strings).\n"
"2) No generic filler. Every bullet must contain at least ONE number (e.g., $X, X%, X mo).\n"
"3) Keep bullets short: <= 12 words.\n"
"4) Prefer trends (start→end) or averages; avoid speculation.\n"
"5) Provide exactly the requested number of charts and slides.\n"
"6) Use chart_id to attach the correct chart to each slide.\n"
"7) Chart anchors: Use cell references ONLY (e.g., 'A2', 'A20', 'A38'). Do NOT include sheet name like 'CHARTS!A'.\n"
)
user = (
f"Goal: {goal}\n"
f"Requested: {num_charts} charts and {num_slides} slides.\n\n"
f"Facts (use these; do not invent):\n{json.dumps(facts, indent=2)}\n\n"
f"Allowed citation ranges (use exact values only):\n{json.dumps(list(allowed_ranges.values()), indent=2)}\n\n"
"Metric keys available for charts:\n"
f"{json.dumps(list(allowed_ranges.keys()), indent=2)}\n\n"
"Chart requirements:\n"
"- x_metric: Use a date/time column (e.g., 'month', 'date', 'period', 'week') for the X-axis.\n"
" If no date column exists, use the first column from the available metrics.\n"
"- Create meaningful charts based on available metrics in the data.\n"
"- Focus on trends, comparisons, and key business metrics.\n"
"- Chart colors: Use these specific shades for all chart series:\n"
" * Primary color: #01ef63 (bright green)\n"
" * Secondary color: #203147 (dark blue/navy)\n"
" * Alternate between these two colors for multiple series in the same chart.\n"
"Slide requirements:\n"
"- Create slides that tell a story with the available data.\n"
"- Suggested structure: Overview, Key Metrics, Trends, Analysis, Summary, Next Steps.\n"
"- Adapt slide titles and content to match the data domain (e.g., sales, marketing, operations, finance).\n"
)
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
plan_json = chat_json_schema(
model=model,
messages=messages,
json_schema=plan_schema,
api_key=api_key,
temperature=0.2,
max_tokens=6000,
)
try:
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)
if len(plan.slides) != num_slides:
raise ValueError(f"Expected {num_slides} slides, got {len(plan.slides)}")
if len(plan.charts) != num_charts:
raise ValueError(f"Expected {num_charts} charts, got {len(plan.charts)}")
return plan
except (ValidationError, ValueError) as e:
repair = messages + [
{"role": "assistant", "content": json.dumps(plan_json)},
{"role": "user", "content": f"Fix JSON to satisfy schema + rules. Error:\n{str(e)}"},
]
plan_json = chat_json_schema(
model=model,
messages=repair,
json_schema=plan_schema,
api_key=api_key,
temperature=0.0,
max_tokens=6000,
)
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)
if len(plan.slides) != num_slides or len(plan.charts) != num_charts:
raise RuntimeError("Plan repaired but counts still wrong. Tighten prompt or increase max_tokens.")
return plan
Die Funktion „ generate_plan_with_llm() ” ist das Herzstück dieser Demo. So fügen wir es zur Pipeline hinzu:
-
Explizites JSON-Schema: Die Schnittstelle „
plan_schema“ legt fest, welche Antwortform das Modell erzeugen darf:charts[]undslides[]mit den erforderlichen Feldern und Aufzählungen. -
System-Eingabeaufforderungen: Wir legen unverhandelbare Punkte fest, damit der Plan immer umsetzbar und überprüfbar bleibt. Wir beschränken die Platzierung von Diagrammen auch auf einfache Zellen, damit das Diagrammlayout fest bleibt und nicht durch das Modell improvisiert wird. Genau für diese Art der mehrstufigen Steuerung wurde GPT-5.2 entwickelt.
-
Benutzernachricht: Die Benutzernachricht enthält das Ziel, die angeforderten Zählungen, die berechneten Fakten und die erlaubten Metrikschlüssel. Weil das Modell nie was anderes sieht, hat es viel weniger Möglichkeiten, Spalten, Bereiche oder nicht unterstützte Behauptungen zu erfinden.
-
Schema-konforme Dekodierung: Wir rufen „
chat_json_schema()“ auf, wobei „response_format“ auf „json_schema“ und „strict: true“ gesetzt ist, was die Einhaltung des Schemas während der Dekodierung sicherstellt. -
In zwei Schichten überprüfen: Zuerst checkt die Funktion „
ArtifactPlan.model_validate(plan_json)“, ob die Antwort zu unseren Pydantic-Modellen passt. Dann sorgt „validate_plan()“ dafür, dass die Domain-Einschränkungen eingehalten werden.
Schritt 9: Pipeline ausführen
Zum Schluss verbinden wir das Laden von CSV-Dateien, das Erstellen von Excel-Dateien, die LLM-Planung, das Rendern von Diagrammen und den endgültigen PPTX-Export miteinander und geben dann die Ausgabepfade zurück.
@dataclass
class RunOutputs:
xlsx_path: str
pptx_path: str
plan_path: str
logs: str
def run_pipeline(csv_path: str, goal: str, outdir: str, model: str, num_slides: int, num_charts: int) -> RunOutputs:
load_dotenv()
openai_key = os.getenv("OPENAI_API_KEY")
openrouter_key = os.getenv("OPENROUTER_API_KEY")
if "/" in model or openrouter_key:
api_key = openrouter_key
if not api_key:
raise RuntimeError("Missing OPENROUTER_API_KEY (required for OpenRouter models)")
elif openai_key:
api_key = openai_key
else:
raise RuntimeError("Missing API key. Set either OPENAI_API_KEY or OPENROUTER_API_KEY")
out = Path(outdir)
out.mkdir(parents=True, exist_ok=True)
xlsx_path = str(out / "investor_update.xlsx")
pptx_path = str(out / "investor_update.pptx")
plan_path = str(out / "plan.json")
raw_df = load_and_validate_csv(csv_path)
model_df = compute_model_df(raw_df)
xlsx_path, row_end = build_workbook_base(raw_df, model_df, xlsx_path)
allowed_ranges = build_allowed_ranges(model_df, row_end)
facts = compute_facts(model_df)
plan = generate_plan_with_llm(
api_key=api_key,
model=model,
goal=goal,
allowed_ranges=allowed_ranges,
facts=facts,
num_slides=num_slides,
num_charts=num_charts,
)
with open(plan_path, "w", encoding="utf-8") as f:
json.dump(plan.model_dump(), f, indent=2)
add_charts_to_workbook(xlsx_path, plan.charts, allowed_ranges)
build_pptx(pptx_path, model_df, plan)
logs = (
f"CSV rows: {len(raw_df)}\n"
f"Excel: {xlsx_path}\n"
f"PPTX: {pptx_path}\n"
f"Plan: {plan_path}\n"
f"Facts used: {facts}\n"
)
return RunOutputs(xlsx_path=xlsx_path, pptx_path=pptx_path, plan_path=plan_path, logs=logs)
Lass uns alle Teile unserer Pipeline Schritt für Schritt zusammenfügen:
- Strukturierte Pipeline-Ausgaben: Die Funktion „
@dataclass” macht einen leichten Container für „xlsx_path”, „pptx_path”, „plan_path” und „logs”, damit die Streamlit-Benutzeroberfläche das Pipeline-Ergebnis als einen einzigen strukturierten Wert behandeln kann, anstatt mit Tupeln oder globalen Variablen rumzuspielen. - API-Schlüssel-Weiterleitung: Die Methode „
load_dotenv()“ lädt unsere Datei „.env“ in Umgebungsvariablen, sodass „OPENAI_API_KEY“ und „OPENROUTER_API_KEY“ über „os.getenv()“ gelesen werden können. Dann suchen wir den Anbieterschlüssel nach einer einfachen Regel aus: WennOPENROUTER_API_KEYda ist, nehmen wir OpenRouter; wenn nicht, greifen wir auf OpenAI zurück, falls verfügbar. - Mach die Datenvorbereitung: Wir laden und überprüfen die CSV-Datei, erstellen unsere abgeleitete Tabelle und machen dann das Excel-Grundgerüst über „
build_workbook_base()“ (Excel-Tabelle erstellen). Die Funktion „generate_plan_with_llm()“ gibt dann ein „ArtifactPlan“ zurück, das mit „json.dump()“ als JSON gespeichert wird, sodass es einfach zu überprüfen und zu debuggen ist. - Rendere die Artefakte: Die Funktion „add_charts_to_workbook()” macht dann die Excel-Diagramme im Blatt „Charts” sichtbar, und „
build_pptx()” erstellt die Präsentation aus dem Plan und der Tabelle.
Nach diesem Schritt kann unsere Streamlit-App run_pipeline() aufrufen, die logs anzeigen und drei festgelegte Ausgaben zum Download bereitstellen, darunter die Excel-Arbeitsmappe, die PPTX-Präsentation und den JSON-Plan.
Schritt 10: Streamlit-Benutzeroberfläche
Jetzt, wo die Kernpipeline fertig ist, geht's darum, sie in eine Streamlit-App zu packen, damit jeder eine Datei hochladen, ein Ziel festlegen, ein Modell auswählen und die erstellten Artefakte runterladen kann.
load_dotenv()
st.set_page_config(
page_title="GPT-5.2 PPT and Excel Generator",
page_icon="",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1f77b4;
margin-bottom: 1rem;
}
.sub-header {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.success-box {
padding: 1rem;
background-color: #d4edda;
border-left: 4px solid #28a745;
margin: 1rem 0;
}
.error-box {
padding: 1rem;
background-color: #f8d7da;
border-left: 4px solid #dc3545;
margin: 1rem 0;
}
.info-box {
padding: 1rem;
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
margin: 1rem 0;
}
.slide-preview {
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
""", unsafe_allow_html=True)
def main():
try:
if 'outputs' not in st.session_state:
st.session_state.outputs = None
if 'generated' not in st.session_state:
st.session_state.generated = False
st.markdown('<div class="main-header">Spreadsheet to PowerPoint Generator</div>', unsafe_allow_html=True)
with st.sidebar:
# API Provider selection
api_provider = st.selectbox(
"API Provider",
["OpenAI", "OpenRouter"],
help="Select which API to use. Make sure the corresponding API key is set in your .env file"
)
# Model selection based on provider
if api_provider == "OpenAI":
model = st.selectbox(
"AI Model",
[
"gpt-4o",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
],
help="OpenAI models. Requires OPENAI_API_KEY in .env file"
)
else: # OpenRouter
model = st.selectbox(
"AI Model",
[
"openai/gpt-5.2",
"openai/gpt-4-turbo",
"openai/gpt-4o",
"anthropic/claude-3-opus",
"anthropic/claude-3-sonnet",
],
help="OpenRouter models. Requires OPENROUTER_API_KEY in .env file"
)
with st.expander("Advanced Settings"):
num_slides = st.slider("Number of Slides", 4, 10, 6)
num_charts = st.slider("Number of Charts", 3, 6, 4)
col1, col2 = st.columns([1, 1])
with col1:
st.markdown("### Input")
uploaded_file = st.file_uploader(
"Upload CSV",
type=['csv'],
help="Upload any CSV file. The app will automatically detect columns and create appropriate charts and slides."
)
use_example = st.checkbox("Use example data (kpis.csv)", value=True if not uploaded_file else False)
if uploaded_file or use_example:
if use_example:
csv_path = "kpis.csv"
df = pd.read_csv(csv_path)
else:
df = pd.read_csv(uploaded_file)
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w') as f:
df.to_csv(f, index=False)
csv_path = f.name
with st.expander(" Preview Data", expanded=False):
try:
st.dataframe(df, width='stretch')
except Exception as e:
st.write("**Data Preview:**")
st.table(df.head(20))
if len(df) > 20:
st.caption(f"Showing first 20 of {len(df)} rows")
st.caption(f"Rows: {len(df)} | Columns: {len(df.columns)}")
else:
csv_path = None
st.info("Upload a CSV or use example data to continue")
goal = st.text_area(
"Goal",
value="Create a 6-slide investor update for Apr–Sep 2025. Focus on growth, retention, unit economics, and runway.",
height=100,
help="Describe what you want the AI to create"
)
generate_btn = st.button("Generate Artifacts", type="primary", use_container_width=True)
with col2:
st.markdown("### Output")
status_container = st.empty()
progress_bar = st.progress(0)
if not generate_btn:
status_container.info("Configure settings and click 'Generate Artifacts'")
if generate_btn:
if not csv_path:
st.error("Please upload a CSV or select 'Use example data'")
st.stop()
try:
status_container.info("Processing... This may take 30-60 seconds")
progress_bar.progress(10)
output_dir = Path("out")
output_dir.mkdir(exist_ok=True)
progress_bar.progress(20)
with st.spinner("Generating artifacts..."):
outputs: RunOutputs = run_pipeline(
csv_path=csv_path,
goal=goal,
outdir=str(output_dir),
model=model,
num_slides=num_slides,
num_charts=num_charts
)
st.session_state.outputs = outputs
st.session_state.generated = True
progress_bar.progress(100)
except Exception as e:
status_container.markdown(
f'<div class="error-box"> <b>Error:</b> {str(e)}</div>',
unsafe_allow_html=True
)
st.exception(e)
st.session_state.generated = False
if st.session_state.generated and st.session_state.outputs:
outputs = st.session_state.outputs
try:
if "Error" not in outputs.logs:
status_container.markdown(
'<div class="success-box"><b>Success!</b> Artifacts generated successfully</div>',
unsafe_allow_html=True
)
tab1, tab2, tab3, tab4 = st.tabs(["PowerPoint", "Excel", "JSON Plan", "Logs"])
with tab1:
st.markdown("### PowerPoint Slides")
pptx_path = Path(outputs.pptx_path)
if pptx_path.exists():
try:
prs = Presentation(str(pptx_path))
slide_count = len(prs.slides)
except:
slide_count = "unknown"
with open(pptx_path, "rb") as f:
st.download_button(
label="Download PowerPoint",
data=f.read(),
file_name="investor_update.pptx",
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation",
width='stretch',
key="pptx_download"
)
st.markdown("---")
col_info1, col_info2 = st.columns(2)
with col_info1:
st.metric("Slides", slide_count)
with col_info2:
file_size = pptx_path.stat().st_size / 1024 # KB
st.metric("File Size", f"{file_size:.1f} KB")
else:
st.error("PowerPoint file not found")
with tab2:
st.markdown("### Excel Workbook")
xlsx_path = Path(outputs.xlsx_path)
if xlsx_path.exists():
with open(xlsx_path, "rb") as f:
st.download_button(
label="Download Excel",
data=f.read(),
file_name="investor_update.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
width='stretch',
key="xlsx_download"
)
wb = openpyxl.load_workbook(xlsx_path)
st.markdown("#### Data Preview")
ws = wb["Model"]
data = []
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=2, values_only=True):
data.append(row)
df_preview = pd.DataFrame(data, columns=headers)
try:
st.dataframe(df_preview.head(10), width='stretch')
except Exception:
st.table(df_preview.head(10))
if len(df_preview) > 10:
st.caption(f"Showing first 10 of {len(df_preview)} rows")
else:
st.error("Excel file not found")
with tab3:
st.markdown("### Schema-Locked JSON Plan")
plan_path = Path(outputs.plan_path)
if plan_path.exists():
with open(plan_path, "r") as f:
plan_data = json.load(f)
st.download_button(
label="Download JSON",
data=json.dumps(plan_data, indent=2),
file_name="plan.json",
mime="application/json",
key="json_download"
)
st.markdown("---")
st.json(plan_data)
col_a, col_b = st.columns(2)
with col_a:
st.metric("Charts Generated", len(plan_data.get("charts", [])))
with col_b:
st.metric("Slides Generated", len(plan_data.get("slides", [])))
else:
st.error("JSON plan not found")
with tab4:
st.markdown("### Generation Logs")
st.code(outputs.logs, language="text")
else:
status_container.markdown(
f'<div class="error-box"> <b>Error occurred</b><br>{outputs.logs}</div>',
unsafe_allow_html=True
)
except Exception as e:
st.error(f"Error displaying results: {str(e)}")
st.exception(e)
except Exception as e:
st.error(f"Fatal Error: {str(e)}")
st.exception(e)
st.info("Try installing dependencies: pip install -r requirements.txt")
if __name__ == "__main__":
try:
main()
except Exception as e:
import sys
print(f"Fatal error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
In der Streamlit-Ebene haben wir einen App-Rahmen mit „ st.set_page_config( layout="wide") “ eingerichtet und dann den Bildschirm mit „ st.columns() “ geteilt, sodass auf der linken Seite Eingaben gesammelt werden, während auf der rechten Seite die Steuerelemente für die Ausführung und die Ausgaben angezeigt werden. Die Pipeline läuft nur, wenn der Benutzer auf „ st.button() “ klickt. Dabei checken wir erst mal, ob Uploads fehlen, und packen „ st.spinner() “ rein, damit die Benutzeroberfläche nicht eingefroren aussieht. Dann schicken wir die Artefakte mit „ st.download_button() “ für die Excel-, PowerPoint- und Plan-Datei zurück an den Benutzer.
Nachdem wir diesen Schritt erledigt haben, speichern wir alles als „ app.py ” und starten das ganze Programm mit:
streamlit run app.pyFazit
In diesem Tutorial haben wir eine komplette Pipeline „CSV zu Artefakten” gebaut, die GPT-5.2 nutzt, um einen Plan mit Diagrammen, Folienstruktur und Zellbereichszitaten zu erstellen. Wir checken dann diesen Plan, bevor wir irgendwas machen. Dieser Ansatz „erst planen, dann rendern“ passt super zu den Stärken von GPT-5.2 bei mehrstufigen Arbeitsabläufen wie Tabellenkalkulationen und Präsentationen.
Das Ergebnis ist ein wiederholbarer Arbeitsablauf, bei dem der Benutzer eine CSV-Datei hochlädt, der Code eine saubere Arbeitsmappenbasis erstellt, GPT-5.2 einen Plan erstellt, der nur zulässige Excel-Bereiche zitieren kann, und der Renderer Diagramme und Folien mit openpyxl und python-pptx rendert.
Von hier aus kannst du die Demo erweitern, ohne die Kernlogik zu ändern, z. B. durch Hinzufügen weiterer Diagrammtypen, Einführen von Folienvorlagen, Unterstützung von Multi-CSV-Verbindungen usw. Das Grundmuster bleibt dasselbe, d. h. das Modell wird wie ein Planer behandelt, der einem Schema folgen muss, und die ganze Rendering-Logik bleibt im Code, damit die Ergebnisse konsistent und debugbar bleiben.

Ich bin Google Developers Expertin für ML (Gen AI), dreifache Kaggle-Expertin und Women-Techmakers-Botschafterin mit über drei Jahren Erfahrung in der Tech-Branche. 2020 habe ich ein Health-Tech-Startup mitgegründet und absolviere derzeit einen Master in Informatik an der Georgia Tech mit Schwerpunkt Machine Learning.