Curso
GPT-5.2 es un modelo potente para generar flujos de trabajo y procesos de varios pasos que requieren fiabilidad, estructura y menos alucinaciones. OpenAI ha destacado mejoras en el trabajo con contextos largos y un rendimiento aún mejor en tareas tipo hoja de cálculo.
En este tutorial, crearemos una aplicación Streamlit que se comporte como un analista junior:
- Sube un archivo CSV y escribe un objetivo (por ejemplo: Actualización de 6 diapositivas centrada en el crecimiento, la retención, la economía unitaria y la trayectoria.
- Generar un plan JSON con esquema bloqueado
- Crea un cuaderno de Excel con datos sin procesar, métricas derivadas y gráficos.
- Por último, genera una presentación de PowerPoint a juego con las citas adecuadas.
La clave aquí es que GPT-5.2 no «crea diapositivas» directamente. Genera un plan estricto que tu código ejecuta de forma determinista.
¿Qué es GPT-5.2?
GPT-5.2 es el modelo más reciente de la serie GPT-5 de OpenAI ,con mejoras para tareas integrales como crear hojas de cálculo, elaborar presentaciones, escribir y depurar código, comprender contextos largos y utilizar herramientas.
Estas son las propiedades de GPT-5.2 más importantes para este proyecto:
- Razonamiento en contexto largo: El pensamiento GPT-5.2 se describe como lo último en evaluaciones de razonamiento en contextos largos (OpenAI MRCRv2), con implicaciones prácticas para trabajar de forma coherente en documentos muy largos y entradas de múltiples archivos.
- Ejecución en varios pasos: Este modelo destaca en proyectos complejos de múltiples pasos, incluyendo una mayor capacidad de invocación de herramientas y un comportamiento de largo plazo, lo que se corresponde directamente con nuestra planificación basada en esquemas fijos y nuestro bucle de renderización determinista.
- Mejora de la veracidad: GPT-5.2 Thinking genera menos respuestas erróneas que GPT-5.1 Thinking, lo cual resulta útil en este caso, ya que reduce las posibilidades de generar texto o números que puedan desviarse de los rangos de Excel citados.
- Compromisos en cuanto a la latencia: En la API, puedes iterar con GPT-5.2 Instant para obtener una respuesta rápida y, a continuación, cambiar a GPT-5.2 Thinking o Pro cuando desees una planificación integral sólida y artefactos de mayor calidad, teniendo en cuenta que las generaciones más complejas pueden tardar varios minutos.
Proyecto de ejemplo de GPT-5.2: Construye un generador de artefactos estructurados
En esta sección, crearemos un generador de artefactos estructurados (Excel y PowerPoint) utilizando el modelo GPT 5.2 integrado en una aplicación Streamlit. A alto nivel, la aplicación Streamlit final hace lo siguiente:
-
Lee CSV y crea un cuaderno de trabajo canónico.
-
A continuación, llama a GPT-5.2 a través de OpenAI u OpenRouter con salidas estructuradas (
response_format: json_schema). -
Valida el plan devuelto con respecto a tu esquema.
-
Convierte los gráficos a Excel y los adapta al estilo de nuestra paleta de colores corporativa.
-
Por último, genera un PowerPoint con gráficos incrustados con citas.

Construyámoslo paso a paso.
Paso 1: Requisitos previos y configuración
Antes de generar cuadernos de trabajo de Excel y presentaciones de PowerPoint con GPT-5.2, es necesario configurar un entorno local. Comenzaremos instalando las bibliotecas principales y configurando las claves API como variables de entorno.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
Usaremos Streamlit para crear la interfaz de usuario web interactiva, Pandas para cargar y preparar el CSV, Pydantic para validar el plan JSON con esquema bloqueado de GPT-5.2. También utilizamos python-dotenv para cargar de forma segura claves como OPENROUTER_API_KEY/OPENAI_API_KEY desde un archivo .env . Las solicitudes llama al punto final compatible con OpenRouter/OpenAI, mientras que openpyxl genera el cuaderno de trabajo de Excel y los gráficos, y python-pptx crea la presentación de PowerPoint con gráficos nativos (editables).
A continuación, configura tu(s) clave(s) API:
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Ten en cuenta que se puede acceder a GPT 5.2 a través de múltiples servicios, incluida la API oficial de OpenAI, así como servicios como OpenRouter y otros.
Se recomienda utilizar un archivo .env para almacenar localmente las claves API:
En el archivo .env, guarda lo siguiente:
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
A continuación, carga estas claves API utilizando load_dotenv :
from dotenv import load_dotenv
load_dotenv()
En este punto, nuestro entorno ya está listo para autenticarse con la API de OpenAI.
Paso 2: Definición del esquema del plan
A continuación, definimos un plan de artefactos con esquema bloqueado que consiste en un diseño JSON estricto que indica a GPT-5.2 exactamente qué gráficos crear y cómo estructurar las diapositivas, de modo que la salida del modelo se pueda decodificar y validar utilizando salidas estructuradas a través de response_format: { type: "json_schema", strict: true } en lugar de texto de formato 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)
El fragmento de código anterior define el plan utilizando modelos Pydantic, que realizan tres funciones importantes:
-
Estructura del plan:
ChartKind = Literal["line", "bar"]limita los tipos de gráficos a una pequeña enumeración,ChartSpeccaptura el contrato completo del gráfico ySlideSpecdefine el diseño de la diapositiva, al tiempo que vincula opcionalmente una diapositiva a un gráfico a través dechart_id. -
Aplicación automática de la calidad: Las restricciones, como
Field(), garantizan que siempre tengamos suficientes gráficos/diapositivas, que cada diapositiva tenga suficientes viñetas y que cada viñeta incluya citas. -
Planificación y renderización: GPT-5.2 solo produce el esquema, es decir, gráficos, diapositivas y citas, como JSON estructurado, mientras que el resto del código se encarga de la ejecución, el formato de Excel, la representación de gráficos y el diseño de la presentación.
Con este esquema, GPT-5.2 se convierte en un planificador que produce un esquema JSON validado.
Paso 3: Llama a GPT-5.2 con salidas estructuradas.
Una vez definido el esquema del plan, el siguiente paso es llamar a GPT-5.2 en «modo esquema», de modo que devuelva un plano JSON que puedas validar y ejecutar. En lugar de esperar a que el modelo «se comporte», le indicamos a la API que decodifique directamente en tu esquema JSON utilizando response_format: { type: "json_schema", ... strict: true }, que es precisamente para lo que sirven las salidas estructuradas.
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 función ` chat_json_schema() ` es la única puerta de enlace que convierte las indicaciones en un plan validado:
-
Enrutamiento del proveedor (OpenRouter frente a OpenAI): Si el nombre del modelo tiene un espacio de nombres (openai/gpt-5.2), se utiliza OpenRouter de forma predeterminada, ya que ese es el formato que OpenRouter utiliza para el enrutamiento de modelos. Si estableces explícitamente api_provider="openai", enviarás la misma carga útil de Chat Completions al punto final de OpenAI. No dudes en omitir OpenRouter por completo.
-
Configuración de los puntos finales: Para OpenAI, llamas al punto final
https://api.openai.com/v1/chat/completionscon la autenticación Bearer estándar. Para OpenRouter, llamas al punto finalhttps://openrouter.ai/api/v1/chat/completionse incluyes opcionalmente encabezados de atribución como HTTP-Referer y X-Title, que OpenRouter documenta como metadatos recomendados. -
Decodificación de esquemas a través de
response_format: El bloque de carga útil clave esresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, que indica al modelo que genere una salida restringida a nuestro esquema JSON.
A continuación, validaremos el plan JSON del modelo con Pydantic y, solo si supera las comprobaciones de límites, procederemos a renderizar los gráficos de Excel y generar la presentación de PowerPoint.
Paso 4: Funciones auxiliares
Antes de renderizar nada, necesitamos una pequeña capa que haga que cualquier CSV cargado sea seguro de analizar y coherente para consultar. Estas funciones auxiliares realizan cuatro tareas: cargar y limpiar el CSV, crear una tabla de modelos derivados, calcular un conjunto compacto de datos para el mensaje y hacer que la salida de Excel sea legible con columnas de tamaño automático.
Paso 4.1: Cargar y validar CSV
GPT 5.2 no puede planificar gráficos y diapositivas de forma fiable si la tabla de entrada está vacía o tiene campos numéricos almacenados como cadenas. Esta función estandariza el conjunto de datos para que los pasos posteriores no tengan que hacer conjeturas.
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 función anterior:
- Lee el archivo en un DataFrame utilizando
pd.read_csv. - Detecta columnas utilizando heurística de nombres (fecha, mes, semana, período) y las convierte en cadenas. Esto mantiene las etiquetas estables en los gráficos y el texto de las diapositivas.
- Por último, convierte las columnas que no son de fecha en numéricas utilizando
pd.to_numericy rellena los valores numéricos que faltan con ceros para evitar cascadas NaN más adelante.
Al final de este paso, tendrás un DataFrame que no está vacío y tiene columnas numéricas.
Paso 4.2: Calcular el DataFrame del modelo
Los archivos CSV sin procesar son desordenados e inconsistentes entre los distintos dominios. La hoja de modelo actúa como una capa de normalización en la que añadimos métricas derivadas que son útiles para la creación de gráficos, sin necesidad de que el usuario las calcule manualmente.
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 función ` compute_model_df() ` realiza dos tareas muy útiles:
-
Busca una columna de valor de salida primaria y una columna de entrada secundaria y calcula una relación simple para capturar cuánto queda de la señal primaria después de tener en cuenta la señal secundaria.
-
Para un puñado de columnas numéricas, calcula
pct_change(), que devuelve el cambio fraccionario de la fila anterior de forma predeterminada, lo que se convierte en una base de referencia fácil para capturar el movimiento en cualquier métrica de serie temporal.
Paso 4.3: Calcular datos
Incluso con salidas estructuradas, queremos proporcionar a GPT-5.2 un resumen conciso de lo que ha cambiado durante ese periodo. Estos datos también sirven como indicaciones que el modelo puede reutilizar en las viñetas de las diapositivas.
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
Primero seleccionamos la primera y la última fila para definir la ventana del periodo. A continuación, seleccionamos una columna (fecha, mes, semana, período) para generar informes legibles para los usuarios.
Para un pequeño conjunto de columnas numéricas, calculamos una instantánea del inicio, el final, el promedio y el cambio porcentual total desde el inicio hasta el final. Esto genera un diccionario ligero de señales que ayuda al modelo a escribir viñetas para las diapositivas.
Paso 4.4: Ajustar automáticamente el tamaño de las columnas de Excel
El ancho predeterminado de las columnas de Excel hace que los cuadernos de trabajo generados tengan un aspecto poco cuidado. El ajuste automático del tamaño es un sencillo paso de pulido que mejora inmediatamente la usabilidad.
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 función ` autosize_columns() ` analiza cada columna, busca la longitud máxima de la cadena y establece ` ws.column_dimensions[col_letter].width`. A continuación, la biblioteca OpenPyXL expone column_dimensions y su propiedad width para controlar el tamaño de visualización de las columnas en Excel.
A continuación, introducimos la tabla del modelo limpio y los datos en la ventana de planificación de GPT-5.2, validamos el plan devuelto y, a continuación, generamos gráficos y diapositivas.
Paso 5: Crea el cuaderno de trabajo.
Utilizando la tabla del modelo limpio y los datos resumidos ligeros del paso anterior, ahora generamos un cuaderno de trabajo de Excel determinista con 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
Esto es lo que hace este generador de cuadernos de trabajo:
-
Crea el esqueleto del cuaderno de trabajo:
Workbook()crea una instancia de un nuevo objeto de cuaderno de trabajo y, a continuación,wb.remove(wb.active)elimina la hoja predeterminada para que podamos controlar totalmente el orden de las hojas. Las nuevas hojas de cálculo se añaden explícitamente conwb.create_sheet("Raw"),wb.create_sheet("Model")ywb.create_sheet("Charts"). -
Escribe la hoja Raw: Escribimos una fila de encabezado utilizando Font, PatternFill y Alignment, y luego añadimos todas las filas de raw_df. La función .
autosize_columns(ws_raw)ayuda a mejorar la legibilidad sin necesidad de cambiar manualmente el tamaño. -
Calcular límites: Utilizamos
row_end = 1 + len(raw_df), lo que nos proporciona una última fila estable para poder generar de forma segura rangos comoA2:A{row_end}para las categorías y citas de los gráficos. -
Aplicar heurística de formato numérico: A continuación, establecemos el tipo de datos (
cell.number_format) basándonos en las palabras clave del nombre de la columna y el tipo de datos (dtype) para que Excel muestre los valores como moneda, porcentajes o números simples. -
Crea una hoja de gráficos: El
wb.create_sheet("Gráficos") nos proporciona un lienzo estable para los anclajes de los gráficos. Esto coincide con el flujo de trabajo de gráficos de openpyxl, en el que creamos un LineChart o un BarChart, vinculamos rangos medianteReference, establecemos categorías conset_categoriesy, a continuación, lo colocamos conws.add_chart().
Después de este paso, tendrás un archivo Excel coherente al que la siguiente etapa podrá hacer referencia de forma segura mediante rangos de celdas exactos.
Paso 6: Renderizar gráficos
Ahora que la estructura del cuaderno de trabajo está fijada, podemos representar los gráficos en la hoja Gráficos y aplicar una imagen de marca coherente. Apliqué la paleta de colores de DataCamp con:
- Color principal: #01ef63
- Color secundario: #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 función add_charts_to_workbook() es el motor de representación de gráficos que convierte nuestro esquema en imágenes de Excel con la marca.
-
Carga el cuaderno de trabajo: Empezamos abriendo el archivo Excel generado con
load_workbook(xlsx_path). A continuación, creamos una tabla de nombres (header_map) a partir de la primera fila, asignando cada nombre de columna a su índice de columna de Excel. -
Crear referencias de rango de celdas: La función «
Reference()» define el rango exacto para el gráfico, y «chart.set_categories(x_ref)» conecta las categorías del eje X. El patrón estándar consiste en crear objetos de referencia, añadir datos y, a continuación, establecer categorías. -
Instanciar el tipo de gráfico: Para cada especificación de gráfico, creamos un objeto «
LineChart()» o «BarChart()» y establecemos el objeto «chart.title». -
Añade la paleta de colores de la marca: Para cada serie de la especificación, vinculamos el rango Y con «
chart.add_data()» y, a continuación, establecemos la etiqueta de visualización mediante «SeriesLabel()».
Por último, la función « ws_charts.add_chart() » coloca cada gráfico en una celda específica de la parte superior izquierda.
Paso 7: Renderizar el PowerPoint
En este paso, el plan se convierte en una presentación real. Creamos un PowerPoint, diseñamos títulos y viñetas, incorporamos gráficos creados a partir de la misma tabla que utilizamos para Excel y añadimos un pie de página que conserva las citas del rango de celdas del 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}")
Entendamos el código anterior paso a paso:
-
Crear diapositivas e índices:
prs = Presentation()inicia un nuevo PPTX, mientras queblank = prs.slide_layouts[6]nos ofrece un lienzo en blanco para una colocación precisa. -
Títulos y viñetas: La función «
add_title()» escribe en un cuadro de texto «text_frame», y «add_bullets()» añade un párrafo por viñeta mientras controla el tamaño de la fuente y el espaciado mediante el formato de párrafo. -
Citas al pie de página: La función «
add_sources_footer()» (Eliminar duplicados de citas) elimina las citas duplicadas en las viñetas y muestra una línea compacta «Sources:» (Citas en orden cronológico) en la parte inferior, de modo que la trazabilidad permanece intacta sin saturar la diapositiva. -
Incrustar gráficos:
add_chart()crea un objetoCategoryChartData()utilizando una columna X similar al tiempo y los valores de la serie planificada, y luego lo inserta a través deslide.shapes.add_chart(), que devuelve unGraphicFrame. -
Estilo de marca: Por último, convertimos hexadecimal a e
RGBColoro y lo aplicamos por series. -
Reglas de diseño y barreras de protección: Si una diapositiva tiene un
chart_idválido, utilizamos un diseño de dos columnas; de lo contrario, utilizamos viñetas de ancho completo. La función «build_allowed_ranges()» crea la lista de rangos citables permitidos, y «validate_plan()» impone identificadores de gráficos únicos, referencias válidas, citas permitidas y métricas válidas para que el modelo no pueda inventar campos.
Paso 8: Genera el plan con el LLM.
En este punto, tenemos datos tabulares limpios, que convertiremos en un plan de construcción de esquemas (gráficos, diapositivas y citas) que el renderizador puede ejecutar.
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 función generate_plan_with_llm() es el cerebro de esta demostración. Así es como lo añadimos al proceso:
-
Esquema JSON explícito: El modelo de respuesta (
plan_schema) define la única forma de respuesta que el modelo puede producir,charts[]yslides[], con campos y enumeraciones obligatorios. -
Barreras de protección del sistema: Codificamos de forma rígida los elementos no negociables para garantizar que el plan siga siendo viable y auditable. También restringimos la colocación de gráficos a celdas simples, de modo que el diseño de los gráficos siga siendo determinista en lugar de improvisado por el modelo. Este tipo de control de múltiples pasos es precisamente para lo que se ha diseñado GPT-5.2.
-
Mensaje del usuario: El mensaje de usuario proporciona el objetivo, los recuentos solicitados, los datos calculados y las claves métricas permitidas. Dado que el modelo nunca ve nada más, tiene muchas menos formas de inventar columnas, rangos o afirmaciones sin fundamento.
-
Decodificación conforme al esquema: Llamamos a
chat_json_schema()conresponse_formatestablecido enjson_schemaystrict: true, lo que garantiza el cumplimiento del esquema durante la decodificación. -
Validar en dos capas: En primer lugar, la función «
ArtifactPlan.model_validate(plan_json)» confirma que la respuesta coincide con nuestros modelos Pydantic. A continuación,validate_plan()aplica restricciones de dominio.
Paso 9: Ejecutar canalización
Por último, conectamos la carga de CSV, la generación de Excel, la planificación de LLM, la representación de gráficos y la exportación final a PPTX, y luego devolvemos las rutas de salida.
@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)
Unamos todos los componentes de vuestra canalización paso a paso:
- Resultados estructurados del proceso:
@dataclasscrea un contenedor ligero paraxlsx_path,pptx_path,plan_pathylogs, de modo que la interfaz de usuario de Streamlit pueda tratar el resultado del pipeline como un único valor estructurado en lugar de manejar tuplas o variables globales. - Enrutamiento de claves API: El método `
load_dotenv()` carga nuestro archivo `.env` en variables de entorno, de modo que `OPENAI_API_KEY` y `OPENROUTER_API_KEY` se pueden leer a través de `os.getenv()`. A continuación, seleccionamos la clave del proveedor siguiendo una regla sencilla: si existeOPENROUTER_API_KEY, utilizamos OpenRouter; de lo contrario, recurrimos a OpenAI, si está disponible. - Ejecuta la preparación de datos: Cargamos y validamos el CSV, calculamos nuestra tabla de modelos derivados y, a continuación, creamos el esqueleto de Excel a través de
build_workbook_base(). La funcióngenerate_plan_with_llm()devuelve un objetoArtifactPlanque se guarda como JSON utilizandojson.dump(), por lo que es fácil de inspeccionar y depurar. - Renderizar los artefactos: A continuación, la función add_charts_to_workbook() materializa los gráficos de Excel en la hoja Charts, y
build_pptx()crea la presentación de diapositivas a partir del plan y la tabla.
Después de este paso, nuestra aplicación Streamlit puede llamar a run_pipeline(), mostrar el logs y exponer tres resultados deterministas para su descarga, incluyendo el cuaderno de trabajo de Excel, la presentación PPTX y el plan JSON.
Paso 10: Interfaz de usuario Streamlit
Ahora que el canal principal está listo, el último paso es integrarlo en una aplicación Streamlit para que cualquiera pueda cargar un archivo, establecer un objetivo, elegir un modelo y descargar los artefactos generados.
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()
En la capa Streamlit, establecemos un marco de aplicación con st.set_page_config( layout="wide") y, a continuación, dividimos la pantalla con st.columns() para que el lado izquierdo recopile las entradas, mientras que el lado derecho muestra los controles de ejecución y las salidas. El proceso solo se ejecuta cuando el usuario hace clic en « st.button() » (Crear y enviar artefactos), donde primero nos aseguramos de que no falten archivos cargados, envolvemos « st.spinner() » (Crear y enviar artefactos) para que la interfaz de usuario no parezca congelada y, a continuación, enviamos los artefactos al usuario con « st.download_button() » (Crear y enviar artefactos) para los archivos de Excel, PowerPoint y plan.
Una vez completado este paso, guardamos todo como app.py y lanzamos la experiencia completa con:
streamlit run app.pyConclusión
En este tutorial, hemos creado un proceso completo de «CSV a artefactos» que utiliza GPT-5.2 para devolver un plan que incluye gráficos, estructura de diapositivas y citas de rangos de celdas. A continuación, validamos ese plan antes de generar nada. Este enfoque de «planificar y luego renderizar» se adapta bien a los puntos fuertes de GPT-5.2 en flujos de trabajo de varios pasos, como hojas de cálculo y presentaciones.
El resultado es un flujo de trabajo repetible en el que el usuario carga un archivo CSV, el código crea una base de cuaderno de trabajo limpia, GPT-5.2 elabora un plan que solo puede citar rangos de Excel incluidos en la lista de permitidos y el renderizador materializa gráficos y diapositivas utilizando openpyxl y python-pptx.
Desde aquí, puedes ampliar la demostración sin cambiar la lógica central, por ejemplo, añadiendo más tipos de gráficos, introduciendo plantillas de diapositivas, admitiendo uniones multi-CSV, etc. El patrón clave sigue siendo el mismo, es decir, tratar el modelo como un planificador que debe obedecer un esquema y mantener toda la lógica de renderizado en el código para que los resultados sigan siendo coherentes y se puedan depurar.

Soy experta Google Developers en ML (Gen AI), triple experta en Kaggle y embajadora de Women Techmakers, con más de tres años de experiencia en el sector tecnológico. Cofundé una startup de salud en 2020 y actualmente curso un máster en informática en Georgia Tech, con especialización en aprendizaje automático.