Ir al contenido principal

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.
Actualizado 7 ene 2026  · 13 min leer

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:

  1. Lee CSV y crea un cuaderno de trabajo canónico.

  2. A continuación, llama a GPT-5.2 a través de OpenAI u OpenRouter con salidas estructuradas (response_format: json_schema).

  3. Valida el plan devuelto con respecto a tu esquema.

  4. Convierte los gráficos a Excel y los adapta al estilo de nuestra paleta de colores corporativa.

  5. Por último, genera un PowerPoint con gráficos incrustados con citas.

Demostración final

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, 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.

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:

  1. 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.

  2. 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.

  3. 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.

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_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.

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

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[] 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.

@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: @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)

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.py

Conclusió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.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

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.

Temas

Aprende con DataCamp

Curso

Ingeniería de avisos con la API OpenAI

4 h
37.4K
"Explora los principios y mejores prácticas de la ingeniería de prompts para usar modelos como ChatGPT."
Ver detallesRight Arrow
Iniciar curso
Ver másRight Arrow