Curso
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:
-
Lê um CSV e cria uma pasta de trabalho canônica
-
Depois, chama o GPT-5.2 via OpenAI ou OpenRouter com Structured Outputs (
response_format: json_schema) -
Valida o plano retornado contra nosso schema
-
Renderiza gráficos no Excel e os estiliza com a paleta da marca
-
Por fim, gera um PowerPoint com gráficos incorporados e citações

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,ChartSpeccaptura o contrato completo do gráfico, eSlideSpecdefine o layout do slide, vinculando opcionalmente um slide a um gráfico viachart_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:
-
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.
-
Definição de endpoints: para OpenAI, você chama o endpoint
https://api.openai.com/v1/chat/completionscom Bearer auth padrão. Para OpenRouter, você chamahttps://openrouter.ai/api/v1/chat/completionse, opcionalmente, inclui cabeçalhos de atribuição como HTTP-Referer e X-Title, conforme a documentação do OpenRouter. -
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_numerice 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 ewb.remove(wb.active)remove a planilha padrão para controlarmos totalmente a ordem. Novas abas são adicionadas comwb.create_sheet("Raw"),wb.create_sheet("Model")ewb.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çãoautosize_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 comoA2:A{row_end}para categorias de gráficos e citações. -
Aplicar heurísticas de formatação numérica: definimos
cell.number_formatcom 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 viaReference, definimos categorias comset_categoriese posicionamos comws.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 umheader_mapa 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, echart.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()ouBarChart()e definimoschart.title. -
Aplicar a paleta da marca: para cada série, vinculamos o eixo Y com
chart.add_data()e definimos o rótulo comSeriesLabel().
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, enquantoblank = 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 textotext_frame, eadd_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 compactaSources:no rodapé, mantendo a rastreabilidade sem poluir o slide. -
Gráficos embutidos:
add_chart()constrói umCategoryChartData()usando uma coluna temporal no eixo X e os valores das séries planejadas, e insere comslide.shapes.add_chart(). -
Estilo da marca: por fim, convertemos o hex para
RGBColore aplicamos por série. -
Regras de layout e proteções: se o slide tiver um
chart_idválido, usamos layout em duas colunas; caso contrário, bullets em largura total. A funçãobuild_allowed_ranges()cria a lista permitida de intervalos citáveis, evalidate_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_schemadefine o único formato de resposta permitido ao modelo, comcharts[]eslides[], 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()comresponse_formatemjson_schemaestrict: 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
@dataclasscria um contêiner leve paraxlsx_path,pptx_path,plan_pathelogs, 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.envem variáveis de ambiente para queOPENAI_API_KEYeOPENROUTER_API_KEYpossam ser lidas comos.getenv(). Depois escolhemos o provedor com uma regra simples: se existirOPENROUTER_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çãogenerate_plan_with_llm()retorna umArtifactPlan, que é salvo como JSON comjson.dump()para facilitar inspeção e debug. - Renderizar os artefatos:
add_charts_to_workbook()materializa os gráficos no Excel, ebuild_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.pyConclusã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.
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.




