Proyecto GPT-5.2: Crea un generador de Excel y PowerPoint
Aprende a utilizar GPT-5.2 para convertir cualquier archivo CSV en un cuaderno de trabajo de Excel y una presentación de PowerPoint basada en citas dentro de una aplicación Streamlit.
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-5de 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.
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).
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 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"]defsafe_float(x)->float:try:if x isNone:return0.0ifisinstance(x,(int,float)):returnfloat(x)returnfloat(str(x).replace(",","").replace("$",""))except Exception:return0.0deffmt_money(v:float)->str:
sign ="-"if v <0else""
v =abs(v)if v >=1_000_000:returnf"{sign}${v/1_000_000:.2f}M"if v >=1_000:returnf"{sign}${v/1_000:.1f}K"returnf"{sign}${v:.0f}"deffmt_pct(v:float)->str:if v isNoneor(isinstance(v,float)and(math.isnan(v)or math.isinf(v))):return"n/a"returnf"{v*100:.1f}%"classChartSeriesSpec(BaseModel):
name:str
metric:strclassChartSpec(BaseModel):id:str
kind: ChartKind
title:str
x_metric:str
series: List[ChartSeriesSpec]
anchor:strclassSlideBullet(BaseModel):
text:str
citations: List[str]= Field(min_length=1, max_length=4)classSlideSpec(BaseModel):
title:str
chart_id: Optional[str]=None
bullets: List[SlideBullet]= Field(min_length=2, max_length=4)classArtifactPlan(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, ChartSpec captura el contrato completo del gráfico y SlideSpec define el diseño de la diapositiva, al tiempo que vincula opcionalmente una diapositiva a un gráfico a través de chart_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.
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/completions con la autenticación Bearer estándar. Para OpenRouter, llamas al punto final https://openrouter.ai/api/v1/chat/completions e 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 es response_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.
defload_and_validate_csv(csv_path:str)-> pd.DataFrame:
df = pd.read_csv(csv_path)iflen(df)==0:raise ValueError("CSV is empty")iflen(df.columns)==0:raise ValueError("CSV has no columns")
date_cols =[c for c in df.columns ifany(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 notin 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):passreturn df
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_numeric y 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.
defcompute_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 ifany(x in c.lower()for x in['revenue','mrr','income','sales'])]
cost_cols =[c for c in df.columns ifany(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 notin 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.
defcompute_facts(model_df: pd.DataFrame)-> Dict[str,str]:defdelta(a:float, b:float)->float:return0.0if a ==0else(b - a)/ a
iflen(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 ifany(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 inenumerate(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 >=100elsef"{start_val:.2f}"
facts[f"{col}_end"]= fmt_money(end_val)if end_val >=100elsef"{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 >=100elsef"{avg_val:.2f}"return factsCon la tecnología de
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.
defautosize_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 isNone: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.
defbuild_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 inenumerate(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 inenumerate(raw_df.itertuples(index=False), start=2):for j, val inenumerate(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 inenumerate(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 inenumerate(model_df.itertuples(index=False), start=2):for j, val inenumerate(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 +1for idx, name inenumerate(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 =Falseif col_name in model_df.columns:
sample_vals = model_df[col_name].dropna()iflen(sample_vals)>0and pd.api.types.is_numeric_dtype(sample_vals):
max_val =abs(sample_vals.max())
is_large_number = max_val >1000for r inrange(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 con wb.create_sheet("Raw"), wb.create_sheet("Model") y wb.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 como A2: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: Elwb.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 mediante Reference, establecemos categorías con set_categories y, a continuación, lo colocamos con ws.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
defadd_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 inrange(1, ws_model.max_column +1)}defref_from_metric(metric:str, row_start:int, row_end:int)-> Reference:if metric notin 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 notin header_map:
date_keywords =['date','time','month','year','period','week']
date_cols =[col for col in header_map.keys()if col andany(kw instr(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 inenumerate(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 =30000else:
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"ifnot 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.
defbuild_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}defadd_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 =Truedefadd_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 inenumerate(bullets):
p = tf.paragraphs[0]if i ==0else 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)defadd_sources_footer(slide, bullets: List[SlideBullet]):
sources =[]
seen =set()for b in bullets:for c in b.citations:if c notin seen:
seen.add(c)
sources.append(c)ifnot 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 =Truedefadd_chart(slide, chart_spec: ChartSpec, left, top, width, height):
date_cols =[c for c in model_df.columns ifany(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 inenumerate(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 =Falsefrom pptx.dml.color import RGBColor
for i, series inenumerate(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)defbuild_allowed_ranges(model_df: pd.DataFrame, row_end:int)-> Dict[str,str]:
ranges ={}for i, col inenumerate(model_df.columns, start=1):
col_letter = get_column_letter(i)
ranges[col]=f"Model!{col_letter}2:{col_letter}{row_end}"return ranges
defvalidate_plan(plan: ArtifactPlan, allowed_ranges: Dict[str,str])->None:
allowed_set =set(allowed_ranges.values())
chart_ids ={c.idfor c in plan.charts}iflen(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 notin 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 notin 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 notin allowed_metrics:passfor s in c.series:if s.metric notin allowed_metrics:raise ValueError(f"Unknown series metric: {s.metric}")
Crear diapositivas e índices: prs = Presentation() inicia un nuevo PPTX, mientras que blank = 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 objeto CategoryChartData() utilizando una columna X similar al tiempo y los valores de la serie planificada, y luego lo inserta a través de slide.shapes.add_chart(), que devuelve un GraphicFrame.
Estilo de marca: Por último, convertimos hexadecimal a e RGBColor o y lo aplicamos por series.
Reglas de diseño y barreras de protección: Si una diapositiva tiene un chart_id vá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.
defgenerate_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)iflen(plan.slides)!= num_slides:raise ValueError(f"Expected {num_slides} slides, got {len(plan.slides)}")iflen(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)iflen(plan.slides)!= num_slides orlen(plan.charts)!= num_charts:raise RuntimeError("Plan repaired but counts still wrong. Tighten prompt or increase max_tokens.")return planCon la tecnología de
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[] y slides[], 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() con response_format establecido en json_schema y strict: 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.
Unamos todos los componentes de vuestra canalización paso a paso:
Resultados estructurados del proceso: @dataclass crea un contenedor ligero para xlsx_path, pptx_path, plan_path y logs, 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 existe OPENROUTER_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ón generate_plan_with_llm() devuelve un objeto ArtifactPlan que se guarda como JSON utilizando json.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)defmain():try:if'outputs'notin st.session_state:
st.session_state.outputs =Noneif'generated'notin 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 providerif 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=Trueifnot uploaded_file elseFalse)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))iflen(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)ifnot generate_btn:
status_container.info("Configure settings and click 'Generate Artifacts'")if generate_btn:ifnot 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 =Falseif st.session_state.generated and st.session_state.outputs:
outputs = st.session_state.outputs
try:if"Error"notin 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"withopen(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():withopen(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))iflen(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():withopen(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()Con la tecnología de
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:
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.
Alternativas de código abierto a GPT-4 que pueden ofrecer un rendimiento similar y requieren menos recursos informáticos para funcionar. Estos proyectos vienen con instrucciones, fuentes de código, pesos del modelo, conjuntos de datos e IU de chatbot.
Aprende a utilizar modelos de IA generativa para crear un editor de imágenes, un chatbot similar a ChatGPT con pocos recursos y una aplicación clasificadora de aprobación de préstamos y a automatizar interacciones PDF y un asistente de voz con GPT.
Descubre cómo GPT-5 evolucionará hasta convertirse en un sistema unificado con funciones avanzadas, cuyo lanzamiento está previsto para el verano de 2025, basándose en la última hoja de ruta de OpenAI y en la historia de GPT.
Explore GPT-3.5 Turbo y descubra el potencial transformador del ajuste fino. Aprenda a personalizar este modelo de lenguaje avanzado para aplicaciones especializadas, mejore su rendimiento y comprenda los costes asociados, la seguridad y las consideraciones de privacidad.
Este tutorial le presentará todo lo que necesita saber sobre GPT-4 Vision, desde cómo acceder a él hasta ejemplos prácticos del mundo real y sus limitaciones.
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:
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:
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.
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_KEYdesde 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:
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
.envpara almacenar localmente las claves API:En el archivo
.env, guarda lo siguiente:A continuación, carga estas claves API utilizando 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.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.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.
La función anterior:
pd.read_csv.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.
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.
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.
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 exponecolumn_dimensionsy su propiedadwidthpara 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.
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:
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.
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.
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.
Unamos todos los componentes de vuestra canalización paso a paso:
@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.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.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.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 ellogsy 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.
En la capa Streamlit, establecemos un marco de aplicación con
st.set_page_config( layout="wide")y, a continuación, dividimos la pantalla const.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.pyy lanzamos la experiencia completa con: