Curso
O GPT-5.2 é um modelo robusto para gerar fluxos de trabalho e pipelines de várias etapas que exigem confiabilidade, estrutura e menos alucinações. A OpenAI destacou melhorias no trabalho com contextos longos e um desempenho ainda melhor em tarefas do tipo planilha.
Neste tutorial, vamos criar um aplicativo Streamlit que funciona como um analista júnior:
- Carregue um CSV e escreva uma meta (por exemplo: Atualização de 6 slides com foco em crescimento, retenção, economia unitária e runway.
- Gerar um plano JSON com bloqueio de esquema
- Crie uma pasta de trabalho do Excel com dados brutos, métricas derivadas e gráficos.
- Por fim, crie uma apresentação em PowerPoint com as citações certas.
O ponto principal aqui é que o GPT-5.2 não “cria slides” diretamente. Ele cria um plano bem rígido que o seu código segue de forma determinada.
O que é o GPT-5.2?
O GPT-5.2 é o mais novo modelo da série GPT-5 da OpenAI ,com melhorias para tarefas completas, como criar planilhas, fazer apresentações, escrever e depurar código, entender contextos longos e usar ferramentas.
Aqui estão as propriedades do GPT-5.2 que são mais importantes para este projeto:
- Raciocínio de contexto longo: O GPT-5.2 Thinking é considerado o que há de mais moderno em avaliações de raciocínio de contexto longo (OpenAI MRCRv2), com implicações práticas para trabalhar de forma coerente em documentos muito longos e entradas com vários arquivos.
- Execução em várias etapas: Esse modelo é ótimo pra projetos complexos com várias etapas, incluindo chamadas de ferramentas mais fortes e comportamento de longo prazo, que se encaixam direitinho no nosso planejamento com esquema fixo e ciclo de renderização determinístico.
- Melhoria na veracidade: O GPT-5.2 Thinking dá menos respostas erradas do que o GPT-5.1 Thinking, o que é útil aqui porque diminui as chances de gerar textos/números que podem se desviar dos nossos intervalos do Excel citados.
- Compromissos de latência: Na API, você pode usar o GPT-5.2 Instant para um feedback rápido e, depois, mudar para o GPT-5.2 Thinking ou Pro quando quiser um planejamento completo e artefatos de melhor qualidade, lembrando que as gerações mais complexas podem demorar alguns minutos.
Projeto de exemplo 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 integrado a um aplicativo Streamlit. Em um nível mais alto, o aplicativo Streamlit final faz o seguinte:
-
Ele lê CSV e cria uma pasta de trabalho padrão.
-
Então, ele chama o GPT-5.2 via OpenAI ou OpenRouter com saídas estruturadas (
response_format: json_schema). -
Valida o plano retornado em relação ao nosso esquema
-
Converte gráficos para o Excel e os personaliza de acordo com a paleta de cores da nossa marca.
-
Por fim, ele gera um PowerPoint com gráficos embutidos e citações.

Vamos construir passo a passo.
Passo 1: Pré-requisitos e configuração
Antes de criar pastas de trabalho do Excel e apresentações do PowerPoint com o GPT-5.2, precisamos configurar um ambiente local. Vamos começar instalando as bibliotecas principais e definindo as chaves API como variáveis de ambiente.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
Vamos usar o Streamlit para criar a interface interativa da web, Pandas para carregar e preparar o CSV, Pydantic para validar o plano JSON com esquema bloqueado do GPT-5.2. Também usamos python-dotenv para carregar com segurança chaves como OPENROUTER_API_KEY/OPENAI_API_KEY a partir de um arquivo .env . Os solicitações chama o endpoint compatível com OpenRouter/OpenAI, enquanto openpyxl gera a pasta de trabalho e os gráficos do Excel, e o python-pptx cria a apresentação do PowerPoint com gráficos nativos (editáveis).
Depois, vamos definir nossas chaves API:
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Vale lembrar que dá pra acessar o GPT 5.2 por vários serviços, tipo a API oficial da OpenAI e outros serviços como o OpenRouter e mais.
É recomendável usar um arquivo .env para guardar as chaves da API localmente:
No arquivo .env, salve o seguinte:
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
Depois, carregue essas chaves API usando o load_dotenv :
from dotenv import load_dotenv
load_dotenv()
Agora, nosso ambiente está pronto para fazer a autenticação com a API OpenAI.
Passo 2: Definindo o esquema do plano
Depois, a gente define um plano de artefato com esquema fixo que consiste em um blueprint JSON bem rígido que diz ao GPT-5.2 exatamente quais gráficos criar e como estruturar os slides, pra que a saída do modelo possa ser decodificada e validada usando Saídas Estruturadas 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 de código 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áficos a uma pequena enumeração,ChartSpeccaptura o contrato completo do gráfico eSlideSpecdefine o layout do slide, vinculando opcionalmente um slide a um gráfico por meio dechart_id. -
Aplicação automática da qualidade: As restrições, como
Field(), garantem que sempre tenhamos gráficos/slides suficientes, que cada slide tenha pontos suficientes e que cada ponto inclua citações. -
Planejamento e renderização: O GPT-5.2 só faz o plano, tipo gráficos, slides e citações, como JSON estruturado, enquanto o resto do código cuida da execução, formatação do Excel, renderização de gráficos e layout do deck.
Com esse esquema em vigor, o GPT-5.2 se torna um planejador que produz um projeto JSON validado.
Passo 3: Chame o GPT-5.2 com saídas estruturadas
Com o esquema do plano definido, o próximo passo é chamar o GPT-5.2 no “modo esquema”— para que ele retorne um blueprint JSON que você possa validar e executar. Em vez de esperar que o modelo “se comporte”, dizemos à API paradecodificar diretamente no seu JSON Schema usando response_format: { type: "json_schema", ... strict: true }, que é exatamente para isso que servem as saídas estruturadas.
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() ` é o único gateway que transforma prompts em um plano validado:
-
Roteamento do provedor (OpenRouter vs OpenAI): Se o nome do modelo tiver um namespace (openai/gpt-5.2), você vai usar o OpenRouter por padrão, porque esse é o formato que o OpenRouter usa para o roteamento do modelo. Se você definir explicitamente api_provider="openai", você vai mandar a mesma carga útil do Chat Completions para o endpoint da OpenAI. Fique à vontade para ignorar completamente o OpenRouter.
-
Definindo pontos finais: Para o OpenAI, você chama o endpoint
https://api.openai.com/v1/chat/completionscom autenticação Bearer padrão. Para o OpenRouter, você chama o endpointhttps://openrouter.ai/api/v1/chat/completionse, se quiser, inclui cabeçalhos de atribuição como HTTP-Referer e X-Title, que o OpenRouter documenta como metadados recomendados. -
Decodificação do esquema através de
response_format: O bloco de carga útil principal éresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, que diz ao modelo para criar uma saída que siga nosso esquema JSON.
Depois, vamos validar o plano JSON do modelo com o Pydantic e, só se ele passar nas verificações de limites, vamos renderizar os gráficos do Excel e gerar a apresentação do PowerPoint.
Passo 4: Funções auxiliares
Antes de renderizar qualquer coisa, precisamos de uma pequena camada que torne qualquer CSV carregado seguro para análise e consistente para referência. Essas funções auxiliares fazem quatro coisas: carregam e limpam o CSV, criam uma tabela de modelo derivada, calculam um conjunto compacto de fatos para o prompt e tornam a saída do Excel legível com colunas de tamanho automático.
Passo 4.1: Carregar e validar 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 tiver campos numéricos armazenados como strings. Essa função padroniza o conjunto de dados para que as etapas posteriores 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 usando heurística de nome (data, mês, semana, período) e as transforma em uma string. Isso mantém as etiquetas estáveis nos gráficos e no texto dos slides.
- Por fim, ele transforma as colunas que não são de data em numéricas usando
pd.to_numerice preenche os valores numéricos que faltam com zero pra evitar NaN em cascata mais tarde.
No final dessa etapa, você vai ter um DataFrame que não está vazio e tem colunas numéricas.
Passo 4.2: Calcule o DataFrame do modelo
Os CSV brutos são confusos e inconsistentes entre os domínios. A folha de modelo funciona como uma camada de normalização onde adicionamos métricas derivadas que são úteis para a criação de gráficos, sem que o usuário precise calculá-las 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 bem úteis:
-
Ele procura uma coluna de valor de saída primária e uma coluna de entrada secundária e calcula uma razão simples para capturar quanto do sinal primário permanece após contabilizar o sinal secundário.
-
Para um punhado de colunas numéricas, ele calcula
pct_change(), que retorna a variação fracionária da linha anterior por padrão, o que se torna uma base fácil para capturar o movimento em qualquer métrica de série temporal.
Passo 4.3: Calcular fatos
Mesmo com saídas estruturadas, ainda queremos dar ao GPT-5.2 um resumo compacto do que mudou ao longo do período. Esses fatos também funcionam como o prompt que o modelo pode usar de novo nos marcadores 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 linha para definir a janela do período. Depois, escolhemos uma coluna (data, mês, semana, período) para relatórios que a gente consegue entender.
Para um pequeno conjunto de colunas numéricas, calculamos um instantâneo do início, fim, média e variação percentual geral do início ao fim. Isso cria um dicionário leve de sinais que ajuda o modelo a escrever tópicos de slides.
Passo 4.4: Dimensionar automaticamente as colunas do Excel
As larguras padrão das colunas do Excel fazem com que as pastas de trabalho geradas pareçam desorganizadas. O redimensionamento automático é uma etapa simples de aperfeiçoamento que melhora imediatamente a usabilidade.
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() ` examina cada coluna, encontra o comprimento máximo da string e define ` ws.column_dimensions[col_letter].width`. Então, a biblioteca OpenPyXL mostra o column_dimensions e sua propriedade width pra controlar o tamanho da exibição das colunas no Excel.
Depois, a gente coloca a tabela do modelo limpa e os fatos no prompt de planejamento do GPT-5.2, valida o plano que aparece e, então, gera gráficos e slides.
Passo 5: Crie a pasta de trabalho
Usando a tabela do modelo limpa e os fatos resumidos leves da etapa anterior, agora geramos uma pasta de trabalho determinística do Excel com o 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
E aí, o que esse criador de pasta de trabalho faz?
-
Crie a estrutura da pasta de trabalho: O método `
Workbook()` cria um novo objeto pasta de trabalho e, em seguida, o método `wb.remove(wb.active)` exclui a planilha padrão para que possamos controlar totalmente a ordem das planilhas. Novas planilhas são adicionadas explicitamente comwb.create_sheet("Raw"),wb.create_sheet("Model")ewb.create_sheet("Charts"). -
Escreva a planilha Raw: Escrevemos uma linha de cabeçalho usando Font, PatternFill e Alignment e, em seguida, adicionamos todas as linhas do raw_df. A função .
autosize_columns(ws_raw)ajuda na legibilidade sem precisar ajustar o tamanho manualmente. -
Limites de computação: Usamos
row_end = 1 + len(raw_df), que nos dá uma última linha estável para que possamos gerar com segurança intervalos comoA2:A{row_end}para categorias de gráficos e citações. -
Aplique heurísticas de formato numérico: Depois, definimos o
cell.number_formatcom base nas palavras-chave do nome da coluna e no tipo de dados para que o Excel exiba os valores como moeda, porcentagens ou números simples. -
Crie uma planilha de gráficos: O
wb.create_sheet("Gráficos") nos dá uma tela estável para âncoras de gráficos. Isso combina com o fluxo de trabalho do gráfico openpyxl, onde criamos um LineChart ou BarChart, vinculamos intervalos por meio deReference, definimos categorias comset_categoriese, em seguida, posicionamos comws.add_chart().
Depois dessa etapa, você vai ter um arquivo Excel consistente que a próxima etapa pode usar com segurança como referência por intervalos de células exatos.
Etapa 6: Gerar gráficos
Agora que a estrutura da pasta de trabalho está pronta, podemos colocar os gráficos na planilha Gráficos e dar um visual consistente à marca. Usei a paleta de cores do 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 mecanismo de renderização de gráficos que transforma nosso plano de esquema em visuais do Excel com a marca.
-
Carregue a pasta de trabalho: Começamos abrindo o arquivo Excel gerado com o
load_workbook(xlsx_path). Depois, criamos uma tabela de colunas (header_map) a partir da primeira linha, mapeando cada nome de coluna para o seu índice de coluna no Excel. -
Criar referências de intervalo de células: 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, em seguida, definir categorias. -
Instancie o tipo de gráfico: Para cada especificação de gráfico, criamos um
LineChart()ou umBarChart()e definimos ochart.title. -
Adicione a paleta da marca: Para cada série na especificação, vinculamos o intervalo Y com
chart.add_data()e, em seguida, definimos o rótulo de exibição usandoSeriesLabel().
Por fim, a função ws_charts.add_chart() coloca cada gráfico em uma célula específica no canto superior esquerdo.
Passo 7: Renderizar o PowerPoint
É nessa etapa que o plano vira uma apresentação de slides de verdade. A gente cria um PowerPoint, organiza títulos e marcadores, coloca gráficos feitos a partir da mesma tabela que usamos no Excel e adiciona um rodapé que mantém as referências às células do plano.
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 gráficos de deck e índice:
prs = Presentation()abre um novo PPTX, enquantoblank = prs.slide_layouts[6]nos dá uma tela em branco para posicionamento preciso. -
Títulos e marcadores: A função
add_title()escreve em uma caixa de textotext_frame, eadd_bullets()adiciona um parágrafo por marcador enquanto controla o tamanho da fonte e o espaçamento por meio da formatação do parágrafo. -
Notas de rodapé: A função “
add_sources_footer()” elimina citações repetidas entre os marcadores e cria uma linha compacta “Sources:” na parte inferior, para que a rastreabilidade fique intacta sem sobrecarregar o slide. -
Incorporar gráficos:
add_chart()cria um objetoCategoryChartData()usando uma coluna X semelhante ao tempo e os valores da série planejada e, em seguida, insere-o por meio deslide.shapes.add_chart(), que retorna umGraphicFrame. -
Estilo da marca: Por fim, a gente converte hexadecimal para
RGBColore aplica por série. -
Regras e diretrizes de layout: Se um slide tiver um
chart_idválido, a gente usa um layout de duas colunas, caso contrário, usamos marcadores de largura total. A funçãobuild_allowed_ranges()cria a lista de permissões de intervalos citáveis, evalidate_plan()impõe IDs de gráficos exclusivos, referências válidas, citações permitidas e métricas válidas para que o modelo não possa inventar campos.
Passo 8: Crie o plano com o LLM
Neste ponto, temos dados tabulares limpos, que vamos transformar em um plano de construção de esquema (gráficos, slides e citações) que o renderizador pode 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 desta demonstração. Veja como a gente adiciona isso ao pipeline:
-
Explicit JSON Schema: O
plan_schemadefine o único formato de resposta que o modelo pode gerar,charts[]eslides[], com campos e enums obrigatórios. -
Proteções de prompt do sistema: Codificamos itens não negociáveis para garantir que o plano continue viável e auditável. Também limitamos a colocação do gráfico a células simples, para que o layout do gráfico continue sendo determinístico, em vez de improvisado pelo modelo. Esse tipo de controle em várias etapas é exatamente o que o GPT-5.2 foi feito pra lidar.
-
Mensagem do usuário: A mensagem do usuário fornece a meta, as contagens solicitadas, os fatos calculados e as chaves métricas permitidas. Como o modelo nunca vê nada mais, ele tem muito menos maneiras de inventar colunas, intervalos ou alegações sem fundamento.
-
Decodificação aderente ao esquema: Chamamos
chat_json_schema()comresponse_formatdefinido comojson_schemaestrict: true, o que garante a conformidade com o esquema durante a decodificação. -
Valide em duas camadas: Primeiro, a função `
ArtifactPlan.model_validate(plan_json)` confirma que a resposta bate com os nossos modelos Pydantic. Então, ovalidate_plan()aplica restrições de domínio.
Passo 9: Executar pipeline
Por fim, conectamos o carregamento de CSV, a geração do Excel, o planejamento LLM, a renderização do gráfico e a exportação final do PPTX, e depois devolvemos 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 todos os componentes do nosso pipeline passo a passo:
- Resultados estruturados do pipeline: O `
@dataclass` cria um contêiner leve para `xlsx_path`, `pptx_path`, `plan_path` e `logs`, para que a interface do usuário do Streamlit possa tratar o resultado do pipeline como um único valor estruturado, em vez de manipular tuplas ou globais. - Roteamento da chave API: O método `
load_dotenv()` carrega nosso arquivo `.env` nas variáveis de ambiente, para que `OPENAI_API_KEY` e `OPENROUTER_API_KEY` possam ser lidos através de `os.getenv()`. Então, escolhemos a chave do provedor usando uma regra simples, ou seja, seOPENROUTER_API_KEYexistir, usamos o OpenRouter; caso contrário, voltamos ao OpenAI, se disponível. - Execute a preparação dos dados: Carregamos e validamos o CSV, calculamos nossa tabela de modelo derivada e, em seguida, criamos o esqueleto do Excel através de
build_workbook_base(). A funçãogenerate_plan_with_llm()retorna umArtifactPlanque é salvo como JSON usandojson.dump(), facilitando a inspeção e a depuração. - Renderizar os artefatos: A função add_charts_to_workbook() transforma os gráficos do Excel na planilha Charts, e a função
build_pptx()cria a apresentação de slides a partir do plano e da tabela.
Depois dessa etapa, nosso aplicativo Streamlit pode chamar run_pipeline(), mostrar o logs e disponibilizar três resultados determinísticos para download, incluindo a pasta de trabalho do Excel, a apresentação PPTX e o plano JSON.
Passo 10: Interface do usuário Streamlit
Agora que o pipeline principal está pronto, o último passo é colocá-lo em um aplicativo Streamlit para que qualquer pessoa possa enviar um arquivo, definir uma meta, 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, a gente define um quadro de aplicativo com st.set_page_config( layout="wide") e, em seguida, divide a tela usando st.columns() para que o lado esquerdo receba as entradas, enquanto o lado direito mostra os controles de execução e as saídas. O pipeline só rola quando o usuário clica em “ st.button() ” (Enviar arquivo), onde primeiro a gente verifica se não tem nenhum upload faltando, faz o “wrap” em “ st.spinner() ” (Enviar arquivo) pra interface do usuário não parecer travada e, depois, manda os artefatos de volta pro usuário com “ st.download_button() ” (Enviar arquivo) pro Excel, PowerPoint e arquivo de plano.
Depois de fazer isso, salvamos tudo como app.py e iniciamos a experiência completa com:
streamlit run app.pyConclusão
Neste tutorial, criamos um pipeline completo de “CSV para artefatos” que usa o GPT-5.2 para gerar um plano com gráficos, estrutura de slides e citações de intervalos de células. Depois, a gente valida esse plano antes de gerar qualquer coisa. Essa abordagem de planejar e depois renderizar combina bem com os pontos fortes do GPT-5.2 em fluxos de trabalho com várias etapas, como planilhas e apresentações.
O resultado é um fluxo de trabalho que dá pra repetir, onde o usuário manda um CSV, o código cria uma base de pasta de trabalho limpa, o GPT-5.2 faz um plano que só pode citar intervalos do Excel que estão na lista de permissões e o renderizador transforma gráficos e slides usando openpyxl e python-pptx.
A partir daqui, você pode ampliar a demonstração sem mexer na lógica principal, como adicionar mais tipos de gráficos, introduzir modelos de slides, oferecer suporte a junções multi-CSV, etc. O padrão principal continua o mesmo, ou seja, tratar o modelo como um planejador que precisa seguir um esquema e manter toda a lógica de renderização no código para que os resultados continuem consistentes e possam ser depurados.

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.