Weiter zum Inhalt

GPT-5.2-Projekt: Excel- und PowerPoint-Generator erstellen

Lerne, wie du mit GPT‑5.2 jede CSV in eine Excel‑Arbeitsmappe und ein PowerPoint‑Deck mit belastbaren Quellen verwandelst – direkt in einer Streamlit‑App.
Aktualisiert 12. Mai 2026  · 13 Min. lesen

GPT-5.2 ist ein starkes Modell für Workflows und mehrstufige Pipelines, bei denen es auf Zuverlässigkeit, Struktur und weniger Halluzinationen ankommt. OpenAI hebt Verbesserungen bei Langkontext-Aufgaben und eine noch bessere Performance bei tabellenartigen Tasks hervor.

In diesem Tutorial bauen wir eine Streamlit-App, die sich wie ein Junior-Analyst verhält:

  • Lade eine CSV hoch und formuliere ein Ziel (zum Beispiel: „6-Folien-Update mit Fokus auf Wachstum, Retention, Unit Economics, Runway“)
  • Erzeuge einen schemafixierten JSON-Plan
  • Erstelle eine Excel-Arbeitsmappe mit Rohdaten, abgeleiteten Kennzahlen und Diagrammen
  • Erzeuge anschließend ein passendes PowerPoint-Deck mit korrekten Quellenangaben

Der Schlüssel ist: GPT-5.2 „macht“ keine Folien. Es erzeugt einen strikten Plan, den dein Code deterministisch ausführt.

Lies auch unsere Guides zu OpenAIs neueren Modellen, GPT-5.5, GPT-5.4, GPT-5.3 Codex und GPT-5.3 Instant

Was ist GPT-5.2?

GPT-5.2 ist OpenAIs neuestes GPT‑5‑Serien-Modell mit Verbesserungen für End-to-End-Aufgaben wie Tabellenkalkulationen, Präsentationen, Code schreiben und debuggen, Langkontext-Verständnis und Tool-Nutzung. 

Das sind die wichtigsten Eigenschaften von GPT-5.2 für dieses Projekt:

  • Langkontext-Reasoning: GPT-5.2 Thinking gilt laut OpenAI (MRCRv2) als State of the Art bei Langkontext-Reasoning, mit praktischen Vorteilen für konsistente Arbeit über sehr lange Dokumente und multifile Inputs.
  • Mehrschritt-Ausführung: Das Modell glänzt bei komplexen, mehrstufigen Projekten, inklusive stärkerem Tool-Calling und Long-Horizon-Verhalten – perfekt für unseren schemafixierten Planungs- und deterministischen Rendering-Loop.
  • Verbesserte Faktentreue: GPT-5.2 Thinking produziert weniger fehlerhafte Antworten als GPT-5.1 Thinking. Das hilft, damit Texte/Zahlen nicht von den zitierten Excel-Bereichen abweichen.
  • Latenz-Trade-offs: In der API kannst du für schnelles Feedback mit GPT-5.2 Instant iterieren und für starkes End-to-End-Planning und hochwertige Artefakte auf GPT-5.2 Thinking oder Pro umschalten. Bedenke: Komplexere Generationen können Minuten dauern.

GPT-5.2-Beispielprojekt: Generator für strukturierte Artefakte bauen

In diesem Abschnitt bauen wir einen Generator für strukturierte Artefakte (Excel und PowerPoint) mit dem GPT‑5.2‑Modell in einer Streamlit-App. Auf hoher Ebene macht die App Folgendes:

  1. Sie liest eine CSV und erstellt eine kanonische Arbeitsmappe

  2. Dann ruft sie GPT-5.2 über OpenAI oder OpenRouter mit Structured Outputs auf (response_format: json_schema)

  3. Validiert den zurückgegebenen Plan gegen unser Schema

  4. Rendert Diagramme in Excel und stylt sie in unserer Markenpalette

  5. Erstellt schließlich ein PowerPoint mit eingebetteten Diagrammen und Quellenangaben

Final demo

Lass uns das Schritt für Schritt bauen.

Schritt 1: Voraussetzungen und Setup

Bevor wir mit GPT-5.2 Excel-Arbeitsmappen und PowerPoint-Decks generieren, richten wir eine lokale Umgebung ein. Wir starten mit der Installation der Kernbibliotheken und setzen API-Schlüssel als Umgebungsvariablen.

pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx

Wir nutzen Streamlit für das interaktive Web-UI, Pandas zum Laden und Vorbereiten der CSV, Pydantic zur Validierung des schemafixierten JSON-Plans von GPT‑5.2. Außerdem verwenden wir python-dotenv, um Schlüssel wie OPENROUTER_API_KEY/OPENAI_API_KEY sicher aus einer .env-Datei zu laden. Die requests-Bibliothek ruft den OpenRouter-/OpenAI-kompatiblen Endpunkt auf, openpyxl erzeugt die Excel-Arbeitsmappe und Diagramme und python-pptx erstellt das PowerPoint-Deck mit nativen (editierbaren) Charts. 

Als Nächstes setzen wir unsere API-Schlüssel:

export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"

GPT‑5.2 ist über mehrere Dienste erreichbar, darunter die offizielle OpenAI‑API sowie Services wie OpenRouter und weitere. 

Empfohlen ist eine .env-Datei, um API-Schlüssel lokal zu speichern:

Speichere darin Folgendes:

OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"

Lade die Schlüssel dann mit der load_dotenv-Bibliothek:

from dotenv import load_dotenv
load_dotenv()

Damit ist unsere Umgebung für die Authentifizierung bei der OpenAI‑API bereit.

Schritt 2: Das Planschema definieren

Als Nächstes definieren wir einen schemafixierten Artefakt-Plan: eine strikte JSON‑Blaupause, die GPT‑5.2 exakt vorgibt, welche Diagramme zu erstellen sind und wie die Folien zu strukturieren sind, damit die Modellausgabe mit Structured Outputs über response_format: { type: "json_schema", strict: true } statt als Freitext dekodiert und validiert 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 Code definiert den Plan mit Pydantic‑Modellen und erfüllt dabei drei zentrale Aufgaben:

  • Planstruktur: ChartKind = Literal["line", "bar"] beschränkt Diagrammtypen auf ein kleines Enum, ChartSpec erfasst den vollständigen Chart‑Vertrag und SlideSpec definiert das Folienlayout und verknüpft optional über chart_id eine Folie mit einem Diagramm.

  • Automatische Qualitätskontrolle: Constraints wie Field() stellen sicher, dass wir genug Charts/Folien bekommen, jede Folie genügend Bullets hat und jede Bullet Quellenangaben enthält.

  • Planung und Rendering: GPT‑5.2 liefert nur die Blaupause – also Charts, Folien und Zitate – als strukturiertes JSON. Der restliche Code übernimmt Ausführung, Excel‑Formatierung, Chartrendering und Deck‑Layout. 

Mit diesem Schema wird GPT‑5.2 zum Planer, der eine validierte JSON‑Blaupause produziert.

Schritt 3: GPT‑5.2 mit Structured Outputs aufrufen

Mit dem definierten Planschema folgt der nächste Schritt: Rufe GPT‑5.2 im „Schema‑Modus“ auf – so kommt eine JSON‑Blaupause zurück, die du validieren und ausführen kannst. Statt zu hoffen, dass sich das Modell „korrekt verhält“, sagst du der API, direkt in dein JSON‑Schema zu dekodieren – mit response_format: { type: "json_schema", ... strict: true }. Genau dafür sind Structured Outputs 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 zentrale Gateway, das Prompts in einen validierten Plan verwandelt:

  1. Provider‑Routing (OpenRouter vs. OpenAI): Wenn der Modellname einen Namespace nutzt (openai/gpt-5.2), nimmst du standardmäßig OpenRouter, da dieses Format dort fürs Model‑Routing verwendet wird. Setzt du api_provider="openai", sendest du dieselbe Chat Completions‑Payload an OpenAIs Endpunkt. Du kannst OpenRouter auch komplett weglassen.

  2. Endpoints setzen: Für OpenAI rufst du https://api.openai.com/v1/chat/completions mit Bearer‑Auth auf. Für OpenRouter nutzt du https://openrouter.ai/api/v1/chat/completions und kannst optionale Attributions‑Header wie HTTP‑Referer und X‑Title mitsenden, wie von OpenRouter empfohlen.

  3. Schema‑Decoding via response_format: Der Schlüsselblock ist response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }. Damit wird das Modell angewiesen, die Ausgabe strikt auf unser JSON‑Schema zu beschränken.

Als Nächstes validieren wir den JSON‑Plan des Modells mit Pydantic und fahren nur fort, wenn alle Grenzen eingehalten werden. Dann rendern wir die Excel‑Diagramme und erzeugen das PowerPoint‑Deck.

Schritt 4: Helferfunktionen

Bevor wir etwas rendern, brauchen wir eine dünne Schicht, die jede hochgeladene CSV analysierbar macht und konsistente Referenzen ermöglicht. Diese Helfer erledigen vier Aufgaben: CSV laden und bereinigen, eine abgeleitete Modelltabelle erstellen, kompakte Fakten für den Prompt berechnen und Excel‑Ausgaben per Auto‑Sizing besser lesbar machen.

Schritt 4.1: CSV laden und validieren

GPT‑5.2 kann Diagramme und Folien nicht verlässlich planen, wenn die Eingabetabelle leer ist oder numerische Felder als Strings vorliegen. Diese Funktion standardisiert den Datensatz, sodass nachgelagerte Schritte nicht raten müssen.

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 Funktion:

  • Liest die Datei mit pd.read_csv in ein DataFrame ein.
  • Erkennt Spalten per Namensheuristiken (date, month, week, period) und erzwingt Strings. So bleiben Labels in Charts und Folientext stabil.
  • Konvertiert Nicht-Datumsspalten zuletzt mit pd.to_numeric in numerische Typen und füllt fehlende Werte mit 0, um spätere NaN‑Kaskaden zu vermeiden. 

Am Ende hast du ein nicht leeres DataFrame mit numerischen Spalten.

Schritt 4.2: Das Modell‑DataFrame berechnen

Roh‑CSVs sind unordentlich und je nach Domäne inkonsistent. Das Modell‑Sheet dient als Normalisierungsschicht, in der wir nützliche abgeleitete Kennzahlen fürs Charting ergänzen, ohne dass Nutzer sie manuell berechnen müssen.

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() leistet zwei besonders nützliche Dinge:

  • Sie sucht eine primäre Output‑Spalte und eine sekundäre Input‑Spalte und bildet eine einfache Ratio, um festzuhalten, wie viel vom Primärsignal nach Berücksichtigung des Sekundärsignals übrig bleibt.

  • Für einige numerische Spalten berechnet sie pct_change() – die Standardänderung zur Vorzeile – als einfache Basis, um Bewegungen in Zeitreihen zu erfassen.

Schritt 4.3: Fakten berechnen

Auch mit Structured Outputs wollen wir GPT‑5.2 eine kompakte Zusammenfassung der Veränderungen im Zeitraum geben. Diese Fakten dienen zugleich als Prompt, den das Modell in Folienbullets 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

Wir greifen auf erste und letzte Zeile zu, um das Periodenfenster zu definieren, und wählen eine Spalte (date, month, week, period) für eine lesbare Berichterstattung.

Für einige numerische Spalten berechnen wir Start, Ende, Durchschnitt und die prozentuale Veränderung von Start zu Ende. So entsteht ein schlankes Signal‑Dictionary, das dem Modell beim Schreiben der Bullets hilft.

Schritt 4.4: Excel‑Spalten automatisch anpassen

Standard‑Spaltenbreiten in Excel wirken roh. Autosizing ist ein einfacher Feinschliff, der die Nutzbarkeit sofort erhöht.

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() scannt jede Spalte, ermittelt die maximale Zeichenlänge und setzt ws.column_dimensions[col_letter].width. OpenPyXL stellt dazu column_dimensions und die Eigenschaft width bereit, um Spaltenbreiten in Excel zu steuern. 

Jetzt speisen wir die bereinigte Modelltabelle und die Fakten in den GPT‑5.2‑Planungsprompt ein, validieren den Plan und rendern dann Charts und Folien.

Schritt 5: Die Arbeitsmappe erstellen 

Mit der bereinigten Modelltabelle und den kompakten Fakten aus dem vorherigen Schritt erzeugen wir jetzt eine deterministische Excel‑Arbeitsmappe mit openpyxl.

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

