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.
O GPT-5.2 é o modelo mais recente da OpenAIda 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.
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).
Depois, carregue essas chaves de API usando a biblioteca load_dotenv:
from dotenv import load_dotenv
load_dotenv()Tecnologia
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"]defsafe_float(x)->float:try:if x isNone:return0.0ifisinstance(x,(int,float)):returnfloat(x)returnfloat(str(x).replace(",","").replace("$",""))except Exception:return0.0deffmt_money(v:float)->str:
sign ="-"if v <0else""
v =abs(v)if v >=1_000_000:returnf"{sign}${v/1_000_000:.2f}M"if v >=1_000:returnf"{sign}${v/1_000:.1f}K"returnf"{sign}${v:.0f}"deffmt_pct(v:float)->str:if v isNoneor(isinstance(v,float)and(math.isnan(v)or math.isinf(v))):return"n/a"returnf"{v*100:.1f}%"classChartSeriesSpec(BaseModel):
name:str
metric:strclassChartSpec(BaseModel):id:str
kind: ChartKind
title:str
x_metric:str
series: List[ChartSeriesSpec]
anchor:strclassSlideBullet(BaseModel):
text:str
citations: List[str]= Field(min_length=1, max_length=4)classSlideSpec(BaseModel):
title:str
chart_id: Optional[str]=None
bullets: List[SlideBullet]= Field(min_length=2, max_length=4)classArtifactPlan(BaseModel):
charts: List[ChartSpec]= Field(min_length=3, max_length=6)
slides: List[SlideSpec]= Field(min_length=4, max_length=10)Tecnologia
O trecho acima define o plano usando modelos Pydantic, que fazem três coisas importantes:
Estrutura do plano:ChartKind = Literal["line", "bar"] limita os tipos de gráfico a um pequeno enum, ChartSpec captura o contrato completo do gráfico, e SlideSpec define o layout do slide, vinculando opcionalmente um slide a um gráfico via chart_id.
Garantia automática de qualidade: as restrições com Field() garantem que sempre tenhamos gráficos/slides suficientes, que cada slide tenha bullets suficientes e que cada bullet inclua citações.
Planejamento e renderização: o GPT-5.2 produz apenas o blueprint — ou seja, gráficos, slides e citações — como JSON estruturado, enquanto o restante do código cuida da execução, formatação no Excel, renderização dos gráficos e layout do deck.
Com esse schema, o GPT-5.2 vira um planejador que gera um blueprint JSON validado.
Passo 3: chame o GPT-5.2 com Structured Outputs
Com o schema do plano definido, o próximo passo é chamar o GPT-5.2 em “modo schema” — assim ele retorna um blueprint em JSON que você pode validar e executar. Em vez de torcer para o modelo “se comportar”, nós pedimos à API para decodificar diretamente no seu JSON Schema usando response_format: { type: "json_schema", ... strict: true }, que é exatamente o propósito de Structured Outputs.
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/completions com Bearer auth padrão. Para OpenRouter, você chama https://openrouter.ai/api/v1/chat/completions e, opcionalmente, inclui cabeçalhos de atribuição como HTTP-Referer e X-Title, conforme a documentação do OpenRouter.
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.
defload_and_validate_csv(csv_path:str)-> pd.DataFrame:
df = pd.read_csv(csv_path)iflen(df)==0:raise ValueError("CSV is empty")iflen(df.columns)==0:raise ValueError("CSV has no columns")
date_cols =[c for c in df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]if date_cols:
date_col = date_cols[0]
df[date_col]= df[date_col].astype(str)for col in df.columns:if col notin date_cols:try:
df[col]= pd.to_numeric(df[col])if df[col].dtype in['float64','int64']:
df[col]= df[col].fillna(0)except(ValueError, TypeError):passreturn dfTecnologia
A função acima:
Lê o arquivo em um DataFrame usando pd.read_csv.
Detecta colunas por heurísticas de nome (date, month, week, period) e as força para string. Isso mantém os rótulos estáveis em gráficos e textos de slide.
Por fim, converte colunas não temporais em numéricas com pd.to_numeric e preenche valores numéricos ausentes com zero para evitar cascatas de NaN depois.
Ao fim deste passo, você tem um DataFrame não vazio e com colunas numéricas.
Passo 4.2: calcular o DataFrame do modelo
CSVs brutos são bagunçados e inconsistentes entre domínios. A planilha model atua como uma camada de normalização onde adicionamos métricas derivadas úteis para gráficos, sem exigir que o usuário as calcule manualmente.
defcompute_model_df(raw_df: pd.DataFrame)-> pd.DataFrame:
df = raw_df.copy()
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()if"new_mrr"in df.columns and"churned_mrr"in df.columns:
df["net_new_mrr"]= df["new_mrr"]- df["churned_mrr"]
revenue_cols =[c for c in df.columns ifany(x in c.lower()for x in['revenue','mrr','income','sales'])]
cost_cols =[c for c in df.columns ifany(x in c.lower()for x in['cost','cogs','expense'])]if revenue_cols and cost_cols:
revenue = df[revenue_cols[0]]
cost = df[cost_cols[0]]
df["gross_margin"]=((revenue - cost)/ revenue.replace(0, pd.NA)).fillna(0)for col in numeric_cols[:5]:if col notin df.columns:continue
df[f"{col}_change"]= df[col].pct_change().fillna(0)return dfTecnologia
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.
defcompute_facts(model_df: pd.DataFrame)-> Dict[str,str]:defdelta(a:float, b:float)->float:return0.0if a ==0else(b - a)/ a
iflen(model_df)==0:return{"error":"No data"}
first = model_df.iloc[0]
last = model_df.iloc[-1]
date_cols =[c for c in model_df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]
period_col = date_cols[0]if date_cols else model_df.columns[0]
facts ={"period":f"{first[period_col]} → {last[period_col]}","row_count":len(model_df),"column_count":len(model_df.columns),}
numeric_cols = model_df.select_dtypes(include=[np.number]).columns.tolist()for i, col inenumerate(numeric_cols[:5]):if col in first.index and col in last.index:
start_val = safe_float(first[col])
end_val = safe_float(last[col])
avg_val = safe_float(model_df[col].mean())
facts[f"{col}_start"]= fmt_money(start_val)if start_val >=100elsef"{start_val:.2f}"
facts[f"{col}_end"]= fmt_money(end_val)if end_val >=100elsef"{end_val:.2f}"
facts[f"{col}_change_pct"]= fmt_pct(delta(start_val, end_val))
facts[f"{col}_avg"]= fmt_money(avg_val)if avg_val >=100elsef"{avg_val:.2f}"return factsTecnologia
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.
defautosize_columns(ws, max_width:int=45):for col in ws.columns:
max_len =0
col_letter = get_column_letter(col[0].column)for cell in col:if cell.value isNone:continue
max_len =max(max_len,len(str(cell.value)))
ws.column_dimensions[col_letter].width =min(max_len +2, max_width)Tecnologia
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.
defbuild_workbook_base(raw_df: pd.DataFrame, model_df: pd.DataFrame, out_xlsx:str)-> Tuple[str,int]:
wb = Workbook()
wb.remove(wb.active)
header_fill = PatternFill("solid", fgColor="F2F2F2")
header_font = Font(bold=True)
ws_raw = wb.create_sheet("Raw")for j, col inenumerate(raw_df.columns, start=1):
c = ws_raw.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")for i, row inenumerate(raw_df.itertuples(index=False), start=2):for j, val inenumerate(row, start=1):
ws_raw.cell(row=i, column=j, value=val)
ws_raw.freeze_panes ="A2"
autosize_columns(ws_raw)
row_end =1+len(raw_df)
ws_model = wb.create_sheet("Model")for j, col inenumerate(model_df.columns, start=1):
c = ws_model.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")for i, row inenumerate(model_df.itertuples(index=False), start=2):for j, val inenumerate(row, start=1):
ws_model.cell(row=i, column=j, value=val)
ws_model.freeze_panes ="A2"
autosize_columns(ws_model)
col_index ={name: idx +1for idx, name inenumerate(model_df.columns)}
money_keywords =['mrr','revenue','income','sales','cost','expense','cash','balance','burn','price','amount','fee']
pct_keywords =['margin','rate','ratio','pct','percent','change','growth']for col_name, col_idx in col_index.items():
col_lower = col_name.lower()
is_money =any(kw in col_lower for kw in money_keywords)
is_pct =any(kw in col_lower for kw in pct_keywords)or'margin'in col_lower
is_large_number =Falseif col_name in model_df.columns:
sample_vals = model_df[col_name].dropna()iflen(sample_vals)>0and pd.api.types.is_numeric_dtype(sample_vals):
max_val =abs(sample_vals.max())
is_large_number = max_val >1000for r inrange(2, row_end +1):if is_money or is_large_number:
ws_model.cell(row=r, column=col_idx).number_format ="$#,##0"elif is_pct:
ws_model.cell(row=r, column=col_idx).number_format ="0.0%"elif pd.api.types.is_numeric_dtype(model_df[col_name]):
ws_model.cell(row=r, column=col_idx).number_format ="0.0"
ws_charts = wb.create_sheet("Charts")
ws_charts["A1"]="Charts"
ws_charts["A1"].font = Font(bold=True)
wb.save(out_xlsx)return out_xlsx, row_endTecnologia
O que este gerador de pasta faz:
Criar o esqueleto da pasta: Workbook() instancia uma nova pasta e wb.remove(wb.active) remove a planilha padrão para controlarmos totalmente a ordem. Novas abas são adicionadas com wb.create_sheet("Raw"), wb.create_sheet("Model") e wb.create_sheet("Charts").
Escrever a aba Raw: criamos a linha de cabeçalho com Font, PatternFill e Alignment e depois incluímos todas as linhas de raw_df. A função autosize_columns(ws_raw) melhora a leitura sem precisar redimensionar manualmente.
Calcular limites: usamos row_end = 1 + len(raw_df), o que nos dá a última linha estável para gerar intervalos como A2:A{row_end} para categorias de gráficos e citações.
Aplicar heurísticas de formatação numérica: definimos cell.number_format com base em palavras-chave no nome da coluna e no dtype para exibir valores no Excel como moeda, porcentagem ou número.
Criar a aba Charts: wb.create_sheet("Charts") oferece um canvas estável para âncoras de gráfico. Isso acompanha o fluxo do openpyxl, no qual criamos um LineChart ou BarChart, vinculamos intervalos via Reference, definimos categorias com set_categories e posicionamos com ws.add_chart().
Depois deste passo, você tem um arquivo do Excel consistente que a próxima etapa pode referenciar com exatidão por intervalos de células. Passo 6: renderizar gráficos
Com a estrutura da pasta pronta, podemos renderizar os gráficos na aba Charts e aplicar uma identidade visual consistente. Apliquei a paleta de cores da DataCamp com:
Cor primária: #01ef63
Cor secundária: #203147
defadd_charts_to_workbook(xlsx_path:str, charts: List[ChartSpec], metric_to_range: Dict[str,str]):
wb = load_workbook(xlsx_path)
ws_model = wb["Model"]
ws_charts = wb["Charts"]
header_map ={ws_model.cell(1, c).value: c for c inrange(1, ws_model.max_column +1)}defref_from_metric(metric:str, row_start:int, row_end:int)-> Reference:if metric notin header_map:raise ValueError(f"Unknown metric in Model sheet: {metric}")
col = header_map[metric]return Reference(ws_model, min_col=col, min_row=row_start, max_col=col, max_row=row_end)for ch in charts:
chart = LineChart()if ch.kind =="line"else BarChart()
chart.title = ch.title
x_metric = ch.x_metric
if x_metric notin header_map:
date_keywords =['date','time','month','year','period','week']
date_cols =[col for col in header_map.keys()if col andany(kw instr(col).lower()for kw in date_keywords)]if date_cols:
x_metric = date_cols[0]else:
x_metric =list(header_map.keys())[0]if header_map else"month"
x_ref = ref_from_metric(x_metric,2, ws_model.max_row)
chart.set_categories(x_ref)
chart_colors =["01EF63","203147"]for i, s inenumerate(ch.series):
y_ref = ref_from_metric(s.metric,2, ws_model.max_row)
chart.add_data(y_ref, titles_from_data=False)
chart.series[-1].title = SeriesLabel(v=s.name)
series = chart.series[-1]
color_hex = chart_colors[i %len(chart_colors)]try:if ch.kind =="line":
series.graphicalProperties.line.solidFill = color_hex
series.graphicalProperties.line.width =30000else:
series.graphicalProperties.solidFill = color_hex
except AttributeError:pass
anchor = ch.anchor.strip()if"!"in anchor:
anchor = anchor.split("!")[-1]if anchor and anchor.isalpha():
anchor =f"{anchor}2"ifnot re.match(r'^[A-Z]+[0-9]+Tecnologia
A função add_charts_to_workbook() é o motor de renderização que transforma nosso plano em visuais de Excel com a identidade da marca.
Carregar a pasta: começamos abrindo o arquivo gerado com load_workbook(xlsx_path). Em seguida, criamos um header_map a partir da primeira linha, mapeando cada nome de coluna ao seu índice de coluna no Excel.
Construir referências de intervalo: a função Reference() define o intervalo exato a ser plotado, e chart.set_categories(x_ref) conecta as categorias do eixo X. O padrão é criar objetos de referência, adicionar dados e definir categorias.
Instanciar o tipo de gráfico: para cada spec, criamos um LineChart() ou BarChart() e definimos chart.title.
Aplicar a paleta da marca: para cada série, vinculamos o eixo Y com chart.add_data() e definimos o rótulo com SeriesLabel().
Por fim, a função ws_charts.add_chart() posiciona cada gráfico em uma célula específica no canto superior esquerdo.
Passo 7: renderizar o PowerPoint
Aqui o plano vira um deck real. Criamos um PowerPoint, definimos títulos e bullets, incorporamos gráficos construídos a partir da mesma tabela usada no Excel e adicionamos um rodapé que preserva as citações por intervalo de células.
defbuild_pptx(out_pptx:str, model_df: pd.DataFrame, plan: ArtifactPlan):
prs = Presentation()
blank = prs.slide_layouts[6]
chart_by_id ={c.id: c for c in plan.charts}defadd_title(slide, title:str):
box = slide.shapes.add_textbox(Inches(0.5), Inches(0.2), Inches(9.0), Inches(0.6))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = title
p.font.size = Pt(32)
p.font.bold =Truedefadd_bullets(slide, bullets: List[SlideBullet], left, top, width, height):
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.word_wrap =True
tf.clear()for i, b inenumerate(bullets):
p = tf.paragraphs[0]if i ==0else tf.add_paragraph()
p.text = b.text
p.font.size = Pt(18)
p.level =0
p.space_before = Pt(4)
p.space_after = Pt(8)defadd_sources_footer(slide, bullets: List[SlideBullet]):
sources =[]
seen =set()for b in bullets:for c in b.citations:if c notin seen:
seen.add(c)
sources.append(c)ifnot sources:return
text ="Sources: "+"; ".join(sources)
box = slide.shapes.add_textbox(Inches(0.5), Inches(6.8), Inches(9.0), Inches(0.4))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(9)
p.font.italic =Truedefadd_chart(slide, chart_spec: ChartSpec, left, top, width, height):
date_cols =[c for c in model_df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]
x_col = date_cols[0]if date_cols else model_df.columns[0]
categories = model_df[x_col].astype(str).tolist()
chart_data = CategoryChartData()
chart_data.categories = categories
chart_colors =["01EF63","203147"]for i, s inenumerate(chart_spec.series):
values = model_df[s.metric].apply(safe_float).tolist()
chart_data.add_series(s.name, values)
chart_type = XL_CHART_TYPE.LINE if chart_spec.kind =="line"else XL_CHART_TYPE.COLUMN_CLUSTERED
graphic_frame = slide.shapes.add_chart(chart_type, left, top, width, height, chart_data)
chart = graphic_frame.chart
chart.has_title =True
chart.chart_title.text_frame.text = chart_spec.title
chart.has_legend =True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout =Falsefrom pptx.dml.color import RGBColor
for i, series inenumerate(chart.series):
color_hex = chart_colors[i %len(chart_colors)]
color_rgb = RGBColor(int(color_hex[0:2],16),int(color_hex[2:4],16),int(color_hex[4:6],16))
fill = series.format.fill
fill.solid()
fill.fore_color.rgb = color_rgb
if chart_spec.kind =="line":
line = series.format.line
line.color.rgb = color_rgb
line.width = Pt(3)for s in plan.slides:
slide = prs.slides.add_slide(blank)
add_title(slide, s.title)
has_chart =bool(s.chart_id and s.chart_id in chart_by_id)if has_chart:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),)
add_chart(
slide,
chart_by_id[s.chart_id],
left=Inches(5.2),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),)else:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(9.0),
height=Inches(5.5),)
add_sources_footer(slide, s.bullets)
prs.save(out_pptx)defbuild_allowed_ranges(model_df: pd.DataFrame, row_end:int)-> Dict[str,str]:
ranges ={}for i, col inenumerate(model_df.columns, start=1):
col_letter = get_column_letter(i)
ranges[col]=f"Model!{col_letter}2:{col_letter}{row_end}"return ranges
defvalidate_plan(plan: ArtifactPlan, allowed_ranges: Dict[str,str])->None:
allowed_set =set(allowed_ranges.values())
chart_ids ={c.idfor c in plan.charts}iflen(chart_ids)!=len(plan.charts):raise ValueError("Duplicate chart ids in plan.")for s in plan.slides:if s.chart_id and s.chart_id notin chart_ids:raise ValueError(f"Slide references unknown chart_id: {s.chart_id}")for s in plan.slides:for b in s.bullets:for c in b.citations:if c notin allowed_set:raise ValueError(f"Citation not in allowed set: {c}")
allowed_metrics =set(allowed_ranges.keys())for c in plan.charts:if c.x_metric notin allowed_metrics:passfor s in c.series:if s.metric notin allowed_metrics:raise ValueError(f"Unknown series metric: {s.metric}")Tecnologia
Vamos entender o código acima, passo a passo:
Criar o deck e indexar gráficos: prs = Presentation() inicia um novo PPTX, enquanto blank = prs.slide_layouts[6] nos dá um canvas em branco para posicionamento preciso.
Títulos e bullets: a função add_title() escreve em uma caixa de texto text_frame, e add_bullets() adiciona um parágrafo por bullet controlando tamanho e espaçamento.
Rodapé de citações: add_sources_footer() deduplica citações entre bullets e renderiza uma linha compacta Sources: no rodapé, mantendo a rastreabilidade sem poluir o slide.
Gráficos embutidos: add_chart() constrói um CategoryChartData() usando uma coluna temporal no eixo X e os valores das séries planejadas, e insere com slide.shapes.add_chart().
Estilo da marca: por fim, convertemos o hex para RGBColor e aplicamos por série.
Regras de layout e proteções: se o slide tiver um chart_id válido, usamos layout em duas colunas; caso contrário, bullets em largura total. A função build_allowed_ranges() cria a lista permitida de intervalos citáveis, e validate_plan() impõe IDs únicos, referências válidas, citações permitidas e métricas conhecidas para o modelo não inventar campos.
Passo 8: gerar o plano com o LLM
Neste ponto, temos dados tabulares limpos, que vamos transformar em um plano de construção (gráficos, slides e citações) que o renderizador consegue executar.
defgenerate_plan_with_llm(
api_key:str,
model:str,
goal:str,
allowed_ranges: Dict[str,str],
facts: Dict[str,str],
num_slides:int=6,
num_charts:int=4,)-> ArtifactPlan:
plan_schema ={"type":"object","properties":{"charts":{"type":"array","minItems":3,"maxItems":6,"items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["line","bar"]},"title":{"type":"string"},"x_metric":{"type":"string"},"series":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","properties":{"name":{"type":"string"},"metric":{"type":"string"},},"required":["name","metric"],"additionalProperties":False,},},"anchor":{"type":"string"},},"required":["id","kind","title","x_metric","series","anchor"],"additionalProperties":False,},},"slides":{"type":"array","minItems":4,"maxItems":10,"items":{"type":"object","properties":{"title":{"type":"string"},"chart_id":{"type":["string","null"]},"bullets":{"type":"array","minItems":2,"maxItems":4,"items":{"type":"object","properties":{"text":{"type":"string"},"citations":{"type":"array","minItems":1,"maxItems":4,"items":{"type":"string"},},},"required":["text","citations"],"additionalProperties":False,},},},"required":["title","chart_id","bullets"],"additionalProperties":False,},},},"required":["charts","slides"],"additionalProperties":False,}
system =("You produce investor-grade slides with traceability.\n""Output MUST match the JSON schema exactly.\n""Rules:\n""1) Use ONLY the allowed citation ranges (exact strings).\n""2) No generic filler. Every bullet must contain at least ONE number (e.g., $X, X%, X mo).\n""3) Keep bullets short: <= 12 words.\n""4) Prefer trends (start→end) or averages; avoid speculation.\n""5) Provide exactly the requested number of charts and slides.\n""6) Use chart_id to attach the correct chart to each slide.\n""7) Chart anchors: Use cell references ONLY (e.g., 'A2', 'A20', 'A38'). Do NOT include sheet name like 'CHARTS!A'.\n")
user =(f"Goal: {goal}\n"f"Requested: {num_charts} charts and {num_slides} slides.\n\n"f"Facts (use these; do not invent):\n{json.dumps(facts, indent=2)}\n\n"f"Allowed citation ranges (use exact values only):\n{json.dumps(list(allowed_ranges.values()), indent=2)}\n\n""Metric keys available for charts:\n"f"{json.dumps(list(allowed_ranges.keys()), indent=2)}\n\n""Chart requirements:\n""- x_metric: Use a date/time column (e.g., 'month', 'date', 'period', 'week') for the X-axis.\n"" If no date column exists, use the first column from the available metrics.\n""- Create meaningful charts based on available metrics in the data.\n""- Focus on trends, comparisons, and key business metrics.\n""- Chart colors: Use these specific shades for all chart series:\n"" * Primary color: #01ef63 (bright green)\n"" * Secondary color: #203147 (dark blue/navy)\n"" * Alternate between these two colors for multiple series in the same chart.\n""Slide requirements:\n""- Create slides that tell a story with the available data.\n""- Suggested structure: Overview, Key Metrics, Trends, Analysis, Summary, Next Steps.\n""- Adapt slide titles and content to match the data domain (e.g., sales, marketing, operations, finance).\n")
messages =[{"role":"system","content": system},{"role":"user","content": user}]
plan_json = chat_json_schema(
model=model,
messages=messages,
json_schema=plan_schema,
api_key=api_key,
temperature=0.2,
max_tokens=6000,)try:
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)iflen(plan.slides)!= num_slides:raise ValueError(f"Expected {num_slides} slides, got {len(plan.slides)}")iflen(plan.charts)!= num_charts:raise ValueError(f"Expected {num_charts} charts, got {len(plan.charts)}")return plan
except(ValidationError, ValueError)as e:
repair = messages +[{"role":"assistant","content": json.dumps(plan_json)},{"role":"user","content":f"Fix JSON to satisfy schema + rules. Error:\n{str(e)}"},]
plan_json = chat_json_schema(
model=model,
messages=repair,
json_schema=plan_schema,
api_key=api_key,
temperature=0.0,
max_tokens=6000,)
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)iflen(plan.slides)!= num_slides orlen(plan.charts)!= num_charts:raise RuntimeError("Plan repaired but counts still wrong. Tighten prompt or increase max_tokens.")return planTecnologia
A função generate_plan_with_llm() é o cérebro deste demo. Veja como ela entra no pipeline:
JSON Schema explícito: o plan_schema define o único formato de resposta permitido ao modelo, com charts[] e slides[], campos obrigatórios e enums.
Regras no system prompt: codificamos não negociáveis para garantir que o plano permaneça renderizável e auditável. Também restringimos a âncora de gráfico a células simples, mantendo o layout determinístico, sem improviso do modelo. Esse controle multi-etapas é exatamente o ponto forte do GPT-5.2.
Mensagem do usuário: fornece o objetivo, as quantidades solicitadas, os fatos calculados e as chaves de métricas disponíveis. Como o modelo não vê mais nada, tem menos espaço para inventar colunas, intervalos ou afirmações sem suporte.
Decodificação aderente ao schema: chamamos chat_json_schema() com response_format em json_schema e strict: true, o que impõe a adesão ao schema durante a decodificação.
Validação em duas camadas: primeiro, ArtifactPlan.model_validate(plan_json) confirma que a resposta bate com nossos modelos Pydantic. Depois, validate_plan() impõe restrições de domínio.
Passo 9: executar o pipeline
Por fim, conectamos o carregamento do CSV, a geração do Excel, o planejamento com LLM, a renderização dos gráficos e a exportação final do PPTX, e então retornamos os caminhos de saída.
Vamos juntar os componentes do pipeline passo a passo:
Saídas estruturadas do pipeline: o @dataclass cria um contêiner leve para xlsx_path, pptx_path, plan_path e logs, para que a interface em Streamlit trate o resultado como um único valor estruturado, sem precisar manipular tuplas ou globais.
Roteamento de chaves da API: load_dotenv() carrega o arquivo .env em variáveis de ambiente para que OPENAI_API_KEY e OPENROUTER_API_KEY possam ser lidas com os.getenv(). Depois escolhemos o provedor com uma regra simples: se existir OPENROUTER_API_KEY, use OpenRouter; caso contrário, caia para OpenAI se disponível.
Preparar os dados: carregamos e validamos o CSV, calculamos a tabela model derivada e criamos o esqueleto do Excel com build_workbook_base(). A função generate_plan_with_llm() retorna um ArtifactPlan, que é salvo como JSON com json.dump() para facilitar inspeção e debug.
Renderizar os artefatos: add_charts_to_workbook() materializa os gráficos no Excel, e build_pptx() monta o deck a partir do plano e da tabela.
Depois deste passo, nosso app em Streamlit pode chamar run_pipeline(), exibir os logs e disponibilizar três saídas determinísticas para download: a pasta do Excel, o deck PPTX e o plano em JSON.
Passo 10: interface em Streamlit
Com o pipeline pronto, o passo final é empacotar tudo em um app Streamlit para qualquer pessoa enviar um arquivo, definir um objetivo, escolher um modelo e baixar os artefatos gerados.
load_dotenv()
st.set_page_config(
page_title="GPT-5.2 PPT and Excel Generator",
page_icon="",
layout="wide",
initial_sidebar_state="expanded")
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1f77b4;
margin-bottom: 1rem;
}
.sub-header {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.success-box {
padding: 1rem;
background-color: #d4edda;
border-left: 4px solid #28a745;
margin: 1rem 0;
}
.error-box {
padding: 1rem;
background-color: #f8d7da;
border-left: 4px solid #dc3545;
margin: 1rem 0;
}
.info-box {
padding: 1rem;
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
margin: 1rem 0;
}
.slide-preview {
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
""", unsafe_allow_html=True)defmain():try:if'outputs'notin st.session_state:
st.session_state.outputs =Noneif'generated'notin st.session_state:
st.session_state.generated =False
st.markdown('<div class="main-header">Spreadsheet to PowerPoint Generator</div>', unsafe_allow_html=True)with st.sidebar:# API Provider selection
api_provider = st.selectbox("API Provider",["OpenAI","OpenRouter"],help="Select which API to use. Make sure the corresponding API key is set in your .env file")# Model selection based on providerif api_provider =="OpenAI":
model = st.selectbox("AI Model",["gpt-4o","gpt-4-turbo","gpt-4","gpt-3.5-turbo",],help="OpenAI models. Requires OPENAI_API_KEY in .env file")else:# OpenRouter
model = st.selectbox("AI Model",["openai/gpt-5.2","openai/gpt-4-turbo","openai/gpt-4o","anthropic/claude-3-opus","anthropic/claude-3-sonnet",],help="OpenRouter models. Requires OPENROUTER_API_KEY in .env file")with st.expander("Advanced Settings"):
num_slides = st.slider("Number of Slides",4,10,6)
num_charts = st.slider("Number of Charts",3,6,4)
col1, col2 = st.columns([1,1])with col1:
st.markdown("### Input")
uploaded_file = st.file_uploader("Upload CSV",type=['csv'],help="Upload any CSV file. The app will automatically detect columns and create appropriate charts and slides.")
use_example = st.checkbox("Use example data (kpis.csv)", value=Trueifnot uploaded_file elseFalse)if uploaded_file or use_example:if use_example:
csv_path ="kpis.csv"
df = pd.read_csv(csv_path)else:
df = pd.read_csv(uploaded_file)with tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w')as f:
df.to_csv(f, index=False)
csv_path = f.name
with st.expander(" Preview Data", expanded=False):try:
st.dataframe(df, width='stretch')except Exception as e:
st.write("**Data Preview:**")
st.table(df.head(20))iflen(df)>20:
st.caption(f"Showing first 20 of {len(df)} rows")
st.caption(f"Rows: {len(df)} | Columns: {len(df.columns)}")else:
csv_path =None
st.info("Upload a CSV or use example data to continue")
goal = st.text_area("Goal",
value="Create a 6-slide investor update for Apr–Sep 2025. Focus on growth, retention, unit economics, and runway.",
height=100,help="Describe what you want the AI to create")
generate_btn = st.button("Generate Artifacts",type="primary", use_container_width=True)with col2:
st.markdown("### Output")
status_container = st.empty()
progress_bar = st.progress(0)ifnot generate_btn:
status_container.info("Configure settings and click 'Generate Artifacts'")if generate_btn:ifnot csv_path:
st.error("Please upload a CSV or select 'Use example data'")
st.stop()try:
status_container.info("Processing... This may take 30-60 seconds")
progress_bar.progress(10)
output_dir = Path("out")
output_dir.mkdir(exist_ok=True)
progress_bar.progress(20)with st.spinner("Generating artifacts..."):
outputs: RunOutputs = run_pipeline(
csv_path=csv_path,
goal=goal,
outdir=str(output_dir),
model=model,
num_slides=num_slides,
num_charts=num_charts
)
st.session_state.outputs = outputs
st.session_state.generated =True
progress_bar.progress(100)except Exception as e:
status_container.markdown(f'<div class="error-box"> <b>Error:</b> {str(e)}</div>',
unsafe_allow_html=True)
st.exception(e)
st.session_state.generated =Falseif st.session_state.generated and st.session_state.outputs:
outputs = st.session_state.outputs
try:if"Error"notin outputs.logs:
status_container.markdown('<div class="success-box"><b>Success!</b> Artifacts generated successfully</div>',
unsafe_allow_html=True)
tab1, tab2, tab3, tab4 = st.tabs(["PowerPoint","Excel","JSON Plan","Logs"])with tab1:
st.markdown("### PowerPoint Slides")
pptx_path = Path(outputs.pptx_path)if pptx_path.exists():try:
prs = Presentation(str(pptx_path))
slide_count =len(prs.slides)except:
slide_count ="unknown"withopen(pptx_path,"rb")as f:
st.download_button(
label="Download PowerPoint",
data=f.read(),
file_name="investor_update.pptx",
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation",
width='stretch',
key="pptx_download")
st.markdown("---")
col_info1, col_info2 = st.columns(2)with col_info1:
st.metric("Slides", slide_count)with col_info2:
file_size = pptx_path.stat().st_size /1024# KB
st.metric("File Size",f"{file_size:.1f} KB")else:
st.error("PowerPoint file not found")with tab2:
st.markdown("### Excel Workbook")
xlsx_path = Path(outputs.xlsx_path)if xlsx_path.exists():withopen(xlsx_path,"rb")as f:
st.download_button(
label="Download Excel",
data=f.read(),
file_name="investor_update.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
width='stretch',
key="xlsx_download")
wb = openpyxl.load_workbook(xlsx_path)
st.markdown("#### Data Preview")
ws = wb["Model"]
data =[]
headers =[cell.value for cell in ws[1]]for row in ws.iter_rows(min_row=2, values_only=True):
data.append(row)
df_preview = pd.DataFrame(data, columns=headers)try:
st.dataframe(df_preview.head(10), width='stretch')except Exception:
st.table(df_preview.head(10))iflen(df_preview)>10:
st.caption(f"Showing first 10 of {len[df_preview]} rows")else:
st.error("Excel file not found")with tab3:
st.markdown("### Schema-Locked JSON Plan")
plan_path = Path(outputs.plan_path)if plan_path.exists():withopen(plan_path,"r")as f:
plan_data = json.load(f)
st.download_button(
label="Download JSON",
data=json.dumps(plan_data, indent=2),
file_name="plan.json",
mime="application/json",
key="json_download")
st.markdown("---")
st.json(plan_data)
col_a, col_b = st.columns(2)with col_a:
st.metric("Charts Generated",len(plan_data.get("charts",[])))with col_b:
st.metric("Slides Generated",len(plan_data.get("slides",[])))else:
st.error("JSON plan not found")with tab4:
st.markdown("### Generation Logs")
st.code(outputs.logs, language="text")else:
status_container.markdown(f'<div class="error-box"> <b>Error occurred</b><br>{outputs.logs}</div>',
unsafe_allow_html=True)except Exception as e:
st.error(f"Error displaying results: {str(e)}")
st.exception(e)except Exception as e:
st.error(f"Fatal Error: {str(e)}")
st.exception(e)
st.info("Try installing dependencies: pip install -r requirements.txt")if __name__ =="__main__":try:
main()except Exception as e:import sys
print(f"Fatal error: {e}",file=sys.stderr)import traceback
traceback.print_exc()Tecnologia
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:
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.
GPT-4 alternativas de código aberto que podem oferecer desempenho semelhante e exigem menos recursos computacionais para serem executadas. Esses projetos vêm com instruções, fontes de código, pesos de modelos, conjuntos de dados e interface de usuário do chatbot.
Explore os melhores GPTs personalizados que vimos até agora na loja GPT, desde ferramentas de ciência de dados até assistentes de SEO e geração de imagens.
Saiba como o GPT-5 evoluirá para um sistema unificado com recursos avançados, visando um lançamento no verão de 2025, com base no mais recente roteiro da OpenAI e no histórico do GPT.
Este tutorial apresentará tudo o que você precisa saber sobre o GPT-4 Vision, desde o acesso a ele, passando por exemplos práticos do mundo real, até suas limitações.
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:
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:
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.
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_KEYde 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:
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
.envpara armazenar localmente as chaves da API:No arquivo
.env, salve o seguinte:Depois, carregue essas chaves de API usando a biblioteca 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.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.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.
A função acima:
pd.read_csv.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.
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.
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.
A função
autosize_columns()percorre cada coluna, encontra o maior comprimento de string e definews.column_dimensions[col_letter].width. A biblioteca OpenPyXL expõecolumn_dimensionse sua propriedadewidthpara 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.
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:
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.
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.
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.
Vamos juntar os componentes do pipeline passo a passo:
@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.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.build_workbook_base(). A funçãogenerate_plan_with_llm()retorna umArtifactPlan, que é salvo como JSON comjson.dump()para facilitar inspeção e debug.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 oslogse 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.
Na camada Streamlit, definimos a moldura do app com
st.set_page_config(layout="wide"), depois dividimos a tela comst.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 emst.button(). Primeiro evitamos faltas de upload, envolvemos emst.spinner()para a UI não parecer travada, e então entregamos os artefatos viast.download_button()para Excel, PowerPoint e o arquivo de plano.Com este passo concluído, salvamos tudo como
app.pye iniciamos a experiência completa com: