Pular para o conteúdo principal

Projeto GPT-5.2: crie um gerador de Excel e PowerPoint

Aprenda a usar o GPT-5.2 para transformar qualquer CSV em uma pasta Excel e um deck no PowerPoint com citações, tudo dentro de um app em Streamlit.
Atualizado 12 de mai. de 2026  · 13 min lido

GPT-5.2 é um modelo robusto para gerar fluxos de trabalho e pipelines de múltiplas etapas que exigem confiabilidade, estrutura e menos alucinações. A OpenAI destacou melhorias em tarefas de longo contexto e um desempenho ainda melhor em atividades no estilo de planilhas.

Neste tutorial, vamos criar um app em Streamlit que se comporta como um analista júnior:

  • Envie um CSV e defina um objetivo (por exemplo: “atualização de 6 slides com foco em crescimento, retenção, unit economics e runway”)
  • Gere um plano em JSON com schema bloqueado
  • Produza uma pasta de trabalho do Excel com dados brutos, métricas derivadas e gráficos
  • Por fim, gere um deck no PowerPoint correspondente, com citações corretas

O ponto-chave é que o GPT-5.2 não “faz slides” diretamente. Ele gera um plano rígido que seu código executa de forma determinística.

Aproveite também para ler nossos guias sobre os modelos mais novos da OpenAI, GPT-5.5, GPT-5.4, GPT-5.3 Codex e GPT-5.3 Instant

O que é o GPT-5.2?

O GPT-5.2 é o modelo mais recente da OpenAI da série GPT-5 com melhorias para tarefas de ponta a ponta como criar planilhas, montar apresentações, escrever e depurar código, entender contextos longos e usar ferramentas. 

Aqui estão as propriedades do GPT-5.2 que mais importam para este projeto:

  • Raciocínio de longo contexto: o GPT-5.2 Thinking é descrito como estado da arte em avaliações de raciocínio com longo contexto (OpenAI MRCRv2), com implicações práticas para trabalhar de forma coerente em documentos muito extensos e entradas com múltiplos arquivos.
  • Execução multi-etapas: este modelo se destaca em projetos complexos de várias etapas, com chamadas de ferramentas mais robustas e comportamento de longo horizonte, o que se conecta diretamente ao nosso planejamento com schema bloqueado e loop de renderização determinístico.
  • Factualidade aprimorada: o GPT-5.2 Thinking comete menos erros do que o GPT-5.1 Thinking, o que ajuda aqui porque reduz as chances de gerar textos/números que se desviem dos intervalos do Excel citados.
  • Trade-offs de latência: na API, você pode iterar com o GPT-5.2 Instant para feedback rápido e depois alternar para o GPT-5.2 Thinking ou Pro quando quiser planejamento de ponta a ponta mais forte e artefatos de maior qualidade, lembrando que gerações mais complexas podem levar minutos.

Projeto exemplo com GPT-5.2: crie um gerador de artefatos estruturados

Nesta seção, vamos criar um gerador de artefatos estruturados (Excel e PowerPoint) usando o modelo GPT-5.2 dentro de um app em Streamlit. Em linhas gerais, o app final faz o seguinte:

  1. Lê um CSV e cria uma pasta de trabalho canônica

  2. Depois, chama o GPT-5.2 via OpenAI ou OpenRouter com Structured Outputs (response_format: json_schema)

  3. Valida o plano retornado contra nosso schema

  4. Renderiza gráficos no Excel e os estiliza com a paleta da marca

  5. Por fim, gera um PowerPoint com gráficos incorporados e citações

Final demo

Vamos construir isso passo a passo.

Passo 1: pré-requisitos e configuração

Antes de gerar planilhas do Excel e decks no PowerPoint com o GPT-5.2, precisamos configurar o ambiente local. Vamos começar instalando as bibliotecas principais e definindo as chaves de API como variáveis de ambiente.

pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx

Usaremos Streamlit para construir a interface web interativa, Pandas para carregar e preparar o CSV, Pydantic para validar o plano JSON com schema bloqueado do GPT-5.2. Também usamos python-dotenv para carregar com segurança chaves como OPENROUTER_API_KEY/OPENAI_API_KEY de um arquivo .env. A biblioteca requests chama o endpoint compatível com OpenRouter/OpenAI, enquanto openpyxl gera a pasta do Excel e os gráficos, e python-pptx cria o deck do PowerPoint com gráficos nativos (editáveis). 

Agora, definimos nossas chaves de API:

export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"

Observe que o GPT-5.2 pode ser acessado por vários serviços, incluindo a API oficial da OpenAI e serviços como o OpenRouter, entre outros. 

É recomendado usar um arquivo .env para armazenar localmente as chaves da API:

No arquivo .env, salve o seguinte:

OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"

Depois, carregue essas chaves de API usando a biblioteca load_dotenv:

from dotenv import load_dotenv
load_dotenv()

A partir daqui, nosso ambiente está pronto para autenticar com a API da OpenAI.

Passo 2: definindo o schema do plano

Em seguida, definimos um plano de artefatos com schema bloqueado, que é um blueprint JSON rígido dizendo ao GPT-5.2 exatamente quais gráficos criar e como estruturar os slides, para que a saída do modelo possa ser decodificada e validada usando Structured Outputs via response_format: { type: "json_schema", strict: true }, em vez de texto livre.

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)

O trecho acima define o plano usando modelos Pydantic, que fazem três coisas importantes:

  • Estrutura do plano: ChartKind = Literal["line", "bar"] limita os tipos de gráfico a um pequeno enum, ChartSpec captura o contrato completo do gráfico, e SlideSpec define o layout do slide, vinculando opcionalmente um slide a um gráfico via chart_id.

  • Garantia automática de qualidade: as restrições com Field() garantem que sempre tenhamos gráficos/slides suficientes, que cada slide tenha bullets suficientes e que cada bullet inclua citações.

  • Planejamento e renderização: o GPT-5.2 produz apenas o blueprint — ou seja, gráficos, slides e citações — como JSON estruturado, enquanto o restante do código cuida da execução, formatação no Excel, renderização dos gráficos e layout do deck. 

Com esse schema, o GPT-5.2 vira um planejador que gera um blueprint JSON validado.

Passo 3: chame o GPT-5.2 com Structured Outputs

Com o schema do plano definido, o próximo passo é chamar o GPT-5.2 em “modo schema” — assim ele retorna um blueprint em JSON que você pode validar e executar. Em vez de torcer para o modelo “se comportar”, nós pedimos à API para decodificar diretamente no seu JSON Schema usando response_format: { type: "json_schema", ... strict: true }, que é exatamente o propósito de Structured Outputs.

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

A função chat_json_schema() é a única porta de entrada que transforma prompts em um plano validado:

  1. Roteamento de provedor (OpenRouter vs OpenAI): se o nome do modelo vier com namespace (openai/gpt-5.2), você usa OpenRouter por padrão, pois esse é o formato que o OpenRouter utiliza para roteamento. Se definir explicitamente api_provider="openai", você envia o mesmo payload de Chat Completions para o endpoint da OpenAI. Fique à vontade para ignorar o OpenRouter se preferir.

  2. Definição de endpoints: para OpenAI, você chama o endpoint https://api.openai.com/v1/chat/completions com Bearer auth padrão. Para OpenRouter, você chama https://openrouter.ai/api/v1/chat/completions e, opcionalmente, inclui cabeçalhos de atribuição como HTTP-Referer e X-Title, conforme a documentação do OpenRouter.

  3. Decodificação via response_format: o bloco crucial do payload é response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, que instrui o modelo a produzir saída restrita ao nosso JSON Schema.

Depois, vamos validar o plano JSON do modelo com Pydantic e, só se ele passar nos limites definidos, seguiremos para renderizar os gráficos no Excel e gerar o deck no PowerPoint.

Passo 4: funções auxiliares

Antes de renderizar qualquer coisa, precisamos de uma camada que torne qualquer CSV enviado seguro para análise e consistente para referência. Essas funções auxiliares fazem quatro trabalhos: carregar e limpar o CSV, criar uma tabela derivada (model), calcular um conjunto compacto de fatos para o prompt e deixar a saída no Excel legível com colunas autoajustadas.

Passo 4.1: carregar e validar o CSV

O GPT-5.2 não consegue planejar gráficos e slides de forma confiável se a tabela de entrada estiver vazia ou se campos numéricos forem strings. Esta função padroniza o conjunto de dados para que as etapas seguintes não precisem adivinhar.

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

A função acima:

  • Lê o arquivo em um DataFrame usando pd.read_csv.
  • Detecta colunas por heurísticas de nome (date, month, week, period) e as força para string. Isso mantém os rótulos estáveis em gráficos e textos de slide.
  • Por fim, converte colunas não temporais em numéricas com pd.to_numeric e preenche valores numéricos ausentes com zero para evitar cascatas de NaN depois. 

Ao fim deste passo, você tem um DataFrame não vazio e com colunas numéricas.

Passo 4.2: calcular o DataFrame do modelo

CSVs brutos são bagunçados e inconsistentes entre domínios. A planilha model atua como uma camada de normalização onde adicionamos métricas derivadas úteis para gráficos, sem exigir que o usuário as 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

A função compute_model_df() faz duas coisas amplamente úteis:

  • Procura uma coluna principal de output e uma coluna secundária de input e calcula uma razão simples para capturar quanto do sinal principal permanece após considerar o sinal secundário.

  • Para um punhado de colunas numéricas, calcula pct_change(), que retorna por padrão a variação fracionária em relação à linha anterior — um ponto de partida simples para capturar movimento em qualquer métrica de série temporal.

Passo 4.3: calcular fatos

Mesmo com Structured Outputs, ainda queremos dar ao GPT-5.2 um resumo compacto do que mudou no período. Esses fatos também servem como prompt que o modelo pode reutilizar nos bullets dos slides.

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

Primeiro escolhemos a primeira e a última linhas para definir a janela do período. Em seguida, selecionamos uma coluna (date, month, week, period) para um relatório legível por humanos.

Para um pequeno conjunto de colunas numéricas, calculamos instantâneos de início, fim, média e a variação percentual do início ao fim. Isso gera um dicionário leve de sinais que ajuda o modelo a escrever os bullets dos slides.

Passo 4.4: autoajuste de colunas no Excel

A largura padrão de colunas no Excel deixa as pastas geradas com aparência rústica. O autoajuste é um polimento simples que melhora a usabilidade imediatamente.

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)

A função autosize_columns() percorre cada coluna, encontra o maior comprimento de string e define ws.column_dimensions[col_letter].width. A biblioteca OpenPyXL expõe column_dimensions e sua propriedade width para controlar a largura de exibição no Excel. 

Agora conectamos a tabela model limpa e os fatos ao prompt de planejamento do GPT-5.2, validamos o plano retornado e então renderizamos os gráficos e os slides.

Passo 5: gerar a pasta de trabalho 

Usando a tabela model limpa e o resumo de fatos do passo anterior, agora geramos uma pasta do Excel determinística com 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

O que este gerador de pasta faz:

  • Criar o esqueleto da pasta: Workbook() instancia uma nova pasta e wb.remove(wb.active) remove a planilha padrão para controlarmos totalmente a ordem. Novas abas são adicionadas com wb.create_sheet("Raw"), wb.create_sheet("Model") e wb.create_sheet("Charts").

  • Escrever a aba Raw: criamos a linha de cabeçalho com Font, PatternFill e Alignment e depois incluímos todas as linhas de raw_df. A função autosize_columns(ws_raw) melhora a leitura sem precisar redimensionar manualmente.

  • Calcular limites:  usamos row_end = 1 + len(raw_df), o que nos dá a última linha estável para gerar intervalos como A2:A{row_end} para categorias de gráficos e citações.

  • Aplicar heurísticas de formatação numérica: definimos cell.number_format com base em palavras-chave no nome da coluna e no dtype para exibir valores no Excel como moeda, porcentagem ou número. 

  • Criar a aba Charts: wb.create_sheet("Charts") oferece um canvas estável para âncoras de gráfico. Isso acompanha o fluxo do openpyxl, no qual criamos um LineChart ou BarChart, vinculamos intervalos via Reference, definimos categorias com set_categories e posicionamos com ws.add_chart().