Das macht der Workbook‑Builder:

  • Gerüst erstellen: Workbook() erzeugt eine neue Arbeitsmappe, wb.remove(wb.active) entfernt das Default‑Sheet. Neue Blätter fügst du explizit mit wb.create_sheet("Raw"), wb.create_sheet("Model") und wb.create_sheet("Charts") hinzu.

  • „Raw“-Sheet schreiben: Kopfzeile mit Font, PatternFill und Alignment, dann alle Zeilen aus raw_df. autosize_columns(ws_raw) erhöht die Lesbarkeit ohne manuelles Ziehen.

  • Grenzen berechnen: Mit row_end = 1 + len(raw_df) bekommen wir eine stabile letzte Zeile für Bereiche wie A2:A{row_end} – für Kategorien und Zitate.

  • Zahlenformat‑Heuristiken: Wir setzen cell.number_format anhand von Spaltennamen‑Keywords und dtypes, damit Excel Beträge, Prozente oder Zahlen korrekt darstellt. 

  • „Charts“-Sheet erstellen: Mit wb.create_sheet("Charts") erhalten wir eine stabile Fläche für Chart‑Anker. Das passt zum openpyxl‑Workflow: LineChart oder BarChart erstellen, Bereiche via Reference binden, Kategorien mit set_categories setzen und mit ws.add_chart() platzieren.

Nach diesem Schritt hast du eine konsistente Excel‑Datei, auf die der nächste Schritt mit exakten Zellbereichen verlässlich zugreifen kann.
Schritt 6: Diagramme rendern 

Jetzt, da die Arbeitsmappe steht, rendern wir die Diagramme ins „Charts“-Sheet und wenden einen konsistenten Markenlook an. Ich habe DataCamps Farbpalette verwendet:

  • 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 Chart‑Engine, die unseren Schema‑Plan in gebrandete Excel‑Visuals umsetzt.

  • Arbeitsmappe laden: Wir öffnen die erzeugte Excel‑Datei mit load_workbook(xlsx_path) und bauen aus der ersten Zeile eine header_map, die Spaltennamen ihren Indizes zuordnet. 

  • Zellbereich‑Referenzen erstellen: Reference() definiert den exakten Plot‑Bereich und chart.set_categories(x_ref) verbindet die X‑Achsen‑Kategorien. Das Muster: Referenzen erstellen, Daten hinzufügen, Kategorien setzen. 

  • Diagrammtyp instanziieren: Pro Spezifikation erstellen wir LineChart() oder BarChart() und setzen chart.title

  • Markenpalette anwenden: Für jede Serie binden wir den Y‑Bereich mit chart.add_data() und setzen das Label via SeriesLabel()

Abschließend platziert ws_charts.add_chart() jedes Diagramm an einer definierten linken oberen Zelle. 

Schritt 7: PowerPoint rendern

Jetzt wird aus dem Plan ein echtes Foliendeck. Wir erstellen eine PowerPoint, setzen Titel und Bullets, betten Diagramme aus derselben Tabelle ein und fügen eine Fußzeile mit den Zellbereich‑Zitaten des Plans hinzu.

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 Code schrittweise an:

  • Deck erstellen und Charts indexieren: prs = Presentation() startet ein neues PPTX, blank = prs.slide_layouts[6] liefert eine leere Folie für präzises Platzieren.

  • Titel und Bullets: add_title() schreibt in ein Textfeld text_frame, add_bullets() fügt pro Bullet einen Absatz hinzu und steuert Schriftgröße und Abstände.

  • Quellen‑Footer: add_sources_footer() dedupliziert Zitate über Bullets hinweg und rendert eine kompakte Sources:-Zeile unten – für Nachvollziehbarkeit ohne visuelles Rauschen.

  • Diagramme einbetten: add_chart() baut ein CategoryChartData() mit einer zeitähnlichen X‑Spalte und den geplanten Reihenwerten und fügt es via slide.shapes.add_chart() als GraphicFrame ein.

  • Brand‑Styling: Wir wandeln Hex in RGBColor und wenden es pro Serie an. 

  • Layoutregeln und Leitplanken: Hat eine Folie eine gültige chart_id, nutzen wir ein Zweispalten‑Layout, sonst Vollbreite für Bullets. build_allowed_ranges() erstellt die Allow‑List zitierbarer Bereiche; validate_plan() erzwingt eindeutige Chart‑IDs, gültige Referenzen, erlaubte Zitate und gültige Metriken, damit das Modell keine Felder erfindet.

Schritt 8: Plan mit dem LLM erzeugen

Jetzt verwandeln wir die sauberen Tabellendaten in einen Schema‑Bauplan (Charts, Folien, Zitate), 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 Gehirn dieser Demo. So binden wir sie ein:

  • Explizites JSON‑Schema: plan_schema definiert die einzig erlaubte Antwortform mit charts[] und slides[] inklusive Pflichtfeldern und Enums.

  • System‑Prompt‑Leitplanken: Nicht verhandelbare Regeln sorgen dafür, dass der Plan renderbar und prüfbar bleibt. Außerdem beschränken wir Chart‑Anker auf reine Zellen, damit das Layout deterministisch bleibt. Solche Mehrschritt‑Kontrollen spielt GPT‑5.2 seine Stärken aus.

  • User‑Nachricht: Ziel, geforderte Anzahlen, berechnete Fakten und erlaubte Metrik‑Keys. Da das Modell nur das sieht, sinkt das Risiko, Spalten, Bereiche oder Behauptungen zu erfinden.

  • Schema‑konformes Decoding: chat_json_schema() wird mit response_format = json_schema und strict: true aufgerufen, wodurch die Schemaeinhaltung bereits beim Dekodieren erzwungen wird.

  • Validierung in zwei Schichten: Zuerst prüft ArtifactPlan.model_validate(plan_json) die Übereinstimmung mit den Pydantic‑Modellen. Danach erzwingt validate_plan() Domänenregeln.

Schritt 9: Pipeline ausführen

Zum Schluss verdrahten wir CSV‑Laden, Excel‑Erzeugung, LLM‑Planung, Chartrendering und finalen PPTX‑Export und geben 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)

So fügen sich die Komponenten zusammen:

  • Strukturierte Pipeline‑Outputs: Das @dataclass kapselt xlsx_path, pptx_path, plan_path und logs, damit das Streamlit‑UI ein einziges strukturiertes Ergebnisobjekt erhält.
  • API‑Key‑Routing: load_dotenv() lädt unsere .env in Umgebungsvariablen, sodass OPENAI_API_KEY und OPENROUTER_API_KEY via os.getenv() verfügbar sind. Dann wählen wir den Provider‑Key nach einer einfachen Regel: Wenn OPENROUTER_API_KEY existiert, nutze OpenRouter, sonst OpenAI, falls vorhanden.
  • Datenvorbereitung ausführen: Wir laden und validieren die CSV, berechnen die abgeleitete Modelltabelle und erzeugen das Excel‑Gerüst via build_workbook_base(). generate_plan_with_llm() liefert den ArtifactPlan, den wir mit json.dump() speichern – ideal zum Prüfen und Debuggen.
  • Artefakte rendern: add_charts_to_workbook() materialisiert Diagramme im Charts‑Sheet und build_pptx() baut das Deck aus Plan und Tabelle.

Im Anschluss kann unsere Streamlit‑App run_pipeline() aufrufen, die logs anzeigen und drei deterministische Downloads anbieten: Excel‑Arbeitsmappe, PPTX‑Deck und JSON‑Plan. 

Schritt 10: Streamlit‑UI

Jetzt verpacken wir die Pipeline in eine Streamlit‑App, damit jede Person eine Datei hochladen, ein Ziel setzen, ein Modell wählen und die Artefakte herunterladen 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‑Schicht setzen wir mit st.set_page_config(layout="wide") den Rahmen, teilen das Layout mit st.columns() in Eingaben links und Steuerung/Outputs rechts. Die Pipeline läuft erst nach Klick auf st.button(): Wir prüfen Uploads, zeigen einen st.spinner() gegen Freeze‑Gefühl und liefern die Artefakte mit st.download_button() für Excel, PowerPoint und Plan aus.

Zum Abschluss speichern wir alles als app.py und starten die Experience mit:

streamlit run app.py

Fazit

In diesem Tutorial haben wir eine vollständige „CSV‑zu‑Artefakte“-Pipeline gebaut, die GPT‑5.2 nutzt, um einen Plan inklusive Diagrammen, Folienstruktur und Zellbereich‑Zitaten zu liefern. Wir validieren den Plan, bevor wir etwas generieren. Dieser „Planen, dann rendern“-Ansatz passt ideal zu GPT‑5.2s Stärken bei mehrstufigen Workflows wie Tabellen und Präsentationen.

Das Ergebnis ist ein wiederholbarer Workflow: Nutzende laden eine CSV hoch, der Code baut ein sauberes Workbook‑Fundament, GPT‑5.2 erzeugt einen Plan, der nur freigegebene Excel‑Bereiche zitieren darf, und der Renderer materialisiert Charts und Folien mit openpyxl und python‑pptx. 

Darauf aufbauend kannst du das Demo erweitern, ohne die Kernlogik zu ändern – etwa mehr Diagrammtypen, Folienschablonen oder Multi‑CSV‑Joins. Das Muster bleibt gleich: Behandle das Modell als Planer, der ein Schema befolgen muss, und halte das Rendering im Code, damit Ausgaben konsistent und debug‑freundlich bleiben.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

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.

FAQs

Warum GPT‑5.2 mit Streamlit kombinieren?

Streamlit macht es leicht, einen KI‑Workflow in eine interaktive App zu verwandeln, ohne ein komplettes Frontend zu bauen. In Kombination mit GPT‑5.2 können Developer schnell vom Experiment zur nutzbaren Lösung wechseln, die Eingaben annimmt, einen modellgetriebenen Prozess ausführt und greifbare Outputs liefert.

Was sind Structured Outputs und warum sind sie hier wichtig?

Structured Outputs erzwingen, dass das Modell Daten exakt nach einem vordefinierten JSON‑Schema zurückgibt. So folgen Charts, Folien, Kennzahlen und Zitate strikten Regeln – der Plan lässt sich validieren, bevor Excel‑ oder PowerPoint‑Dateien erzeugt werden.

Brauche ich tiefes Frontend‑Know-how für solche Apps?

Nein. Streamlit ist so konzipiert, dass die Hauptarbeit in Python mit vertrauter Control‑Flow‑Logik und Bibliotheken passiert. Das senkt die Hürde für Data Scientists und Analysten, KI‑Tools zu bauen, ohne JavaScript oder komplexe Webframeworks lernen zu müssen.

Themen

Lerne mit DataCamp

Kurs

Prompt-Engineering mit der OpenAI-API

4 Std.
48K
Tauche tief in die Prinzipien und Best Practices des Prompt Engineering ein, um leistungsstarke Sprachmodelle wie ChatGPT zur Lösung realer Probleme zu nutzen.
Details anzeigenRight Arrow
Kurs starten
Mehr anzeigenRight Arrow
Verwandt

Blog

Die 36 wichtigsten Fragen und Antworten zum Thema generative KI für 2026

Dieser Blog hat eine ganze Reihe von Fragen und Antworten zu generativer KI, von den Grundlagen bis hin zu fortgeschrittenen Themen.
Hesam Sheikh Hassani's photo

Hesam Sheikh Hassani

15 Min.

Tutorial

Python Datenstrukturen Tutorial

Mach dich mit Python-Datenstrukturen vertraut: Lerne mehr über Datentypen und primitive sowie nicht-primitive Datenstrukturen wie Strings, Listen, Stapel usw.
Sejal Jaiswal's photo

Sejal Jaiswal

Tutorial

Wie man Listen in Python aufteilt: Einfache Beispiele und fortgeschrittene Methoden

Lerne, wie du Python-Listen mit Techniken wie Slicing, List Comprehensions und itertools aufteilen kannst. Finde heraus, wann du welche Methode für die beste Datenverarbeitung nutzen solltest.
Allan Ouko's photo

Allan Ouko

Tutorial

Python Switch Case Statement: Ein Leitfaden für Anfänger

Erforsche Pythons match-case: eine Anleitung zu seiner Syntax, Anwendungen in Data Science und ML sowie eine vergleichende Analyse mit dem traditionellen switch-case.
Matt Crabtree's photo

Matt Crabtree

Tutorial

Python-Arrays

Python-Arrays mit Code-Beispielen. Lerne noch heute, wie du mit Python NumPy Arrays erstellen und ausdrucken kannst!
DataCamp Team's photo

DataCamp Team

Tutorial

Loop-Schleifen in Python-Tutorial

Lerne, wie du For-Schleifen in Python umsetzt, um eine Sequenz oder die Zeilen und Spalten eines Pandas-DataFrame zu durchlaufen.
Aditya Sharma's photo

Aditya Sharma

Mehr anzeigenMehr anzeigen