GPT-5.2 是一款适合生成需要可靠性、结构化与更少幻觉的工作流和多步流水线的强大模型。OpenAI 强调其在长上下文处理上的提升,并在电子表格类任务上表现更佳。
在本教程中,我们将构建一个像初级分析师那样工作的 Streamlit 应用:
上传 CSV 并写下目标(例如:“聚焦增长、留存、单位经济与资金跑道的 6 张更新幻灯片”)
生成 schema 锁定的 JSON 计划
生成包含原始数据、派生指标与图表的 Excel 工作簿
最后,生成带有正确引用的匹配 PowerPoint 文档
关键在于 GPT-5.2 并非直接“做幻灯片”,而是生成一个严格的计划,由您的代码以确定性方式执行。
另请阅读我们关于 OpenAI 更新模型的指南:GPT-5.5 、GPT-5.4 、GPT-5.3 Codex 与 GPT-5.3 Instant
什么是 GPT-5.2?
GPT-5.2 是 OpenAI 的 最新 GPT-5 系列 模型,具备在创建电子表格、构建演示文稿、编写与调试代码、理解长上下文与使用工具等端到端任务上的改进。
以下是本项目最相关的 GPT-5.2 特性:
长上下文推理 :据称 GPT-5.2 Thinking 在长上下文推理评测(OpenAI MRCRv2)中达到最先进水平,实用意义在于可在超长文档与多文件输入间保持连贯工作。
多步执行 :该模型擅长复杂的多步项目,包括更强的工具调用与长时程行为,直接对应我们的 schema 锁定规划与确定性渲染循环。
事实性提升 :GPT-5.2 Thinking 相较 GPT-5.1 Thinking 错误响应更少,有助于降低生成偏离我们 Excel 引用区域的文本/数字的概率。
时延权衡 :在 API 中,您可先用 GPT-5.2 Instant 快速迭代,再在需要强端到端规划与更高质量产物时切换到 GPT-5.2 Thinking 或 Pro,同时注意复杂生成可能耗时数分钟。
GPT-5.2 示例项目:构建结构化制品生成器
本节中,我们将用包裹在 Streamlit 应用中的 GPT 5.2 模型构建一个结构化制品生成器(Excel 与 PowerPoint)。总体而言,最终的 Streamlit 应用会:
读取 CSV 并构建规范化工作簿
随后通过 OpenAI 或 OpenRouter 使用结构化输出(response_format: json_schema)调用 GPT-5.2
依据我们的 schema 验证返回的计划
将图表渲染到 Excel 并按品牌配色进行样式化
最后生成内嵌带引用图表的 PowerPoint
让我们一步步动手构建。
步骤 1:先决条件与环境配置
在用 GPT-5.2 生成 Excel 工作簿与 PowerPoint 文档之前,我们需要搭建本地环境。先安装核心库并将 API 密钥设置为环境变量。
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx技术支持
我们将使用 Streamlit 构建交互式 Web 界面, Pandas 加载并预处理 CSV, Pydantic 验证 GPT-5.2 的 schema 锁定 JSON 计划。我们还会用 python-dotenv 从 .env 文件安全加载 OPENROUTER_API_KEY/OPENAI_API_KEY 等密钥。 requests 库调用 OpenRouter/OpenAI 兼容端点,而 openpyxl 生成 Excel 工作簿与图表, python-pptx 创建带原生(可编辑)图表的 PowerPoint 文档。
接着设置我们的 API 密钥:
export OPENAI_API_KEY = "your_key"
export OPENROUTER_API_KEY = "your_key" 技术支持
请注意,GPT 5.2 可通过多个服务访问,包括 OpenAI 官方 API 以及 OpenRouter 等服务。
建议使用 .env 文件在本地存储 API 密钥:
在 .env 文件内保存如下内容:
OPENROUTER_API_KEY = "your_key"
OPENAI_API_KEY = "your_key" 技术支持
然后使用 load_dotenv 库加载这些 API 密钥:
from dotenv import load_dotenv
load_dotenv( ) 技术支持
至此,我们的环境已准备好与 OpenAI API 进行鉴权。
步骤 2:定义计划的 schema
接下来,我们定义一个 schema 锁定的制品计划,它由严格的 JSON 蓝图组成,明确告知 GPT-5.2 要创建哪些图表以及如何组织幻灯片,因此模型的输出可通过 response_format: { type: "json_schema", strict: true } 的结构化输出来解码与验证,而不是自由文本。
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 ) 技术支持
上述代码片段用 Pydantic 模型定义了计划,带来三点重要作用:
计划结构: ChartKind = Literal["line", "bar"] 将图表类型限制在小型枚举内,ChartSpec 捕获完整图表契约,SlideSpec 定义幻灯片布局,并可通过 chart_id 将幻灯片关联到图表。
自动质量约束: 通过 Field() 等约束确保我们总能得到足够的图表/幻灯片、每张幻灯片有足够要点、每个要点包含引用。
规划与渲染分离: GPT-5.2 仅产出蓝图(即图表、幻灯片与引用)这一结构化 JSON,其余由代码负责执行、Excel 格式化、图表渲染与文档布局。
有了该 schema,GPT-5.2 就成为生成可验证 JSON 蓝图的规划器。
步骤 3:以结构化输出调用 GPT-5.2
定义好计划 schema 后,下一步是 以“schema 模式”调用 GPT-5.2 ——让其返回一个可验证并可执行的 JSON 蓝图。与其寄希望于模型“表现良好”,不如要求 API 直接解码为您的 JSON Schema ,使用 response_format: { type: "json_schema", ... strict: true },这正是结构化输出的用途。
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技术支持
函数 chat_json_schema() 是将提示转化为可验证计划的唯一网关:
提供商路由(OpenRouter vs OpenAI): 若模型名带命名空间(openai/gpt-5.2),默认走 OpenRouter,因为这是其模型路由格式。若显式设置 api_provider="openai",则将同样的 Chat Completions 负载发给 OpenAI 端点。您也可完全不使用 OpenRouter。
端点设置: 对于 OpenAI,调用 https://api.openai.com/v1/chat/completions 并使用标准 Bearer 认证。对于 OpenRouter,调用 https://openrouter.ai/api/v1/chat/completions,并可选加入如 HTTP-Referer 与 X-Title 等署名头,这是 OpenRouter 推荐的元数据。
通过 response_format 进行 schema 解码: 关键负载块为 response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } },指示模型产出受限于我们的 JSON Schema。
接着我们将用 Pydantic 验证模型生成的 JSON 计划,仅在通过边界检查后才渲染 Excel 图表并生成 PowerPoint 文档。
步骤 4:辅助函数
在渲染之前,我们需要一层薄薄的处理,让上传的 CSV 安全可分析且引用一致。这些辅助函数完成四项工作:加载并清洗 CSV、创建派生模型表、为提示计算一组简洁事实、并通过自动列宽让 Excel 输出更易读。
步骤 4.1:加载并验证 CSV
若输入表为空或数值字段以字符串存储,GPT 5.2 无法可靠规划图表与幻灯片。此函数标准化数据集,使下游步骤无需猜测。
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技术支持
上述函数会:
用 pd.read_csv 将文件读入 DataFrame。
基于名称启发(date、month、week、period)检测列并强制转字符串,以保持图表与幻灯片文本中的标签稳定。
最后,使用 pd.to_numeric 将非日期列转为数值,并用 0 填充缺失数值,避免后续 NaN 传播。
此步结束后,您将获得非空且具备数值列的 DataFrame。
步骤 4.2:计算模型 DataFrame
原始 CSV 在不同领域往往杂乱不一。模型表充当归一化层,我们在此添加便于制图的派生指标,而无需用户手动计算。
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技术支持
函数 compute_model_df() 做了两件广泛有用的事:
步骤 4.3:计算事实摘要
即便使用结构化输出,我们仍希望向 GPT-5.2 提供一份紧凑的周期变化摘要。这些事实也可作为模型在幻灯片要点中复用的提示。
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技术支持
我们先取第一行与最后一行界定周期窗口,再选择(date、month、week、period)列用于可读的周期展示。
对一小部分数值列,我们计算起始、结束、均值与从起点到终点的总体百分比变化,生成一份轻量的信号字典,帮助模型撰写幻灯片要点。
步骤 4.4:自动调整 Excel 列宽
默认的 Excel 列宽会让生成的工作簿显得粗糙。自动列宽是立即提升可用性的简单润色。
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) 技术支持
函数 autosize_columns() 扫描每列,找到最大字符串长度并设置 ws.column_dimensions[col_letter].width。OpenPyXL 通过 column_dimensions 及其 width 属性控制 Excel 列显示宽度。
接下来,我们将清洗后的模型表与事实摘要注入 GPT-5.2 的规划提示,验证返回的计划,然后渲染图表与幻灯片。
步骤 5:构建工作簿
利用前一步的清洗模型表与简要事实摘要,我们现在用 openpyxl 生成一个确定性的 Excel 工作簿。
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技术支持
工作簿构建器执行了以下操作:
创建工作簿骨架: 使用 Workbook() 实例化新工作簿,并通过 wb.remove(wb.active) 移除默认工作表,以完全控制顺序。随后显式添加 wb.create_sheet("Raw")、wb.create_sheet("Model") 与 wb.create_sheet("Charts")。
写入 Raw 工作表: 使用 Font、PatternFill 与 Alignment 写入表头行,然后逐行追加 raw_df 的数据。autosize_columns(ws_raw) 提升可读性,无需手动调整。
计算边界: 使用 row_end = 1 + len(raw_df) 获得稳定的末行,以便安全生成 A2:A{row_end} 这样的范围用于图表分类与引用。
应用数字格式启发: 根据列名关键词与 dtype 设置 cell.number_format,让 Excel 将数值显示为货币、百分比或普通数字。
创建 Charts 工作表: wb.create_sheet("Charts") 为图表锚点提供稳定画布。这与 openpyxl 的图表流程相匹配:创建 LineChart 或 BarChart,通过 Reference 绑定范围,用 set_categories 设置分类,最后用 ws.add_chart() 放置图表。
完成此步后,您将拥有一个一致的 Excel 文件,下一阶段可按精确单元格范围安全引用。 步骤 6:渲染图表
现在工作簿结构已固定,我们可以将图表渲染到 Charts 工作表,并应用一致的品牌风格。本文采用 DataCamp 的配色:
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 ] + 技术支持
函数 add_charts_to_workbook() 是将 schema 计划转化为带品牌风格 Excel 可视化的图表渲染引擎。
加载工作簿 :先用 load_workbook(xlsx_path) 打开已生成的 Excel 文件。随后根据首行构建 header_map,将每个列名映射到其 Excel 列索引。
构建单元格范围引用 :Reference() 定义要绘制的精确范围,chart.set_categories(x_ref) 连接 X 轴分类。标准模式是创建引用对象、添加数据、设置分类。
实例化图表类型 :对每个图表规范,创建 LineChart() 或 BarChart(),并设置 chart.title。
应用品牌配色 :对每个系列,用 chart.add_data() 绑定 Y 轴范围,再用 SeriesLabel() 设置显示标签。
最后通过 ws_charts.add_chart() 将每个图表放置在指定左上单元格。
步骤 7:渲染 PowerPoint
此步让计划变为真实的幻灯片文档。我们创建一个 PowerPoint,布局标题与要点,从与 Excel 相同的数据表中嵌入图表,并添加保留计划单元格范围引用的页脚。
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} " ) 技术支持
逐步理解上述代码:
创建文档与索引图表:prs = Presentation() 新建 PPTX,blank = prs.slide_layouts[6] 提供空白画布以便精确放置。
标题与要点:add_title() 写入文本框的 text_frame,add_bullets() 为每条要点创建段落,并通过段落格式控制字号与间距。
引用页脚:add_sources_footer() 去重各要点中的引用,并在底部渲染精简的 Sources: 行,确保可追溯性同时不致拥挤。
嵌入图表:add_chart() 使用类时间的 X 列与规划的系列值构建 CategoryChartData(),再通过 slide.shapes.add_chart() 插入,返回 GraphicFrame。
品牌样式:最后将十六进制色转换为 RGBColor,并逐系列应用。
布局规则与护栏:若幻灯片拥有有效 chart_id,则采用双列布局,否则为全宽要点。build_allowed_ranges() 构建可引用范围白名单,validate_plan() 强制唯一图表 ID、有效引用、允许的引用范围与合法指标,防止模型臆造字段。
步骤 8:用 LLM 生成计划
此时我们已有干净的表格数据,将其转化为可由渲染器执行的 schema 构建计划(图表、幻灯片与引用)。
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技术支持
函数 generate_plan_with_llm() 是本演示的“大脑”。我们将其这样加入流水线:
显式 JSON Schema:plan_schema 定义了模型唯一允许的响应结构,即 charts[] 与 slides[],并包含必填字段与枚举。
系统提示护栏:硬编码不可协商规则,确保计划可渲染且可审计。我们还将图表锚点限制为纯单元格位置,使布局保持确定性,避免模型即兴。这种多步骤把控正是 GPT-5.2 的强项。
用户消息:提供目标、请求的数量、计算出的事实与允许的指标键。由于模型只见到这些,其臆造列、范围或不被支持的论断的空间大幅减少。
遵循 schema 的解码:调用 chat_json_schema() 并将 response_format 设为 json_schema 且 strict: true,在解码期间强制执行 schema 约束。
两层验证:先用 ArtifactPlan.model_validate(plan_json) 确认响应匹配 Pydantic 模型,再用 validate_plan() 强制领域约束。
步骤 9:运行流水线
最后,我们将 CSV 加载、Excel 生成、LLM 规划、图表渲染与最终 PPTX 导出串联起来,并返回输出路径。
@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) 技术支持
让我们按步骤拼接流水线组件:
结构化流水线输出:@dataclass 创建轻量容器,包含 xlsx_path、pptx_path、plan_path 与 logs,以便 Streamlit UI 将流水线结果作为单一结构化值处理,而非杂耍元组或全局变量。
API 密钥路由:load_dotenv() 将 .env 文件加载为环境变量,OPENAI_API_KEY 与 OPENROUTER_API_KEY 可经 os.getenv() 读取。随后依据简单规则选择提供商密钥:若存在 OPENROUTER_API_KEY 则使用 OpenRouter,否则回退到 OpenAI(若可用)。
运行数据准备:加载并验证 CSV,计算派生模型表,然后通过 build_workbook_base() 创建 Excel 骨架。generate_plan_with_llm() 返回 ArtifactPlan,并用 json.dump() 保存为 JSON,便于检查与调试。
渲染制品:add_charts_to_workbook() 将图表实体化到 Charts 工作表,build_pptx() 基于计划与表格构建幻灯片文档。
完成此步后,我们的 Streamlit 应用即可调用 run_pipeline(),显示 logs,并提供三个可下载的确定性输出:Excel 工作簿、PPTX 文档与 JSON 计划。
步骤 10:Streamlit 界面
核心流水线准备就绪后,最后一步是将其封装进 Streamlit 应用,便于任何人上传文件、设定目标、选择模型并下载生成的制品。
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 = st. selectbox(
"API Provider" ,
[ "OpenAI" , "OpenRouter" ] ,
help = "Select which API to use. Make sure the corresponding API key is set in your .env file"
)
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 :
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
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( ) 技术支持
在 Streamlit 层,我们用 st.set_page_config( layout="wide") 设置应用框架,再通过 st.columns() 将界面左右分栏:左侧收集输入,右侧显示运行控制与输出。流水线仅在用户点击 st.button() 时运行;我们先检查是否缺少上传,包裹 st.spinner() 以避免界面“卡住”的感觉,然后通过 st.download_button() 将 Excel、PowerPoint 与计划文件回传给用户。
完成此步后,将所有内容保存为 app.py 并用以下命令启动完整体验:
streamlit run app.py技术支持
GPT-5.2 是一款适合生成需要可靠性、结构化与更少幻觉的工作流和多步流水线的强大模型。OpenAI 强调其在长上下文处理上的提升,并在电子表格类任务上表现更佳。
在本教程中,我们将构建一个像初级分析师那样工作的 Streamlit 应用:
关键在于 GPT-5.2 并非直接“做幻灯片”,而是生成一个严格的计划,由您的代码以确定性方式执行。
另请阅读我们关于 OpenAI 更新模型的指南:GPT-5.5、GPT-5.4、GPT-5.3 Codex 与 GPT-5.3 Instant
什么是 GPT-5.2?
GPT-5.2 是 OpenAI 的最新 GPT-5 系列模型,具备在创建电子表格、构建演示文稿、编写与调试代码、理解长上下文与使用工具等端到端任务上的改进。
以下是本项目最相关的 GPT-5.2 特性:
GPT-5.2 示例项目:构建结构化制品生成器
本节中,我们将用包裹在 Streamlit 应用中的 GPT 5.2 模型构建一个结构化制品生成器(Excel 与 PowerPoint)。总体而言,最终的 Streamlit 应用会:
读取 CSV 并构建规范化工作簿
随后通过 OpenAI 或 OpenRouter 使用结构化输出(
response_format: json_schema)调用 GPT-5.2依据我们的 schema 验证返回的计划
将图表渲染到 Excel 并按品牌配色进行样式化
最后生成内嵌带引用图表的 PowerPoint
让我们一步步动手构建。
步骤 1:先决条件与环境配置
在用 GPT-5.2 生成 Excel 工作簿与 PowerPoint 文档之前,我们需要搭建本地环境。先安装核心库并将 API 密钥设置为环境变量。
我们将使用Streamlit 构建交互式 Web 界面,Pandas 加载并预处理 CSV,Pydantic 验证 GPT-5.2 的 schema 锁定 JSON 计划。我们还会用 python-dotenv 从
.env文件安全加载OPENROUTER_API_KEY/OPENAI_API_KEY等密钥。 requests 库调用 OpenRouter/OpenAI 兼容端点,而 openpyxl 生成 Excel 工作簿与图表,python-pptx 创建带原生(可编辑)图表的 PowerPoint 文档。接着设置我们的 API 密钥:
请注意,GPT 5.2 可通过多个服务访问,包括 OpenAI 官方 API 以及 OpenRouter 等服务。
建议使用
.env文件在本地存储 API 密钥:在
.env文件内保存如下内容:然后使用 load_dotenv 库加载这些 API 密钥:
至此,我们的环境已准备好与 OpenAI API 进行鉴权。
步骤 2:定义计划的 schema
接下来,我们定义一个 schema 锁定的制品计划,它由严格的 JSON 蓝图组成,明确告知 GPT-5.2 要创建哪些图表以及如何组织幻灯片,因此模型的输出可通过
response_format: { type: "json_schema", strict: true }的结构化输出来解码与验证,而不是自由文本。上述代码片段用 Pydantic 模型定义了计划,带来三点重要作用:
计划结构:
ChartKind = Literal["line", "bar"]将图表类型限制在小型枚举内,ChartSpec捕获完整图表契约,SlideSpec定义幻灯片布局,并可通过chart_id将幻灯片关联到图表。自动质量约束: 通过
Field()等约束确保我们总能得到足够的图表/幻灯片、每张幻灯片有足够要点、每个要点包含引用。规划与渲染分离: GPT-5.2 仅产出蓝图(即图表、幻灯片与引用)这一结构化 JSON,其余由代码负责执行、Excel 格式化、图表渲染与文档布局。
有了该 schema,GPT-5.2 就成为生成可验证 JSON 蓝图的规划器。
步骤 3:以结构化输出调用 GPT-5.2
定义好计划 schema 后,下一步是以“schema 模式”调用 GPT-5.2——让其返回一个可验证并可执行的 JSON 蓝图。与其寄希望于模型“表现良好”,不如要求 API直接解码为您的 JSON Schema,使用
response_format: { type: "json_schema", ... strict: true },这正是结构化输出的用途。函数
chat_json_schema()是将提示转化为可验证计划的唯一网关:提供商路由(OpenRouter vs OpenAI): 若模型名带命名空间(openai/gpt-5.2),默认走 OpenRouter,因为这是其模型路由格式。若显式设置 api_provider="openai",则将同样的 Chat Completions 负载发给 OpenAI 端点。您也可完全不使用 OpenRouter。
端点设置: 对于 OpenAI,调用
https://api.openai.com/v1/chat/completions并使用标准 Bearer 认证。对于 OpenRouter,调用https://openrouter.ai/api/v1/chat/completions,并可选加入如 HTTP-Referer 与 X-Title 等署名头,这是 OpenRouter 推荐的元数据。通过
response_format进行 schema 解码: 关键负载块为response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } },指示模型产出受限于我们的 JSON Schema。接着我们将用 Pydantic 验证模型生成的 JSON 计划,仅在通过边界检查后才渲染 Excel 图表并生成 PowerPoint 文档。
步骤 4:辅助函数
在渲染之前,我们需要一层薄薄的处理,让上传的 CSV 安全可分析且引用一致。这些辅助函数完成四项工作:加载并清洗 CSV、创建派生模型表、为提示计算一组简洁事实、并通过自动列宽让 Excel 输出更易读。
步骤 4.1:加载并验证 CSV
若输入表为空或数值字段以字符串存储,GPT 5.2 无法可靠规划图表与幻灯片。此函数标准化数据集,使下游步骤无需猜测。
上述函数会:
pd.read_csv将文件读入 DataFrame。pd.to_numeric将非日期列转为数值,并用 0 填充缺失数值,避免后续 NaN 传播。此步结束后,您将获得非空且具备数值列的 DataFrame。
步骤 4.2:计算模型 DataFrame
原始 CSV 在不同领域往往杂乱不一。模型表充当归一化层,我们在此添加便于制图的派生指标,而无需用户手动计算。
函数
compute_model_df()做了两件广泛有用的事:寻找一个主要输出值列与次要输入列,并计算简单比率,以刻画在考虑次要信号后主要信号的保留程度。
对少量数值列计算
pct_change(),默认返回相对上一行的分数变化,作为任何时间序列指标运动的基础线。步骤 4.3:计算事实摘要
即便使用结构化输出,我们仍希望向 GPT-5.2 提供一份紧凑的周期变化摘要。这些事实也可作为模型在幻灯片要点中复用的提示。
我们先取第一行与最后一行界定周期窗口,再选择(date、month、week、period)列用于可读的周期展示。
对一小部分数值列,我们计算起始、结束、均值与从起点到终点的总体百分比变化,生成一份轻量的信号字典,帮助模型撰写幻灯片要点。
步骤 4.4:自动调整 Excel 列宽
默认的 Excel 列宽会让生成的工作簿显得粗糙。自动列宽是立即提升可用性的简单润色。
函数
autosize_columns()扫描每列,找到最大字符串长度并设置ws.column_dimensions[col_letter].width。OpenPyXL 通过column_dimensions及其width属性控制 Excel 列显示宽度。接下来,我们将清洗后的模型表与事实摘要注入 GPT-5.2 的规划提示,验证返回的计划,然后渲染图表与幻灯片。
步骤 5:构建工作簿
利用前一步的清洗模型表与简要事实摘要,我们现在用 openpyxl 生成一个确定性的 Excel 工作簿。
工作簿构建器执行了以下操作:
创建工作簿骨架: 使用
Workbook()实例化新工作簿,并通过wb.remove(wb.active)移除默认工作表,以完全控制顺序。随后显式添加wb.create_sheet("Raw")、wb.create_sheet("Model")与wb.create_sheet("Charts")。写入 Raw 工作表: 使用 Font、PatternFill 与 Alignment 写入表头行,然后逐行追加 raw_df 的数据。
autosize_columns(ws_raw)提升可读性,无需手动调整。计算边界: 使用
row_end = 1 + len(raw_df)获得稳定的末行,以便安全生成A2:A{row_end}这样的范围用于图表分类与引用。应用数字格式启发: 根据列名关键词与 dtype 设置
cell.number_format,让 Excel 将数值显示为货币、百分比或普通数字。创建 Charts 工作表:
wb.create_sheet("Charts") 为图表锚点提供稳定画布。这与 openpyxl 的图表流程相匹配:创建 LineChart 或 BarChart,通过Reference绑定范围,用set_categories设置分类,最后用ws.add_chart()放置图表。完成此步后,您将拥有一个一致的 Excel 文件,下一阶段可按精确单元格范围安全引用。
步骤 6:渲染图表
现在工作簿结构已固定,我们可以将图表渲染到 Charts 工作表,并应用一致的品牌风格。本文采用 DataCamp 的配色:
函数
add_charts_to_workbook()是将 schema 计划转化为带品牌风格 Excel 可视化的图表渲染引擎。加载工作簿:先用
load_workbook(xlsx_path)打开已生成的 Excel 文件。随后根据首行构建header_map,将每个列名映射到其 Excel 列索引。构建单元格范围引用:
Reference()定义要绘制的精确范围,chart.set_categories(x_ref)连接 X 轴分类。标准模式是创建引用对象、添加数据、设置分类。实例化图表类型:对每个图表规范,创建
LineChart()或BarChart(),并设置chart.title。应用品牌配色:对每个系列,用
chart.add_data()绑定 Y 轴范围,再用SeriesLabel()设置显示标签。最后通过
ws_charts.add_chart()将每个图表放置在指定左上单元格。步骤 7:渲染 PowerPoint
此步让计划变为真实的幻灯片文档。我们创建一个 PowerPoint,布局标题与要点,从与 Excel 相同的数据表中嵌入图表,并添加保留计划单元格范围引用的页脚。
逐步理解上述代码:
创建文档与索引图表:
prs = Presentation()新建 PPTX,blank = prs.slide_layouts[6]提供空白画布以便精确放置。标题与要点:
add_title()写入文本框的text_frame,add_bullets()为每条要点创建段落,并通过段落格式控制字号与间距。引用页脚:
add_sources_footer()去重各要点中的引用,并在底部渲染精简的Sources:行,确保可追溯性同时不致拥挤。嵌入图表:
add_chart()使用类时间的 X 列与规划的系列值构建CategoryChartData(),再通过slide.shapes.add_chart()插入,返回GraphicFrame。品牌样式:最后将十六进制色转换为
RGBColor,并逐系列应用。布局规则与护栏:若幻灯片拥有有效
chart_id,则采用双列布局,否则为全宽要点。build_allowed_ranges()构建可引用范围白名单,validate_plan()强制唯一图表 ID、有效引用、允许的引用范围与合法指标,防止模型臆造字段。步骤 8:用 LLM 生成计划
此时我们已有干净的表格数据,将其转化为可由渲染器执行的 schema 构建计划(图表、幻灯片与引用)。
函数
generate_plan_with_llm()是本演示的“大脑”。我们将其这样加入流水线:显式 JSON Schema:
plan_schema定义了模型唯一允许的响应结构,即charts[]与slides[],并包含必填字段与枚举。系统提示护栏:硬编码不可协商规则,确保计划可渲染且可审计。我们还将图表锚点限制为纯单元格位置,使布局保持确定性,避免模型即兴。这种多步骤把控正是 GPT-5.2 的强项。
用户消息:提供目标、请求的数量、计算出的事实与允许的指标键。由于模型只见到这些,其臆造列、范围或不被支持的论断的空间大幅减少。
遵循 schema 的解码:调用
chat_json_schema()并将response_format设为json_schema且strict: true,在解码期间强制执行 schema 约束。两层验证:先用
ArtifactPlan.model_validate(plan_json)确认响应匹配 Pydantic 模型,再用validate_plan()强制领域约束。步骤 9:运行流水线
最后,我们将 CSV 加载、Excel 生成、LLM 规划、图表渲染与最终 PPTX 导出串联起来,并返回输出路径。
让我们按步骤拼接流水线组件:
@dataclass创建轻量容器,包含xlsx_path、pptx_path、plan_path与logs,以便 Streamlit UI 将流水线结果作为单一结构化值处理,而非杂耍元组或全局变量。load_dotenv()将.env文件加载为环境变量,OPENAI_API_KEY与OPENROUTER_API_KEY可经os.getenv()读取。随后依据简单规则选择提供商密钥:若存在OPENROUTER_API_KEY则使用 OpenRouter,否则回退到 OpenAI(若可用)。build_workbook_base()创建 Excel 骨架。generate_plan_with_llm()返回ArtifactPlan,并用json.dump()保存为 JSON,便于检查与调试。add_charts_to_workbook()将图表实体化到 Charts 工作表,build_pptx()基于计划与表格构建幻灯片文档。完成此步后,我们的 Streamlit 应用即可调用
run_pipeline(),显示logs,并提供三个可下载的确定性输出:Excel 工作簿、PPTX 文档与 JSON 计划。步骤 10:Streamlit 界面
核心流水线准备就绪后,最后一步是将其封装进 Streamlit 应用,便于任何人上传文件、设定目标、选择模型并下载生成的制品。
在 Streamlit 层,我们用
st.set_page_config( layout="wide")设置应用框架,再通过st.columns()将界面左右分栏:左侧收集输入,右侧显示运行控制与输出。流水线仅在用户点击st.button()时运行;我们先检查是否缺少上传,包裹st.spinner()以避免界面“卡住”的感觉,然后通过st.download_button()将 Excel、PowerPoint 与计划文件回传给用户。完成此步后,将所有内容保存为
app.py并用以下命令启动完整体验: