Cours
GPT-5.2 est un modèle performant pour générer des flux de travail et des pipelines en plusieurs étapes qui exigent fiabilité, structure et moins d'hallucinations. OpenAI a mis en avant des améliorations dans le traitement des contextes longs et des performances encore meilleures dans les tâches de type tableur.
Dans ce tutoriel, nous allons créer une application Streamlit qui se comporte comme un analyste junior :
- Veuillez télécharger un fichier CSV et définir un objectif (par exemple : « Mise à jour en 6 diapositives axée sur la croissance, la fidélisation, la rentabilité unitaire et la piste d'atterrissage »
- Générer un plan JSON verrouillé par schéma
- Veuillez créer un classeur Excel contenant les données brutes, les indicateurs dérivés et les graphiques.
- Enfin, veuillez créer une présentation PowerPoint correspondante avec les citations appropriées.
Il est important de noter que GPT-5.2 ne « crée pas directement des diapositives ». Il génère un plan rigoureux que votre code exécute de manière déterministe.
Qu'est-ce que le GPT-5.2 ?
GPT-5.2 est le tout dernier modèle de la série GPT-5 d'OpenAI. Il présente des améliorations pour les tâches de bout en bout telles que la création de feuilles de calcul, la conception de présentations, l'écriture et le débogage de code, la compréhension de contextes longs et l'utilisation d'outils.
Voici les propriétés du GPT-5.2 qui sont les plus pertinentes pour ce projet :
- s de raisonnement à long terme: La pensée GPT-5.2 est considérée comme à la pointe de la technologie en matière d'évaluation du raisonnement dans un contexte long (OpenAI MRCRv2), avec des implications pratiques pour travailler de manière cohérente sur des documents très longs et des entrées multi-fichiers.
- d'exécution en plusieurs étapes: Ce modèle excelle dans les projets complexes à plusieurs étapes, notamment grâce à un appel d'outils plus puissant et un comportement à long terme, qui correspond directement à notre planification verrouillée par schéma et à notre boucle de rendu déterministe.
- Amélioration de l'exactitude des informations : GPT-5.2 Thinking génère moins de réponses erronées que GPT-5.1 Thinking, ce qui est utile ici car cela réduit les risques de générer du texte/des chiffres qui pourraient s'écarter des plages Excel que nous avons citées.
- Compromis en matière de latence: Dans l'API, vous pouvez utiliser GPT-5.2 Instant pour obtenir un retour rapide, puis passer à GPT-5.2 Thinking ou Pro lorsque vous souhaitez bénéficier d'une planification complète et d'artefacts de meilleure qualité, en gardant à l'esprit que les générations plus complexes peuvent prendre plusieurs minutes.
Projet d'exemple GPT-5.2 : Construire un générateur d'artefacts structurés
Dans cette section, nous allons créer un générateur d'artefacts structuré (Excel et PowerPoint) à l'aide du modèle GPT 5.2 intégré dans une application Streamlit. À un niveau élevé, l'application Streamlit finale procède comme suit :
-
Il lit les fichiers CSV et crée un classeur canonique.
-
Ensuite, il contacte GPT-5.2 via OpenAI ou OpenRouter avec des sorties structurées (
response_format: json_schema). -
Valide le plan renvoyé par rapport à notre schéma.
-
Convertit les graphiques au format Excel et les adapte à notre palette de couleurs de marque.
-
Enfin, il génère un fichier PowerPoint contenant des graphiques intégrés avec des citations.

Construisons-le étape par étape.
Étape 1 : Conditions préalables et configuration
Avant de générer des classeurs Excel et des présentations PowerPoint avec GPT-5.2, il est nécessaire de configurer un environnement local. Nous commencerons par installer les bibliothèques principales et définir les clés API en tant que variables d'environnement.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
Nous utiliserons Streamlit pour créer l'interface utilisateur web interactive, Pandas pour charger et préparer le fichier CSV, Pydantic pour valider le plan JSON verrouillé par schéma de GPT-5.2. Nous utilisons également python-dotenv pour charger en toute sécurité des clés telles que OPENROUTER_API_KEY/OPENAI_API_KEY à partir d'un fichier .env . Les demandes appelle le point de terminaison compatible OpenRouter/OpenAI, tandis que openpyxl génère le classeur Excel et les graphiques, et python-pptx crée la présentation PowerPoint avec des graphiques natifs (modifiables).
Ensuite, nous configurons notre ou nos clés API :
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Veuillez noter que GPT 5.2 est accessible via plusieurs services, notamment l'API officielle d'OpenAI ainsi que des services tels qu'OpenRouter et d'autres.
Il est recommandé d'utiliser un fichier .env pour stocker localement les clés API :
Dans le fichier .env, veuillez enregistrer les éléments suivants :
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
Veuillez ensuite charger ces clés API à l'aide de la commande load_dotenv :
from dotenv import load_dotenv
load_dotenv()
À ce stade, notre environnement est désormais prêt pour l'authentification avec l'API OpenAI.
Étape 2 : Définition du schéma du plan
Ensuite, nous définissons un plan d'artefact verrouillé par schéma qui consiste en un plan JSON strict indiquant précisément à GPT-5.2 quels graphiques créer et comment structurer les diapositives, afin que la sortie du modèle puisse être décodée et validée à l'aide de sorties structurées via response_format: { type: "json_schema", strict: true } plutôt que sous forme de texte libre.
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)
L'extrait de code ci-dessus définit le plan à l'aide des modèles Pydantic, qui accomplissent trois tâches importantes :
-
Structure du plan :
ChartKind = Literal["line", "bar"]limite les types de graphiques à une petite énumération,ChartSpeccapture l'intégralité du contrat du graphique, etSlideSpecdéfinit la mise en page de la diapositive tout en liant éventuellement une diapositive à un graphique viachart_id. -
Application automatique de la qualité : Les contraintes telles que
Field()garantissent que nous disposons toujours d'un nombre suffisant de graphiques/diapositives, que chaque diapositive comporte suffisamment de puces et que chaque puce inclut des citations. -
Planification et rendu : GPT-5.2 produit uniquement le plan, c'est-à-dire les graphiques, les diapositives et les citations, sous forme de JSON structuré, tandis que le reste du code gère l'exécution, le formatage Excel, le rendu des graphiques et la mise en page du document.
Grâce à ce schéma, GPT-5.2 devient un planificateur qui produit un plan JSON validé.
Étape 3 : Appelez GPT-5.2 avec des sorties structurées.
Une fois le schéma du plan défini, l'étape suivante consiste à appeler GPT-5.2 en « mode schéma »afin qu'il renvoie un plan JSON que vous pouvez valider et exécuter. Au lieu d'espérer que le modèle « se comporte correctement », nous demandons à l'API d'utiliser pour décoder directement dans votre schéma JSON à l'aide de response_format: { type: "json_schema", ... strict: true }, ce qui correspond exactement à la fonction des sorties structurées.
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
La fonction d'chat_json_schema(), est le point d'accès unique qui transforme les invites en un plan validé :
-
Routage des fournisseurs (OpenRouter vs OpenAI) : Si le nom du modèle est associé à un espace de noms (openai/gpt-5.2), OpenRouter est utilisé par défaut, car c'est le format qu'OpenRouter utilise pour le routage des modèles. Si vous définissez explicitement api_provider="openai", vous envoyez la même charge utile Chat Completions au point de terminaison d'OpenAI. N'hésitez pas à ignorer complètement OpenRouter.
-
Définition des points finaux : Pour OpenAI, veuillez appeler le point de terminaison
https://api.openai.com/v1/chat/completionsavec l'authentification Bearer standard. Pour OpenRouter, veuillez appeler le point de terminaisonhttps://openrouter.ai/api/v1/chat/completionset inclure éventuellement des en-têtes d'attribution tels que HTTP-Referer et X-Title, que OpenRouter documente comme métadonnées recommandées. -
Décodage du schéma via
response_format: Le bloc de charge utile principal estresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, qui demande au modèle de produire une sortie limitée à notre schéma JSON.
Ensuite, nous validerons le plan JSON du modèle avec Pydantic, et uniquement s'il passe les contrôles de limites, nous procéderons au rendu des graphiques Excel et à la génération de la présentation PowerPoint.
Étape 4 : Fonctions d'assistance
Avant de procéder à tout rendu, nous avons besoin d'une petite couche qui garantit la sécurité de l'analyse et la cohérence de la référence de tout fichier CSV téléchargé. Ces fonctions d'aide accomplissent quatre tâches : charger et nettoyer le fichier CSV, créer un tableau de modèle dérivé, calculer un ensemble compact de faits pour l'invite et rendre la sortie Excel lisible avec des colonnes redimensionnées automatiquement.
Étape 4.1 : Charger et valider le fichier CSV
GPT 5.2 ne peut pas planifier de manière fiable des graphiques et des diapositives si le tableau d'entrée est vide ou contient des champs numériques stockés sous forme de chaînes de caractères. Cette fonction normalise l'ensemble de données afin que les étapes en aval n'aient pas à procéder à des estimations.
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
La fonction ci-dessus :
- Lit le fichier dans un DataFrame à l'aide de
pd.read_csv. - Détecte les colonnes à l'aide d'heuristiques de nom (date, mois, semaine, période) et les convertit en chaîne de caractères. Cela permet de maintenir la stabilité des étiquettes dans les graphiques et le texte des diapositives.
- Enfin, il convertit les colonnes non datées en valeurs numériques à l'aide de la fonction `
pd.to_numeric` et remplit les valeurs numériques manquantes par des zéros afin d'éviter les cascades NaN ultérieurement.
À la fin de cette étape, vous disposez d'un DataFrame non vide et comportant des colonnes numériques.
Étape 4.2 : Calculez le DataFrame du modèle.
Les fichiers CSV bruts sont désorganisés et incohérents d'un domaine à l'autre. La feuille de modèle sert de couche de normalisation où nous ajoutons des mesures dérivées utiles pour la création de graphiques, sans que l'utilisateur ait à les calculer manuellement.
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
La fonction ` compute_model_df() ` effectue deux opérations très utiles :
-
Il recherche une colonne de valeur de sortie principale et une colonne d'entrée secondaire, puis calcule un rapport simple afin de déterminer la part du signal principal qui subsiste après prise en compte du signal secondaire.
-
Pour une sélection de colonnes numériques, il calcule
pct_change(), qui renvoie par défaut la variation fractionnaire par rapport à la ligne précédente, ce qui constitue une base de référence simple pour saisir les mouvements dans n'importe quelle métrique de série chronologique.
Étape 4.3 : Calculer les faits
Même avec les sorties structurées, nous souhaitons toujours fournir à GPT-5.2 un résumé concis des changements survenus au cours de la période. Ces faits peuvent également servir de base que le modèle peut réutiliser dans les puces des diapositives.
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
Nous sélectionnons d'abord la première et la dernière ligne afin de définir la période considérée. Ensuite, nous sélectionnons une colonne (date, mois, semaine, période) pour générer un rapport lisible par l'utilisateur.
Pour un petit ensemble de colonnes numériques, nous calculons un instantané du début, de la fin, de la moyenne et du pourcentage global de variation entre le début et la fin. Cela permet de générer un dictionnaire de signaux léger qui aide le modèle à rédiger des puces de diapositives.
Étape 4.4 : Ajuster automatiquement la taille des colonnes Excel
Les largeurs de colonne par défaut dans Excel peuvent donner un aspect peu soigné aux classeurs générés. Le redimensionnement automatique est une étape de perfectionnement simple qui améliore immédiatement la convivialité.
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)
La fonction autosize_columns() analyse chaque colonne, détecte la longueur maximale de la chaîne et définit ws.column_dimensions[col_letter].width. Ensuite, la bibliothèque OpenPyXL met à disposition la propriété ` column_dimensions ` et sa propriété ` width ` pour contrôler la taille d'affichage des colonnes dans Excel.
Ensuite, nous intégrons le tableau et les données nettoyés dans l'invite de planification GPT-5.2, validons le plan renvoyé, puis générons des graphiques et des diapositives.
Étape 5 : Créer le classeur
À l'aide du tableau modèle nettoyé et des faits résumés légers de l'étape précédente, nous générons maintenant un classeur Excel déterministe avec 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
Voici ce que fait ce générateur de classeurs :
-
Veuillez créer la structure du classeur : La méthode `
Workbook()` instancie un nouvel objet classeur, puis `wb.remove(wb.active)` supprime la feuille par défaut afin que nous puissions contrôler entièrement l'ordre des feuilles. De nouvelles feuilles de calcul sont ajoutées explicitement avecwb.create_sheet("Raw"),wb.create_sheet("Model")etwb.create_sheet("Charts"). -
Veuillez remplir la feuille brute : Nous créons une ligne d'en-tête à l'aide de Font, PatternFill et Alignment, puis nous ajoutons chaque ligne de raw_df. La fonction .
autosize_columns(ws_raw)améliore la lisibilité sans nécessiter de redimensionnement manuel. -
Calculer les limites : Nous utilisons
row_end = 1 + len(raw_df), ce qui nous permet d'obtenir une dernière ligne stable afin de générer en toute sécurité des plages telles queA2:A{row_end}pour les catégories de graphiques et les citations. -
Appliquer des heuristiques de formatage des nombres : Ensuite, nous définissons l'
cell.number_formaten fonction des mots-clés des noms de colonnes et du type de données afin qu'Excel affiche les valeurs sous forme de devises, de pourcentages ou de nombres simples. -
Veuillez créer une feuille intitulée « Graphiques » : La fonction
wb.create_sheet("Charts") nous fournit un canevas stable pour les ancrages de graphiques. Cela correspond au flux de travail du graphique openpyxl dans lequel nous créons un LineChart ou un BarChart, lions les plages viaReference, définissons les catégories avecset_categories, puis le plaçons avecws.add_chart().
Après cette étape, vous disposez d'un fichier Excel cohérent que l'étape suivante peut référencer en toute sécurité à l'aide de plages de cellules exactes.
Étape 6 : Générer des graphiques
Maintenant que la structure du classeur est définie, nous pouvons afficher les graphiques dans la feuille Charts et appliquer une apparence cohérente à la marque. J'ai appliqué la palette de couleurs de DataCamp avec :
- Couleur principale : #01ef63
- Couleur secondaire : #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]+
La fonction add_charts_to_workbook() est le moteur de rendu graphique qui transforme notre plan de schéma en visuels Excel personnalisés.
-
Veuillez charger le classeur: Nous commençons par ouvrir le fichier Excel généré à l'aide d'
load_workbook(xlsx_path). Ensuite, nous créons un tableau de correspondance (header_map) à partir de la première ligne, en associant chaque nom de colonne à son index de colonne Excel. -
Créer des références de plage de cellules: La fonction `
Reference()` définit la plage exacte pour le graphique, et `chart.set_categories(x_ref)` relie les catégories de l'axe X. La procédure standard consiste à créer des objets de référence, à ajouter des données, puis à définir des catégories. -
Instancier le type de graphique: Pour chaque spécification de graphique, nous créons soit un fichier
LineChart(), soit un fichierBarChart(), et nous définissons l'chart.title. -
Veuillez ajouter la palette de couleurs de la marque: Pour chaque série dans les spécifications, nous lions la plage Y avec
chart.add_data(), puis définissons l'étiquette d'affichage à l'aide deSeriesLabel().
Enfin, la fonction « ws_charts.add_chart() » positionne chaque graphique dans une cellule spécifique en haut à gauche.
Étape 7 : Veuillez convertir le fichier PowerPoint.
C'est à cette étape que le plan se transforme en véritable présentation PowerPoint. Nous créons une présentation PowerPoint, mettons en page les titres et les puces, intégrons des graphiques créés à partir du même tableau que celui utilisé pour Excel et ajoutons un pied de page qui conserve les références aux plages de cellules du plan.
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}")
Comprenons le code ci-dessus étape par étape :
-
Créer des graphiques de présentation et d'index :
prs = Presentation()permet de créer un nouveau fichier PPTX, tandis queblank = prs.slide_layouts[6]fournit une toile vierge pour un placement précis. -
Titres et puces : La fonction
add_title()permet d'écrire dans une zone de textetext_frame, etadd_bullets()ajoute un paragraphe par puce tout en contrôlant la taille de la police et l'espacement via la mise en forme des paragraphes. -
Références bibliographiques : La fonction «
add_sources_footer()» (Supprimer les doublons) supprime les citations en double dans les puces et affiche une ligne d'Sources:s compacte en bas de la diapositive, ce qui permet de conserver la traçabilité sans encombrer la diapositive. -
Intégrer des graphiques :
add_chart()crée un objetCategoryChartData()à l'aide d'une colonne X de type temporel et des valeurs de la série planifiée, puis l'insère viaslide.shapes.add_chart(), qui renvoie un objetGraphicFrame. -
Style de marque :
RGBColorEnfin, nous convertissons l'hexadécimal en décimal et l'appliquons par série. -
Règles de mise en page et garde-fous : Si une diapositive contient une liste à puces valide (
chart_id), nous utilisons une mise en page à deux colonnes, sinon nous utilisons des puces sur toute la largeur. La fonction `build_allowed_ranges()` crée la liste blanche des plages pouvant être citées, et `validate_plan()` impose des identifiants de graphique uniques, des références valides, des citations autorisées et des métriques valides afin que le modèle ne puisse pas inventer de champs.
Étape 8 : Élaborez le plan à l'aide du LLM.
À ce stade, nous disposons de données tabulaires propres, que nous transformerons en un plan de construction de schéma (graphiques, diapositives et citations) que le moteur de rendu pourra exécuter.
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
La fonction generate_plan_with_llm() constitue le cœur de cette démonstration. Voici comment nous l'ajoutons au pipeline :
-
Schéma JSON explicite : Le modèle de réponse (
plan_schema) définit la seule forme de réponse que le modèle est autorisé à produire,charts[]etslides[], avec les champs et les énumérations obligatoires. -
Barrières de sécurité du système : Nous codons en dur les éléments non négociables afin de garantir que le plan reste réalisable et vérifiable. Nous limitons également le placement des graphiques aux cellules simples, afin que la disposition des graphiques reste déterministe plutôt qu'improvisée par le modèle. Ce type de contrôle en plusieurs étapes est précisément ce pour quoi le GPT-5.2 a été conçu.
-
Message de l'utilisateur : Le message utilisateur fournit l'objectif, les comptes demandés, les faits calculés et les clés métriques autorisées. Étant donné que le modèle ne voit jamais rien d'autre, il dispose de beaucoup moins de moyens pour inventer des colonnes, des plages ou des affirmations non fondées.
-
Décodage conforme au schéma : Nous appelons
chat_json_schema()avecresponse_formatdéfini surjson_schemaetstrict: true, ce qui garantit le respect du schéma lors du décodage. -
Vérifier en deux étapes : Tout d'abord, la fonction `
ArtifactPlan.model_validate(plan_json)` vérifie que la réponse correspond à nos modèles Pydantic. Ensuite,validate_plan()applique les contraintes de domaine.
Étape 9 : Exécuter le pipeline
Enfin, nous connectons le chargement CSV, la génération Excel, la planification LLM, le rendu graphique et l'exportation PPTX finale, puis nous renvoyons les chemins d'accès de sortie.
@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)
Veuillez joindre toutes les composantes de notre pipeline étape par étape :
- Résultats structurés du pipeline :
@dataclasscrée un conteneur léger pourxlsx_path,pptx_path,plan_pathetlogs, afin que l'interface utilisateur Streamlit puisse traiter le résultat du pipeline comme une valeur structurée unique au lieu de jongler avec des tuples ou des variables globales. - Routage des clés API : La méthode `
load_dotenv()` charge notre fichier `.env` dans les variables d'environnement afin que `OPENAI_API_KEY` et `OPENROUTER_API_KEY` puissent être consultés via `os.getenv()`. Ensuite, nous sélectionnons la clé du fournisseur en utilisant une règle simple, à savoir : siOPENROUTER_API_KEYexiste, utiliser OpenRouter ; sinon, se rabattre sur OpenAI si disponible. - Veuillez exécuter la préparation des données : Nous chargeons et validons le fichier CSV, calculons notre tableau de modèle dérivé, puis créons le squelette Excel via
build_workbook_base(). La fonctiongenerate_plan_with_llm()renvoie ensuite uneArtifactPlanqui est enregistrée au format JSON à l'aide dejson.dump(), ce qui facilite l'inspection et le débogage. - Rendre les artefacts : La fonction add_charts_to_workbook() génère ensuite les graphiques Excel dans la feuille Charts, et
build_pptx()crée la présentation à partir du plan et du tableau.
Après cette étape, notre application Streamlit peut appeler run_pipeline(), afficher l'logs, et exposer trois sorties déterministes à télécharger, notamment le classeur Excel, la présentation PPTX et le plan JSON.
Étape 10 : Interface utilisateur Streamlit
Maintenant que le pipeline principal est prêt, la dernière étape consiste à l'intégrer dans une application Streamlit afin que tout utilisateur puisse télécharger un fichier, définir un objectif, choisir un modèle et télécharger les artefacts générés.
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()
Dans la couche Streamlit, nous avons défini un cadre d'application avec st.set_page_config( layout="wide"), puis divisé l'écran à l'aide de st.columns() afin que le côté gauche recueille les entrées tandis que le côté droit affiche les commandes d'exécution et les sorties. Le pipeline ne s'exécute que lorsque l'utilisateur clique sur « st.button() » (Télécharger le fichier). Nous vérifions d'abord qu'aucun téléchargement n'a été manqué, puis nous encapsulons l'st.spinner() afin que l'interface utilisateur ne semble pas figée. Enfin, nous renvoyons les artefacts à l'utilisateur avec « st.download_button() » (Télécharger le fichier) pour les fichiers Excel, PowerPoint et plan.
Une fois cette étape terminée, nous enregistrons le tout sous le nom app.py et lançons l'expérience complète avec :
streamlit run app.pyConclusion
Dans ce tutoriel, nous avons élaboré un pipeline complet « CSV vers artefacts » qui utilise GPT-5.2 pour générer un plan comprenant des graphiques, une structure de diapositives et des citations de plages de cellules. Nous validons ensuite ce plan avant de procéder à toute génération. Cette approche consistant à planifier avant de rendre correspond parfaitement aux points forts de GPT-5.2 dans les workflows en plusieurs étapes, tels que les feuilles de calcul et les présentations.
Le résultat est un flux de travail reproductible dans lequel l'utilisateur télécharge un fichier CSV, le code crée une base de travail propre, GPT-5.2 génère un plan qui ne peut citer que les plages Excel autorisées, et le moteur de rendu matérialise les graphiques et les diapositives à l'aide d'openpyxl et de python-pptx.
À partir de là, vous pouvez étendre la démo sans modifier la logique de base, par exemple en ajoutant d'autres types de graphiques, en introduisant des modèles de diapositives, en prenant en charge les jointures multi-CSV, etc. Le modèle clé reste le même, c'est-à-dire que le modèle est considéré comme un planificateur qui doit respecter un schéma, et toute la logique de rendu est conservée dans le code afin que les résultats restent cohérents et débuggables.

Je suis experte Google Developers en ML (Gen AI), triple experte Kaggle et ambassadrice Women Techmakers, avec plus de trois ans d’expérience dans la tech. J’ai cofondé une startup dans le domaine de la santé en 2020 et je poursuis actuellement un master en informatique à Georgia Tech, avec une spécialisation en apprentissage automatique.