Depois deste passo, você tem um arquivo do Excel consistente que a próxima etapa pode referenciar com exatidão por intervalos de células.
Passo 6: renderizar gráficos 

Com a estrutura da pasta pronta, podemos renderizar os gráficos na aba Charts e aplicar uma identidade visual consistente. Apliquei a paleta de cores da DataCamp com:

  • Cor primária: #01ef63
  • Cor secundária: #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]+

A função add_charts_to_workbook() é o motor de renderização que transforma nosso plano em visuais de Excel com a identidade da marca.

  • Carregar a pasta: começamos abrindo o arquivo gerado com load_workbook(xlsx_path). Em seguida, criamos um header_map a partir da primeira linha, mapeando cada nome de coluna ao seu índice de coluna no Excel. 

  • Construir referências de intervalo: a função Reference() define o intervalo exato a ser plotado, e chart.set_categories(x_ref) conecta as categorias do eixo X. O padrão é criar objetos de referência, adicionar dados e definir categorias. 

  • Instanciar o tipo de gráfico: para cada spec, criamos um LineChart() ou BarChart() e definimos chart.title

  • Aplicar a paleta da marca: para cada série, vinculamos o eixo Y com chart.add_data() e definimos o rótulo com SeriesLabel()

Por fim, a função ws_charts.add_chart() posiciona cada gráfico em uma célula específica no canto superior esquerdo. 

Passo 7: renderizar o PowerPoint

Aqui o plano vira um deck real. Criamos um PowerPoint, definimos títulos e bullets, incorporamos gráficos construídos a partir da mesma tabela usada no Excel e adicionamos um rodapé que preserva as citações por intervalo de células.

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}")

Vamos entender o código acima, passo a passo:

  • Criar o deck e indexar gráficos: prs = Presentation() inicia um novo PPTX, enquanto blank = prs.slide_layouts[6] nos dá um canvas em branco para posicionamento preciso.

  • Títulos e bullets: a função add_title() escreve em uma caixa de texto text_frame, e add_bullets() adiciona um parágrafo por bullet controlando tamanho e espaçamento.

  • Rodapé de citações: add_sources_footer() deduplica citações entre bullets e renderiza uma linha compacta Sources: no rodapé, mantendo a rastreabilidade sem poluir o slide.

  • Gráficos embutidos: add_chart() constrói um CategoryChartData() usando uma coluna temporal no eixo X e os valores das séries planejadas, e insere com slide.shapes.add_chart().

  • Estilo da marca: por fim, convertemos o hex para RGBColor e aplicamos por série. 

  • Regras de layout e proteções: se o slide tiver um chart_id válido, usamos layout em duas colunas; caso contrário, bullets em largura total. A função build_allowed_ranges() cria a lista permitida de intervalos citáveis, e validate_plan() impõe IDs únicos, referências válidas, citações permitidas e métricas conhecidas para o modelo não inventar campos.

Passo 8: gerar o plano com o LLM

Neste ponto, temos dados tabulares limpos, que vamos transformar em um plano de construção (gráficos, slides e citações) que o renderizador consegue executar.

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

A função generate_plan_with_llm() é o cérebro deste demo. Veja como ela entra no pipeline:

  • JSON Schema explícito: o plan_schema define o único formato de resposta permitido ao modelo, com charts[] e slides[], campos obrigatórios e enums.

  • Regras no system prompt: codificamos não negociáveis para garantir que o plano permaneça renderizável e auditável. Também restringimos a âncora de gráfico a células simples, mantendo o layout determinístico, sem improviso do modelo. Esse controle multi-etapas é exatamente o ponto forte do GPT-5.2.

  • Mensagem do usuário: fornece o objetivo, as quantidades solicitadas, os fatos calculados e as chaves de métricas disponíveis. Como o modelo não vê mais nada, tem menos espaço para inventar colunas, intervalos ou afirmações sem suporte.

  • Decodificação aderente ao schema: chamamos chat_json_schema() com response_format em json_schema e strict: true, o que impõe a adesão ao schema durante a decodificação.

  • Validação em duas camadas: primeiro, ArtifactPlan.model_validate(plan_json) confirma que a resposta bate com nossos modelos Pydantic. Depois, validate_plan() impõe restrições de domínio.

Passo 9: executar o pipeline

Por fim, conectamos o carregamento do CSV, a geração do Excel, o planejamento com LLM, a renderização dos gráficos e a exportação final do PPTX, e então retornamos os caminhos de saída.

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

Vamos juntar os componentes do pipeline passo a passo:

  • Saídas estruturadas do pipeline: o @dataclass cria um contêiner leve para xlsx_path, pptx_path, plan_path e logs, para que a interface em Streamlit trate o resultado como um único valor estruturado, sem precisar manipular tuplas ou globais.
  • Roteamento de chaves da API: load_dotenv() carrega o arquivo .env em variáveis de ambiente para que OPENAI_API_KEY e OPENROUTER_API_KEY possam ser lidas com os.getenv(). Depois escolhemos o provedor com uma regra simples: se existir OPENROUTER_API_KEY, use OpenRouter; caso contrário, caia para OpenAI se disponível.
  • Preparar os dados: carregamos e validamos o CSV, calculamos a tabela model derivada e criamos o esqueleto do Excel com build_workbook_base(). A função generate_plan_with_llm() retorna um ArtifactPlan, que é salvo como JSON com json.dump() para facilitar inspeção e debug.
  • Renderizar os artefatos: add_charts_to_workbook() materializa os gráficos no Excel, e build_pptx() monta o deck a partir do plano e da tabela.

Depois deste passo, nosso app em Streamlit pode chamar run_pipeline(), exibir os logs e disponibilizar três saídas determinísticas para download: a pasta do Excel, o deck PPTX e o plano em JSON. 

Passo 10: interface em Streamlit

Com o pipeline pronto, o passo final é empacotar tudo em um app Streamlit para qualquer pessoa enviar um arquivo, definir um objetivo, escolher um modelo e baixar os artefatos gerados.

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()

Na camada Streamlit, definimos a moldura do app com st.set_page_config(layout="wide"), depois dividimos a tela com st.columns() para o lado esquerdo coletar entradas enquanto o lado direito mostra controles e saídas. O pipeline só roda quando o usuário clica em st.button(). Primeiro evitamos faltas de upload, envolvemos em st.spinner() para a UI não parecer travada, e então entregamos os artefatos via st.download_button() para Excel, PowerPoint e o arquivo de plano.

Com este passo concluído, salvamos tudo como app.py e iniciamos a experiência completa com:

streamlit run app.py

Conclusão

Neste tutorial, construímos um pipeline completo de “CSV para artefatos” que usa o GPT-5.2 para retornar um plano com gráficos, estrutura de slides e citações por intervalo de células. Validamos esse plano antes de gerar qualquer coisa. Essa abordagem de planejar e depois renderizar combina com os pontos fortes do GPT-5.2 em fluxos de trabalho multi-etapas como planilhas e apresentações.

O resultado é um fluxo repetível no qual o usuário envia um CSV, o código constrói uma base limpa no Excel, o GPT-5.2 produz um plano que só pode citar intervalos permitidos, e o renderizador materializa gráficos e slides usando openpyxl e python-pptx. 

A partir daqui, você pode estender o demo sem mudar a lógica central — adicionando mais tipos de gráfico, introduzindo templates de slides, suporte a junção de múltiplos CSVs etc. O padrão-chave permanece: trate o modelo como um planejador que deve obedecer a um schema e mantenha toda a renderização no código para que as saídas permaneçam consistentes e fáceis de depurar.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Sou Especialista Google Developers em ML (Gen AI), tricampeã no Kaggle e Embaixadora Women Techmakers, com mais de três anos de experiência na área de tecnologia. Cofundei uma startup de saúde em 2020 e atualmente faço um mestrado em ciência da computação na Georgia Tech, com foco em aprendizado de máquina.

Perguntas frequentes

Por que combinar o GPT-5.2 com Streamlit?

O Streamlit facilita transformar um fluxo de IA em um app interativo sem precisar construir um frontend completo. Combinado ao GPT-5.2, permite que desenvolvedores avancem rápido da experimentação para uma ferramenta usável que recebe entradas, roda um processo orientado por modelo e retorna resultados concretos.

O que são Structured Outputs e por que são importantes aqui?

Structured Outputs obrigam o modelo a retornar dados que obedecem exatamente a um JSON Schema predefinido. Isso garante que gráficos, slides, métricas e citações sigam regras rígidas, permitindo validar o plano antes de criar qualquer arquivo do Excel ou PowerPoint.

Preciso ter experiência profunda em frontend para construir apps como este?

Não. O Streamlit é feito para que a maior parte do trabalho aconteça em Python, usando fluxo de controle e bibliotecas familiares. Isso reduz a barreira para cientistas de dados e analistas que querem publicar ferramentas com IA sem aprender JavaScript ou frameworks web complexos.

Tópicos

Aprenda com a DataCamp

Curso

Engenharia rápida com a API OpenAI

4 h
48K
Mergulhe nos princípios e práticas do prompt engineering para usar modelos como ChatGPT em problemas reais.
Ver detalhesRight Arrow
Iniciar curso
Ver maisRight Arrow
Relacionado
An avian AI exits its cage

blog

12 Alternativas de código aberto ao GPT-4

GPT-4 alternativas de código aberto que podem oferecer desempenho semelhante e exigem menos recursos computacionais para serem executadas. Esses projetos vêm com instruções, fontes de código, pesos de modelos, conjuntos de dados e interface de usuário do chatbot.
Abid Ali Awan's photo

Abid Ali Awan

9 min

blog

Os 10 melhores GPTs personalizados na GPT Store

Explore os melhores GPTs personalizados que vimos até agora na loja GPT, desde ferramentas de ciência de dados até assistentes de SEO e geração de imagens.
Nisha Arya Ahmed's photo

Nisha Arya Ahmed

10 min

blog

Tudo o que sabemos sobre o GPT-5

Saiba como o GPT-5 evoluirá para um sistema unificado com recursos avançados, visando um lançamento no verão de 2025, com base no mais recente roteiro da OpenAI e no histórico do GPT.
Josep Ferrer's photo

Josep Ferrer

8 min

Tutorial

Como criar modelos personalizados do ChatGPT: 5 etapas fáceis para GPTs personalizados

Confira estas cinco etapas simples para liberar todo o potencial do ChatGPT com seus próprios GPTs personalizados.
Moez Ali's photo

Moez Ali

Tutorial

Um guia para iniciantes na engenharia de prompts do ChatGPT

Descubra como fazer com que o ChatGPT forneça os resultados que você deseja, fornecendo a ele as entradas necessárias.
Matt Crabtree's photo

Matt Crabtree

Tutorial

Visão GPT-4: Um guia abrangente para iniciantes

Este tutorial apresentará tudo o que você precisa saber sobre o GPT-4 Vision, desde o acesso a ele, passando por exemplos práticos do mundo real, até suas limitações.
Arunn Thevapalan's photo

Arunn Thevapalan

Ver maisVer mais