Cursus
GPT-5.2 is een krachtig model voor het genereren van workflows en meerstaps-pijplijnen die betrouwbaarheid, structuur en minder hallucinaties vereisen. OpenAI heeft verbeteringen benadrukt in long-contextwerk en nog betere prestaties bij taken in spreadsheet-stijl.
In deze tutorial bouwen we een Streamlit-app die zich gedraagt als een junior analist:
- Upload een CSV en schrijf een doel (bijvoorbeeld: “update van 6 slides met focus op groei, retentie, unit economics, runway”)
- Genereer een schema-vergrendeld JSON-plan
- Produceer een Excel-werkmap met ruwe data, afgeleide metrics en grafieken
- Genereer tot slot een bijbehorende PowerPoint-deck met correcte citaties
Het belangrijkste hier is dat GPT-5.2 niet direct “slides maakt”. Het genereert een strikt plan dat je code deterministisch uitvoert.
Lees ook onze gidsen over de nieuwere modellen van OpenAI, GPT-5.5, GPT-5.4, GPT-5.3 Codex en GPT-5.3 Instant
Wat is GPT-5.2?
GPT-5.2 is OpenAI’s nieuwste GPT-5-serie-model met verbeteringen voor end-to-end taken zoals spreadsheets maken, presentaties bouwen, code schrijven en debuggen, lange contexten begrijpen en tools gebruiken.
Dit zijn de GPT-5.2-eigenschappen die voor dit project het belangrijkst zijn:
- Long-context redeneren: GPT-5.2 Thinking wordt omschreven als state of the art op long-context redeneringsevaluaties (OpenAI MRCRv2), met praktische implicaties voor coherent werken over zeer lange documenten en invoer met meerdere bestanden.
- Meerstaps-uitvoering: Dit model blinkt uit in complexe projecten met meerdere stappen, waaronder sterker tool-calling en long-horizon gedrag, wat direct aansluit op onze schema-vergrendelde planning en deterministische renderlus.
- Verbeterde factualiteit: GPT-5.2 Thinking geeft minder foutieve reacties dan GPT-5.1 Thinking, wat hier helpt omdat het de kans verkleint dat tekst/cijfers afwijken van onze geciteerde Excel-bereiken.
- Latentietrade-offs: In de API kun je itereren met GPT-5.2 Instant voor snelle feedback, en dan overschakelen naar GPT-5.2 Thinking of Pro wanneer je sterke end-to-end planning en artefacten van hogere kwaliteit wilt, met in het achterhoofd dat de complexere generaties minuten kunnen duren.
GPT-5.2 voorbeeldproject: bouw een Structured Artifacts Generator
In deze sectie bouwen we een generator voor gestructureerde artefacten (Excel en PowerPoint) met het GPT 5.2-model verpakt in een Streamlit-app. Op hoofdlijnen doet de uiteindelijke Streamlit-app dit:
-
Hij leest een CSV en bouwt een canonieke werkmap
-
Vervolgens roept hij GPT-5.2 aan via OpenAI of OpenRouter met Structured Outputs (
response_format: json_schema) -
Valideert het teruggestuurde plan tegen ons schema
-
Rendert grafieken in Excel en stijlt ze volgens ons merkpalet
-
Ten slotte genereert hij een PowerPoint met ingesloten grafieken en citaties

Laten we dit stap voor stap bouwen.
Stap 1: Vereisten en setup
Voordat we met GPT-5.2 Excel-werkboeken en PowerPoint-decks genereren, richten we een lokale omgeving in. We beginnen met het installeren van de kernbibliotheken en het instellen van API-sleutels als omgevingsvariabelen.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
We gebruiken Streamlit om de interactieve web-UI te bouwen, Pandas om de CSV te laden en voor te bereiden, Pydantic om het schema-vergrendelde JSON-plan van GPT-5.2 te valideren. We gebruiken ook python-dotenv om sleutels zoals OPENROUTER_API_KEY/OPENAI_API_KEY veilig te laden uit een .env-bestand. De requests-bibliotheek roept het OpenRouter/OpenAI-compatibele endpoint aan, terwijl openpyxl de Excel-werkmap en grafieken genereert, en python-pptx het PowerPoint-deck met native (bewerkbare) grafieken maakt.
Vervolgens stellen we onze API-sleutel(s) in:
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Let op: GPT 5.2 is beschikbaar via meerdere diensten, waaronder de officiële API van OpenAI en diensten zoals OpenRouter en meer.
Het is aan te raden een .env-bestand te gebruiken om API-sleutels lokaal op te slaan:
Sla in het .env-bestand het volgende op:
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
Laad deze API-sleutels daarna met de load_dotenv-bibliotheek:
from dotenv import load_dotenv
load_dotenv()
Op dit punt is onze omgeving klaar om te authenticeren met de OpenAI API.
Stap 2: Het planschema definiëren
Vervolgens definiëren we een schema-vergrendeld artefactplan dat bestaat uit een strikt JSON-blauwdruk die GPT-5.2 exact vertelt welke grafieken te maken en hoe de slides te structureren, zodat de output van het model kan worden gedecodeerd en gevalideerd met Structured Outputs via response_format: { type: "json_schema", strict: true } in plaats van vrije tekst.
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)
Bovenstaande code definieert het plan met Pydantic-modellen, die drie belangrijke dingen doen:
-
Planstructuur:
ChartKind = Literal["line", "bar"]beperkt grafiektypes tot een kleine enum,ChartSpeclegt het volledige grafiekcontract vast enSlideSpecdefinieert de slide-indeling terwijl een slide optioneel aan een grafiek wordt gekoppeld viachart_id. -
Automatische kwaliteitsborging: Beperkingen zoals
Field()zorgen ervoor dat we altijd genoeg grafieken/slides krijgen, elke slide genoeg bullets heeft en elke bullet citaties bevat. -
Planning en rendering: GPT-5.2 produceert alleen de blauwdruk, oftewel grafieken, slides en citaties, als gestructureerde JSON, terwijl de rest van de code de uitvoering, Excel-opmaak, grafiekrendering en decklay-out afhandelt.
Met dit schema fungeert GPT-5.2 als planner die een gevalideerde JSON-blauwdruk produceert.
Stap 3: GPT-5.2 aanroepen met structured outputs
Met het planschema gedefinieerd is de volgende stap om GPT-5.2 in “schema-modus” aan te roepen—zodat het een JSON-blauwdruk terugstuurt die je kunt valideren en uitvoeren. In plaats van te hopen dat het model zich “gedraagt”, vertellen we de API om direct te decoderen naar je JSON Schema met response_format: { type: "json_schema", ... strict: true }, precies waar Structured Outputs voor bedoeld zijn.
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
De functie chat_json_schema() is de enige gateway die prompts omzet in een gevalideerd plan:
-
Provider-routing (OpenRouter vs OpenAI): Als de modelnaam genamespaceerd is (openai/gpt-5.2), val je standaard terug op OpenRouter omdat dat het formaat is dat OpenRouter gebruikt voor modelrouting. Als je expliciet api_provider="openai" instelt, stuur je dezelfde Chat Completions-payload naar het endpoint van OpenAI. Je kunt OpenRouter gerust overslaan.
-
Endpoints instellen: Voor OpenAI roep je het endpoint
https://api.openai.com/v1/chat/completionsaan met standaard Bearer-auth. Voor OpenRouter roep je het endpointhttps://openrouter.ai/api/v1/chat/completionsaan en kun je optioneel attributie-headers zoals HTTP-Referer en X-Title meesturen, die OpenRouter als aanbevolen metadata documenteert. -
Schema-decoding via
response_format: Het kernblok in de payload isresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, wat het model instrueert om output te produceren die beperkt is tot ons JSON Schema.
Daarna valideren we het JSON-plan van het model met Pydantic, en pas als het door de grenscontroles komt, gaan we de Excel-grafieken renderen en het PowerPoint-deck genereren.
Stap 4: Helperfuncties
Voordat we iets renderen, hebben we een kleine laag nodig die elke geüploade CSV veilig maakt om te analyseren en consistent om naar te verwijzen. Deze helperfuncties doen vier taken: de CSV laden en opschonen, een afgeleide modeltabel maken, een compacte set feiten voor de prompt berekenen en de Excel-uitvoer leesbaar maken met automatisch aangepaste kolommen.
Stap 4.1: CSV laden en valideren
GPT 5.2 kan niet betrouwbaar grafieken en slides plannen als de invoertabel leeg is of numerieke velden als strings zijn opgeslagen. Deze functie standaardiseert de dataset zodat downstream-stappen niet hoeven te raden.
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
De bovenstaande functie:
- Leest het bestand in een DataFrame met
pd.read_csv. - Detecteert kolommen met naamheuristieken (date, month, week, period) en dwingt ze af als string. Dit houdt labels stabiel in grafieken en slidetekst.
- Zet ten slotte niet-datumkolommen om naar numeriek met
pd.to_numericen vult ontbrekende numerieke waarden met nul om latere NaN-ketens te voorkomen.
Aan het einde van deze stap heb je een DataFrame dat niet leeg is en numerieke kolommen bevat.
Stap 4.2: Het model-DataFrame berekenen
Ruwe CSV’s zijn rommelig en inconsistent tussen domeinen. Het modelsheet fungeert als normalisatielaag waarin we afgeleide metrics toevoegen die handig zijn voor grafieken, zonder dat de gebruiker ze handmatig hoeft te berekenen.
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
De functie compute_model_df() doet twee breed nuttige dingen:
-
Zoekt naar een primaire outputwaarde-kolom en een secundaire inputkolom en berekent een eenvoudige ratio om vast te leggen hoeveel van het primaire signaal overblijft na aftrek van het secundaire signaal.
-
Voor een handvol numerieke kolommen berekent hij
pct_change(), wat standaard de fractionele verandering ten opzichte van de vorige rij teruggeeft; dit vormt een eenvoudige basis om beweging in elke tijdreeksmetric vast te leggen.
Stap 4.3: Feiten berekenen
Zelfs met Structured Outputs willen we GPT-5.2 nog steeds een compacte samenvatting geven van wat er in de periode veranderde. Deze feiten werken ook als prompt die het model kan hergebruiken in slide-bullets.
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
We nemen eerst de eerste en laatste rij om het periodevenster te definiëren. Vervolgens selecteren we een kolom (date, month, week, period) voor menselijk leesbare rapportage.
Voor een kleine set numerieke kolommen berekenen we een momentopname van begin, eind, gemiddelde en totale procentuele verandering van begin tot eind. Dit levert een lichtgewicht woordenboek van signalen op dat het model helpt bij het schrijven van slide-bullets.
Stap 4.4: Excel-kolommen automatisch aanpassen
Standaard kolombreedtes in Excel laten gegenereerde werkmappen er ruw uitzien. Automatisch aanpassen is een simpele afwerking die de bruikbaarheid direct verbetert.
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)
De functie autosize_columns() scant elke kolom, vindt de maximale tekenlengte en stelt ws.column_dimensions[col_letter].width in. De OpenPyXL-bibliotheek biedt vervolgens column_dimensions en de eigenschap width om de kolomweergave in Excel te bepalen.
Daarna stoppen we de opgeschoonde modeltabel en feiten in de GPT-5.2-planningsprompt, valideren het teruggestuurde plan en renderen vervolgens grafieken en slides.
Stap 5: De werkmap bouwen
Met de opgeschoonde modeltabel en de compacte samenvattingsfeiten uit de vorige stap genereren we nu een deterministische Excel-werkmap met 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
Dit is wat deze werkkopbouwer doet:
-
De basis van de werkmap maken: De
Workbook()instantieert een nieuwe werkmap enwb.remove(wb.active)verwijdert het standaardblad zodat we de bladvolgorde volledig bepalen. Nieuwe werkbladen worden expliciet toegevoegd metwb.create_sheet("Raw"),wb.create_sheet("Model")enwb.create_sheet("Charts"). -
Het Raw-blad schrijven: We schrijven een koprij met Font, PatternFill en Alignment en voegen vervolgens elke rij uit raw_df toe. De functie
.autosize_columns(ws_raw)verbetert de leesbaarheid zonder handmatig aanpassen. -
Grenzen bepalen: We gebruiken
row_end = 1 + len(raw_df), wat ons een stabiele laatste rij geeft zodat we veilig bereiken zoalsA2:A{row_end}kunnen genereren voor categorieën en citaties. -
Heuristieken voor getalnotatie toepassen: Vervolgens stellen we
cell.number_formatin op basis van sleutelwoorden in kolomnamen en dtype, zodat Excel waarden als valuta, percentages of gewone getallen weergeeft. -
Een Charts-blad maken: De
wb.create_sheet("Charts") geeft ons een stabiel canvas voor grafiekankers. Dit past bij de openpyxl-grafiekworkflow waarbij we een LineChart of BarChart maken, bereiken koppelen viaReference, categorieën instellen metset_categoriesen hem dan plaatsen metws.add_chart().
Na deze stap heb je een consistente Excel-file waar de volgende fase veilig naar kan verwijzen met exacte celnummers.
Stap 6: Grafieken renderen
Nu de werkmapstructuur vastligt, kunnen we de grafieken renderen in het Charts-blad en een consistente merklijn toepassen. Ik heb het kleurpalet van DataCamp toegepast met:
- Primaire kleur: #01ef63
- Secundaire kleur: #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]+
De functie add_charts_to_workbook() is de grafiek-renderengine die ons schemaplan omzet in merkgebonden Excel-visuals.
-
De werkmap laden: We beginnen met het openen van het gegenereerde Excel-bestand met
load_workbook(xlsx_path). Vervolgens bouwen we eenheader_mapuit de eerste rij, die elke kolomnaam naar zijn Excel-kolomindex mappt. -
Celbereik-referenties opbouwen: De functie
Reference()definieert het exacte bereik om te plotten, enchart.set_categories(x_ref)koppelt de X-as-categorieën. Het standaardpatroon is referentieobjecten maken, data toevoegen en vervolgens categorieën instellen. -
Het grafiektype instantiëren: Voor elke grafiekspecificatie maken we een
LineChart()of eenBarChart()en stellen dechart.titlein. -
Het merkpalet toevoegen: Voor elke serie in de specificatie koppelen we het Y-bereik met
chart.add_data()en stellen vervolgens het weergavelabel in metSeriesLabel().
Ten slotte plaatst de functie ws_charts.add_chart() elke grafiek op een specifieke cel linksboven.
Stap 7: De PowerPoint renderen
In deze stap wordt het plan een echte slide-deck. We maken een PowerPoint, zetten titels en bullets neer, voegen grafieken in die zijn opgebouwd uit dezelfde tabel die we voor Excel gebruikten, en voegen een voettekst toe die de celbereik-citaties uit het plan bewaart.
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}")
Laten we de bovenstaande code stap voor stap begrijpen:
-
Deck maken en grafieken indexeren:
prs = Presentation()start een nieuwe PPTX, terwijlblank = prs.slide_layouts[6]ons een leeg canvas geeft voor precieze plaatsing. -
Titels en bullets: De functie
add_title()schrijft in een textbox-text_frame, enadd_bullets()voegt één alinea per bullet toe met controle over lettergrootte en afstand via alineaopmaak. -
Citaties-voettekst: De functie
add_sources_footer()dedupliceert citaties over bullets en rendert een compacteSources:-regel onderaan zodat traceerbaarheid intact blijft zonder de slide te vervuilen. -
Grafieken insluiten:
add_chart()bouwt eenCategoryChartData()-object met een tijdachtige X-kolom en de geplande serie-waarden, en voegt dit in viaslide.shapes.add_chart(), dat eenGraphicFrameretourneert. -
Merkstyling: Tot slot zetten we hex om naar
RGBColoren passen dit per serie toe. -
Lay-outregels en vangrails: Als een slide een geldige
chart_idheeft, gebruiken we een tweekolomsindeling, anders volledige breedte voor bullets. De functiebuild_allowed_ranges()maakt de allowlist van citeerbare bereiken envalidate_plan()handhaaft unieke grafiek-ID’s, geldige verwijzingen, toegestane citaties en geldige metrics zodat het model geen velden kan verzinnen.
Stap 8: Het plan genereren met de LLM
Op dit punt hebben we schone tabelgegevens, die we omzetten in een schema-buildplan (grafieken, slides en citaties) dat de renderer kan uitvoeren.
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
De functie generate_plan_with_llm() is het brein van deze demo. Zo voegen we hem toe aan de pijplijn:
-
Expliciet JSON Schema: Het
plan_schemadefinieert de enige responsvorm die het model mag produceren,charts[]enslides[], met verplichte velden en enums. -
System prompt-rails: We hardcoden niet-onderhandelbare regels om ervoor te zorgen dat het plan renderbaar en controleerbaar blijft. We beperken ook de grafiekplaatsing tot platte cellen, zodat de lay-out deterministisch blijft in plaats van door het model geïmproviseerd. Dit soort meerstapscontrole is precies waar GPT-5.2 voor is ontworpen.
-
Gebruikersbericht: Het gebruikersbericht geeft het doel, de gevraagde aantallen, de berekende feiten en de toegestane metrische sleutels. Omdat het model niets anders ziet, heeft het veel minder manieren om kolommen, bereiken of niet-ondersteunde claims te verzinnen.
-
Schemagebonden decodering: We roepen
chat_json_schema()aan metresponse_formatingesteld opjson_schemaenstrict: true, wat schema-naleving tijdens het decoderen afdwingt. -
Valideren in twee lagen: Eerst controleert
ArtifactPlan.model_validate(plan_json)dat de respons overeenkomt met onze Pydantic-modellen. Vervolgens handhaaftvalidate_plan()domeinbeperkingen.
Stap 9: Pijplijn draaien
Tot slot koppelen we het laden van de CSV, Excel-generatie, LLM-planning, grafiekrendering en de uiteindelijke PPTX-export aan elkaar en geven we de outputpaden terug.
@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)
Laten we alle componenten van onze pijplijn stap voor stap samenbrengen:
- Gestructureerde pijplijnoutputs: De
@dataclassmaakt een lichtgewicht container voorxlsx_path,pptx_path,plan_pathenlogs, zodat de Streamlit-UI het pijplijnresultaat kan behandelen als één gestructureerde waarde in plaats van tuples of globals te moeten jongleren. - API-sleutelrouting: De methode
load_dotenv()laadt ons.env-bestand in omgevingsvariabelen zodatOPENAI_API_KEYenOPENROUTER_API_KEYkunnen worden gelezen viaos.getenv(). Daarna kiezen we de provider-sleutel met een simpele regel: alsOPENROUTER_API_KEYbestaat, gebruik OpenRouter; anders val terug op OpenAI indien beschikbaar. - Data prep uitvoeren: We laden en valideren de CSV, berekenen onze afgeleide modeltabel en maken vervolgens het Excel-skelet via
build_workbook_base(). De functiegenerate_plan_with_llm()retourneert vervolgens eenArtifactPlandat als JSON wordt opgeslagen metjson.dump()zodat het eenvoudig te inspecteren en debuggen is. - De artefacten renderen: De functie add_charts_to_workbook() materialiseert vervolgens Excel-grafieken in het Charts-blad, en
build_pptx()bouwt de slide-deck op basis van het plan en de tabel.
Na deze stap kan onze Streamlit-app run_pipeline() aanroepen, de logs tonen en drie deterministische outputs aanbieden om te downloaden: de Excel-werkmap, de PPTX-deck en het JSON-plan.
Stap 10: Streamlit-UI
Nu de kernpijplijn klaar is, is de laatste stap deze verpakken in een Streamlit-app zodat iedereen een bestand kan uploaden, een doel kan instellen, een model kan kiezen en de gegenereerde artefacten kan downloaden.
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 de Streamlit-laag zetten we een app-frame op met st.set_page_config( layout="wide"), en splitsen vervolgens het scherm met st.columns() zodat links de invoer wordt verzameld en rechts de runbesturing en outputs getoond worden. De pijplijn draait alleen wanneer de gebruiker op st.button() klikt; we beschermen eerst tegen ontbrekende uploads, pakken st.spinner() in zodat de UI niet bevroren lijkt en leveren vervolgens de artefacten terug aan de gebruiker met st.download_button() voor het Excel-, PowerPoint- en planbestand.
Met deze stap ronden we alles af als app.py en starten we de volledige ervaring met:
streamlit run app.pyConclusie
In deze tutorial bouwden we een volledige “CSV naar artefacten”-pijplijn die GPT-5.2 gebruikt om een plan terug te geven met grafieken, slide-structuur en celbereik-citaties. We valideren dat plan vervolgens voordat we iets genereren. Deze plan-then-render aanpak past goed bij de sterke punten van GPT-5.2 voor meerstaps-workflows zoals spreadsheets en presentaties.
Het resultaat is een herhaalbare workflow waarbij de gebruiker een CSV uploadt, de code een schone werkmapfundament bouwt, GPT-5.2 een plan produceert dat alleen allowlisted Excel-bereiken kan citeren en de renderer grafieken en slides materialiseert met openpyxl en python-pptx.
Vanaf hier kun je de demo uitbreiden zonder de kernlogica te wijzigen, zoals meer grafiektypes toevoegen, slidetemplates introduceren, ondersteuning voor joins van meerdere CSV’s, enzovoort. Het sleutelpatroon blijft hetzelfde: behandel het model als een planner die een schema moet volgen, en houd alle renderlogica in code zodat outputs consistent en debugbaar blijven.
Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.
Veelgestelde vragen
Waarom GPT-5.2 combineren met Streamlit?
Streamlit maakt het eenvoudig om een AI-workflow om te zetten in een interactieve applicatie zonder een volledige frontend te bouwen. In combinatie met GPT-5.2 kunnen developers snel bewegen van experimenteren naar een bruikbare tool die input accepteert, een modelgestuurd proces draait en tastbare outputs teruggeeft.
Wat zijn Structured Outputs en waarom zijn ze hier belangrijk?
Structured Outputs dwingen het model om data terug te geven die exact voldoet aan een vooraf gedefinieerd JSON Schema. Zo volgen grafieken, slides, metrics en citaties strikte regels, waardoor het plan kan worden gevalideerd vóór er ook maar één Excel- of PowerPoint-bestand wordt gemaakt.
Heb ik diepgaande frontend-ervaring nodig om zulke apps te bouwen?
Nee. Streamlit is zo ontworpen dat het meeste werk in Python gebeurt, met vertrouwde control flow en bibliotheken. Dit verlaagt de drempel voor data scientists en analisten die AI-tools willen uitrollen zonder JavaScript of complexe webframeworks te leren.

