Kursus
GPT-5.2 adalah model yang andal untuk menghasilkan alur kerja dan pipeline multi-langkah yang memerlukan keandalan, struktur, dan lebih sedikit halusinasi. OpenAI menyoroti peningkatan pada pekerjaan konteks panjang dan kinerja yang lebih baik pada tugas bergaya spreadsheet.
Dalam tutorial ini, kita akan membuat aplikasi Streamlit yang berperan seperti analis junior:
- Unggah CSV dan tulis tujuan (contoh: “pembaruan 6 slide berfokus pada pertumbuhan, retensi, unit economics, runway”)
- Hasilkan rencana JSON yang terkunci skema
- Produksi workbook Excel dengan data mentah, metrik turunan, dan bagan
- Terakhir, hasilkan deck PowerPoint yang sesuai dengan sitasi yang tepat
Kuncinya adalah GPT-5.2 tidak langsung “membuat slide.” Model ini menghasilkan rencana ketat yang dijalankan kode Anda secara deterministik.
Pastikan juga membaca panduan kami tentang model terbaru OpenAI, GPT-5.5, GPT-5.4, GPT-5.3 Codex, dan GPT-5.3 Instant
Apa Itu GPT-5.2?
GPT-5.2 adalah model seri GPT-5 terbaru dari OpenAI dengan peningkatan untuk tugas end-to-end seperti membuat spreadsheet, membangun presentasi, menulis dan men-debug kode, memahami konteks panjang, dan menggunakan alat.
Berikut adalah properti GPT-5.2 yang paling relevan untuk proyek ini:
- Penalaran konteks panjang: GPT-5.2 Thinking digambarkan sebagai yang terdepan pada evaluasi penalaran konteks panjang (OpenAI MRCRv2), dengan implikasi praktis untuk bekerja secara koheren pada dokumen yang sangat panjang dan input multi-berkas.
- Eksekusi multi-langkah: Model ini unggul pada proyek kompleks multi-langkah, termasuk pemanggilan alat yang lebih kuat dan perilaku horizon panjang, yang langsung selaras dengan perencanaan terkunci skema dan loop rendering deterministik kita.
- Faktualitas yang lebih baik: GPT-5.2 Thinking menghasilkan lebih sedikit respons keliru dibanding GPT-5.1 Thinking, yang membantu di sini karena mengurangi kemungkinan menghasilkan teks/angka yang mungkin melenceng dari rentang Excel yang kami sitir.
- Trade-off latensi: Di API, Anda dapat beriterasi dengan GPT-5.2 Instant untuk umpan balik cepat, lalu beralih ke GPT-5.2 Thinking atau Pro saat Anda menginginkan perencanaan end-to-end yang kuat dan artefak berkualitas lebih tinggi, dengan catatan bahwa generasi yang lebih kompleks bisa memakan waktu menit.
Proyek Contoh GPT-5.2: Bangun Generator Structured Artifacts
Di bagian ini, kita akan membangun generator structured artifacts (Excel dan PowerPoint) menggunakan model GPT 5.2 yang dibungkus dalam aplikasi Streamlit. Secara garis besar, aplikasi Streamlit final melakukan hal-hal berikut:
-
Membaca CSV dan membangun workbook kanonik
-
Lalu memanggil GPT-5.2 via OpenAI atau OpenRouter dengan Structured Outputs (
response_format: json_schema) -
Memvalidasi rencana yang dikembalikan terhadap skema kita
-
Merender bagan ke dalam Excel dan menatanya sesuai palet merek kita
-
Terakhir, menghasilkan PowerPoint dengan bagan tersemat beserta sitasinya

Mari kita bangun selangkah demi selangkah.
Langkah 1: Prasyarat dan penyiapan
Sebelum membuat workbook Excel dan deck PowerPoint dengan GPT-5.2, kita perlu menyiapkan lingkungan lokal. Kita akan mulai dengan memasang pustaka inti dan mengatur kunci API sebagai variabel lingkungan.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptx
Kita akan menggunakan Streamlit untuk membangun UI web interaktif, Pandas untuk memuat dan menyiapkan CSV, Pydantic untuk memvalidasi rencana JSON terkunci skema GPT-5.2. Kita juga menggunakan python-dotenv untuk memuat kunci seperti OPENROUTER_API_KEY/OPENAI_API_KEY dari berkas .env secara aman. Pustaka requests memanggil endpoint yang kompatibel OpenRouter/OpenAI, sementara openpyxl menghasilkan workbook dan bagan Excel, dan python-pptx membuat deck PowerPoint dengan bagan native (dapat diedit).
Selanjutnya, kita mengatur kunci API:
export OPENAI_API_KEY="your_key"
export OPENROUTER_API_KEY="your_key"
Perlu dicatat bahwa GPT 5.2 dapat diakses melalui beberapa layanan, termasuk API resmi OpenAI serta layanan seperti OpenRouter dan lainnya.
Disarankan menggunakan berkas .env untuk menyimpan kunci API secara lokal:
Di dalam berkas .env, simpan hal berikut:
OPENROUTER_API_KEY="your_key"
OPENAI_API_KEY="your_key"
Lalu muat kunci API ini menggunakan pustaka load_dotenv:
from dotenv import load_dotenv
load_dotenv()
Pada tahap ini, lingkungan kita siap melakukan autentikasi dengan API OpenAI.
Langkah 2: Mendefinisikan skema rencana
Berikutnya, kita mendefinisikan rencana artefak terkunci skema yang terdiri dari cetak biru JSON ketat yang memberi tahu GPT-5.2 dengan tepat bagan mana yang harus dibuat dan bagaimana menyusun slide, sehingga output model dapat didekodekan dan divalidasi menggunakan Structured Outputs melalui response_format: { type: "json_schema", strict: true } alih-alih teks bebas.
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)
Potongan kode di atas mendefinisikan rencana menggunakan model Pydantic, yang melakukan tiga hal penting:
-
Struktur rencana:
ChartKind = Literal["line", "bar"]membatasi jenis bagan ke enum kecil,ChartSpecmenangkap kontrak bagan penuh, danSlideSpecmendefinisikan tata letak slide sambil opsional menautkan slide ke bagan melaluichart_id. -
Penegakan kualitas otomatis: Batasan seperti
Field()memastikan kita selalu mendapatkan cukup bagan/slide, setiap slide memiliki cukup butir, dan setiap butir menyertakan sitasi. -
Perencanaan dan rendering: GPT-5.2 hanya menghasilkan cetak biru, yaitu bagan, slide, dan sitasi, sebagai JSON terstruktur, sementara sisanya ditangani kode: eksekusi, pemformatan Excel, rendering bagan, dan tata letak deck.
Dengan skema ini, GPT-5.2 menjadi perencana yang menghasilkan cetak biru JSON tervalidasi.
Langkah 3: Memanggil GPT-5.2 dengan structured outputs
Dengan skema rencana didefinisikan, langkah berikutnya adalah memanggil GPT-5.2 dalam “mode skema”—sehingga ia mengembalikan cetak biru JSON yang dapat Anda validasi dan eksekusi. Alih-alih berharap model “berperilaku baik,” kita memberi tahu API untuk mendekode langsung ke JSON Schema Anda menggunakan response_format: { type: "json_schema", ... strict: true }, persis seperti tujuan Structured Outputs.
def chat_json_schema(
model: str,
messages: list,
json_schema: dict,
api_key: str,
temperature: float = 0.2,
max_tokens: int = 6000,
timeout_s: int = 120,
api_provider: Optional[str] = None,
) -> dict:
if api_provider is None:
if "/" in model:
api_provider = "openrouter"
elif os.getenv("OPENAI_API_KEY") and api_key == os.getenv("OPENAI_API_KEY"):
api_provider = "openai"
elif os.getenv("OPENROUTER_API_KEY") and api_key == os.getenv("OPENROUTER_API_KEY"):
api_provider = "openrouter"
else:
api_provider = "openrouter"
if api_provider == "openai":
url = "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
if "/" in model:
model = model.split("/", 1)[1]
else:
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
if os.getenv("OPENROUTER_SITE_URL"):
headers["HTTP-Referer"] = os.getenv("OPENROUTER_SITE_URL")
if os.getenv("OPENROUTER_APP_NAME"):
headers["X-Title"] = os.getenv("OPENROUTER_APP_NAME")
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "artifact_plan",
"strict": True,
"schema": json_schema,
},
},
}
r = requests.post(url, headers=headers, data=json.dumps(payload), timeout=timeout_s)
r.raise_for_status()
data = r.json()
if "error" in data:
provider_name = "OpenAI" if api_provider == "openai" else "OpenRouter"
raise RuntimeError(f"{provider_name} error: {data['error']}")
finish_reason = data["choices"][0].get("finish_reason")
if finish_reason == "length":
raise RuntimeError("Model output truncated (max_tokens). Increase max_tokens or reduce schema size.")
content = data["choices"][0]["message"]["content"]
if isinstance(content, dict):
return content
try:
return json.loads(content)
except json.JSONDecodeError as e:
preview = (content or "")[:800]
raise RuntimeError(f"Failed to parse JSON. Preview:\n{preview}") from e
Fungsi chat_json_schema() adalah satu-satunya gerbang yang mengubah prompt menjadi rencana tervalidasi:
-
Perutean penyedia (OpenRouter vs OpenAI): Jika nama model memiliki namespace (openai/gpt-5.2), Anda default ke OpenRouter karena itu format yang digunakan OpenRouter untuk perutean model. Jika Anda secara eksplisit mengatur api_provider="openai", Anda mengirim payload Chat Completions yang sama ke endpoint OpenAI. Silakan lewati OpenRouter sepenuhnya jika diinginkan.
-
Pengaturan endpoint: Untuk OpenAI, Anda memanggil endpoint
https://api.openai.com/v1/chat/completionsdengan Bearer auth standar. Untuk OpenRouter, Anda memanggil endpointhttps://openrouter.ai/api/v1/chat/completionsdan opsional menyertakan header atribusi seperti HTTP-Referer dan X-Title, yang didokumentasikan OpenRouter sebagai metadata yang direkomendasikan. -
Dekode skema via
response_format: Blok payload kunci adalahresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, yang menginstruksikan model untuk menghasilkan output yang dibatasi oleh JSON Schema kita.
Selanjutnya, kita akan memvalidasi rencana JSON model dengan Pydantic, dan hanya jika lolos pemeriksaan batas, kita lanjutkan untuk merender bagan Excel dan menghasilkan deck PowerPoint.
Langkah 4: Fungsi pembantu
Sebelum merender apa pun, kita memerlukan lapisan kecil yang membuat CSV apa pun yang diunggah menjadi aman untuk dianalisis dan konsisten untuk direferensikan. Fungsi pembantu ini melakukan empat tugas: memuat dan membersihkan CSV, membuat tabel model turunan, menghitung set fakta ringkas untuk prompt, dan membuat output Excel mudah dibaca dengan kolom auto-size.
Langkah 4.1: Memuat dan memvalidasi CSV
GPT 5.2 tidak dapat secara andal merencanakan bagan dan slide jika tabel input kosong atau memiliki kolom numerik yang disimpan sebagai string. Fungsi ini menstandarkan dataset sehingga langkah hilir tidak perlu menebak.
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
Fungsi di atas:
- Membaca berkas ke DataFrame menggunakan
pd.read_csv. - Mendeteksi kolom menggunakan heuristik nama (date, month, week, period) dan memaksanya menjadi string. Ini menjaga label tetap stabil pada bagan dan teks slide.
- Terakhir, mengonversi kolom non-tanggal menjadi numerik menggunakan
pd.to_numeric, dan mengisi nilai numerik yang hilang dengan nol untuk menghindari NaN berantai nanti.
Di akhir langkah ini, Anda memiliki DataFrame yang tidak kosong dan memiliki kolom numerik.
Langkah 4.2: Menghitung DataFrame model
CSV mentah itu berantakan dan tidak konsisten lintas domain. Lembar model berfungsi sebagai lapisan normalisasi di mana kita menambahkan metrik turunan yang berguna untuk pembuatan bagan, tanpa mengharuskan pengguna menghitungnya secara manual.
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
Fungsi compute_model_df() melakukan dua hal yang berguna secara umum:
-
Mencari kolom nilai output primer dan kolom input sekunder lalu menghitung rasio sederhana untuk menangkap seberapa besar sinyal primer yang tersisa setelah memperhitungkan sinyal sekunder.
-
Untuk sejumlah kecil kolom numerik, menghitung
pct_change(), yang secara default mengembalikan perubahan fraksional dari baris sebelumnya, sehingga menjadi baseline mudah untuk menangkap pergerakan pada metrik deret waktu apa pun.
Langkah 4.3: Menghitung fakta
Bahkan dengan Structured Outputs, kita tetap ingin memberi GPT-5.2 ringkasan ringkas tentang apa yang berubah selama periode tersebut. Fakta-fakta ini juga berfungsi sebagai prompt yang dapat digunakan ulang model dalam butir slide.
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
Kita terlebih dahulu memilih baris pertama dan terakhir untuk mendefinisikan jendela periode. Lalu kita memilih satu kolom (date, month, week, period) untuk pelaporan yang mudah dibaca manusia.
Untuk sejumlah kecil kolom numerik, kita menghitung snapshot nilai awal, akhir, rata-rata, dan persentase perubahan keseluruhan dari awal ke akhir. Ini menghasilkan kamus sinyal ringan yang membantu model menulis butir slide.
Langkah 4.4: Auto-size kolom Excel
Lebar kolom default Excel membuat workbook yang dihasilkan terlihat kasar. Auto-size adalah langkah pemolesan sederhana yang langsung meningkatkan kegunaan.
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)
Fungsi autosize_columns() memindai setiap kolom, menemukan panjang string maksimum, dan mengatur ws.column_dimensions[col_letter].width. Lalu pustaka OpenPyXL mengekspos column_dimensions dan properti width-nya untuk mengontrol ukuran tampilan kolom di Excel.
Selanjutnya, kita pasangkan tabel model yang telah dibersihkan dan fakta ke dalam prompt perencanaan GPT-5.2, validasi rencana yang dikembalikan, lalu render bagan dan slide.
Langkah 5: Bangun workbook
Menggunakan tabel model yang telah dibersihkan dan ringkasan fakta ringan dari langkah sebelumnya, kita sekarang menghasilkan workbook Excel yang deterministik dengan openpyxl.
def build_workbook_base(raw_df: pd.DataFrame, model_df: pd.DataFrame, out_xlsx: str) -> Tuple[str, int]:
wb = Workbook()
wb.remove(wb.active)
header_fill = PatternFill("solid", fgColor="F2F2F2")
header_font = Font(bold=True)
ws_raw = wb.create_sheet("Raw")
for j, col in enumerate(raw_df.columns, start=1):
c = ws_raw.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")
for i, row in enumerate(raw_df.itertuples(index=False), start=2):
for j, val in enumerate(row, start=1):
ws_raw.cell(row=i, column=j, value=val)
ws_raw.freeze_panes = "A2"
autosize_columns(ws_raw)
row_end = 1 + len(raw_df)
ws_model = wb.create_sheet("Model")
for j, col in enumerate(model_df.columns, start=1):
c = ws_model.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")
for i, row in enumerate(model_df.itertuples(index=False), start=2):
for j, val in enumerate(row, start=1):
ws_model.cell(row=i, column=j, value=val)
ws_model.freeze_panes = "A2"
autosize_columns(ws_model)
col_index = {name: idx + 1 for idx, name in enumerate(model_df.columns)}
money_keywords = ['mrr', 'revenue', 'income', 'sales', 'cost', 'expense', 'cash', 'balance', 'burn', 'price', 'amount', 'fee']
pct_keywords = ['margin', 'rate', 'ratio', 'pct', 'percent', 'change', 'growth']
for col_name, col_idx in col_index.items():
col_lower = col_name.lower()
is_money = any(kw in col_lower for kw in money_keywords)
is_pct = any(kw in col_lower for kw in pct_keywords) or 'margin' in col_lower
is_large_number = False
if col_name in model_df.columns:
sample_vals = model_df[col_name].dropna()
if len(sample_vals) > 0 and pd.api.types.is_numeric_dtype(sample_vals):
max_val = abs(sample_vals.max())
is_large_number = max_val > 1000
for r in range(2, row_end + 1):
if is_money or is_large_number:
ws_model.cell(row=r, column=col_idx).number_format = "$#,##0"
elif is_pct:
ws_model.cell(row=r, column=col_idx).number_format = "0.0%"
elif pd.api.types.is_numeric_dtype(model_df[col_name]):
ws_model.cell(row=r, column=col_idx).number_format = "0.0"
ws_charts = wb.create_sheet("Charts")
ws_charts["A1"] = "Charts"
ws_charts["A1"].font = Font(bold=True)
wb.save(out_xlsx)
return out_xlsx, row_end
Berikut yang dilakukan pembangun workbook ini:
-
Membuat kerangka workbook:
Workbook()membuat objek workbook baru, laluwb.remove(wb.active)menghapus sheet default agar kita sepenuhnya mengontrol urutan sheet. Worksheet baru ditambahkan secara eksplisit denganwb.create_sheet("Raw"),wb.create_sheet("Model"), danwb.create_sheet("Charts"). -
Menulis sheet Raw: Kita menulis baris header menggunakan Font, PatternFill, dan Alignment, lalu menambahkan setiap baris dari raw_df. Fungsi
.autosize_columns(ws_raw)membantu keterbacaan tanpa pengubahan ukuran manual. -
Menghitung batas: Kita gunakan
row_end = 1 + len(raw_df), yang memberi baris terakhir stabil sehingga kita dapat dengan aman menghasilkan rentang sepertiA2:A{row_end}untuk kategori bagan dan sitasi. -
Menerapkan heuristik format angka: Berikutnya, kita atur
cell.number_formatberdasarkan kata kunci nama kolom dan dtype sehingga Excel menampilkan nilai sebagai mata uang, persentase, atau angka biasa. -
Membuat sheet Charts: Pernyataan
wb.create_sheet("Charts") memberi kita kanvas stabil untuk jangkar bagan. Ini sesuai dengan alur kerja bagan openpyxl di mana kita membuat LineChart atau BarChart, mengikat rentang viaReference, mengatur kategori denganset_categories, lalu menempatkannya denganws.add_chart().
Setelah langkah ini, Anda memiliki berkas Excel konsisten yang dapat direferensikan tahap berikutnya dengan rentang sel yang presisi.
Langkah 6: Render bagan
Kini struktur workbook telah tetap, kita dapat merender bagan ke dalam sheet Charts dan menerapkan tampilan merek yang konsisten. Saya menerapkan palet warna DataCamp dengan:
- Warna primer: #01ef63
- Warna sekunder: #203147
def add_charts_to_workbook(xlsx_path: str, charts: List[ChartSpec], metric_to_range: Dict[str, str]):
wb = load_workbook(xlsx_path)
ws_model = wb["Model"]
ws_charts = wb["Charts"]
header_map = {ws_model.cell(1, c).value: c for c in range(1, ws_model.max_column + 1)}
def ref_from_metric(metric: str, row_start: int, row_end: int) -> Reference:
if metric not in header_map:
raise ValueError(f"Unknown metric in Model sheet: {metric}")
col = header_map[metric]
return Reference(ws_model, min_col=col, min_row=row_start, max_col=col, max_row=row_end)
for ch in charts:
chart = LineChart() if ch.kind == "line" else BarChart()
chart.title = ch.title
x_metric = ch.x_metric
if x_metric not in header_map:
date_keywords = ['date', 'time', 'month', 'year', 'period', 'week']
date_cols = [col for col in header_map.keys() if col and any(kw in str(col).lower() for kw in date_keywords)]
if date_cols:
x_metric = date_cols[0]
else:
x_metric = list(header_map.keys())[0] if header_map else "month"
x_ref = ref_from_metric(x_metric, 2, ws_model.max_row)
chart.set_categories(x_ref)
chart_colors = ["01EF63", "203147"]
for i, s in enumerate(ch.series):
y_ref = ref_from_metric(s.metric, 2, ws_model.max_row)
chart.add_data(y_ref, titles_from_data=False)
chart.series[-1].title = SeriesLabel(v=s.name)
series = chart.series[-1]
color_hex = chart_colors[i % len(chart_colors)]
try:
if ch.kind == "line":
series.graphicalProperties.line.solidFill = color_hex
series.graphicalProperties.line.width = 30000
else:
series.graphicalProperties.solidFill = color_hex
except AttributeError:
pass
anchor = ch.anchor.strip()
if "!" in anchor:
anchor = anchor.split("!")[-1]
if anchor and anchor.isalpha():
anchor = f"{anchor}2"
if not re.match(r'^[A-Z]+[0-9]+
Fungsi add_charts_to_workbook() adalah mesin perenderan bagan yang mengubah rencana skema kita menjadi visual Excel berbranding.
-
Memuat workbook: Kita mulai dengan membuka berkas Excel yang dihasilkan dengan
load_workbook(xlsx_path). Selanjutnya, kita membangunheader_mapdari baris pertama, memetakan setiap nama kolom ke indeks kolom Excel-nya. -
Membangun referensi rentang sel: Fungsi
Reference()mendefinisikan rentang tepat untuk diplot, danchart.set_categories(x_ref)menghubungkan kategori sumbu-X. Pola standarnya adalah membuat objek referensi, menambahkan data, lalu mengatur kategori. -
Instansiasi jenis bagan: Untuk setiap spesifikasi bagan, kita membuat
LineChart()atauBarChart(), dan mengaturchart.title. -
Menambahkan palet merek: Untuk setiap seri dalam spesifikasi, kita mengikat Y-range dengan
chart.add_data(), lalu mengatur label tampilan menggunakanSeriesLabel().
Terakhir, fungsi ws_charts.add_chart() menempatkan setiap bagan pada sel kiri-atas tertentu.
Langkah 7: Render PowerPoint
Langkah ini adalah saat rencana menjadi deck slide nyata. Kita membuat PowerPoint, menata judul dan butir, menyematkan bagan yang dibangun dari tabel yang sama seperti di Excel, dan menambahkan footer yang mempertahankan sitasi rentang sel dari rencana.
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}")
Mari pahami kode di atas selangkah demi selangkah:
-
Membuat deck dan mengindeks bagan:
prs = Presentation()memulai PPTX baru, sementarablank = prs.slide_layouts[6]memberi kanvas kosong untuk penempatan presisi. -
Judul dan butir: Fungsi
add_title()menulis ke dalam textboxtext_frame, danadd_bullets()menambahkan satu paragraf per butir sambil mengontrol ukuran font dan spasi melalui pemformatan paragraf. -
Footer sitasi: Fungsi
add_sources_footer()menghapus duplikasi sitasi lintas butir dan merender baris ringkasSources:di bagian bawah agar keterlacakan tetap utuh tanpa membuat slide penuh sesak. -
Menyematkan bagan:
add_chart()membangun objekCategoryChartData()menggunakan kolom X mirip waktu dan nilai seri yang direncanakan, lalu menyisipkannya melaluislide.shapes.add_chart(), yang mengembalikanGraphicFrame. -
Gaya merek: Terakhir, kita mengonversi heks ke
RGBColordan menerapkannya per seri. -
Aturan tata letak dan pagar pembatas: Jika sebuah slide memiliki
chart_idyang valid, maka kita gunakan tata letak dua kolom, jika tidak maka butir lebar penuh. Fungsibuild_allowed_ranges()membuat daftar-izin rentang yang bisa disitir, danvalidate_plan()menegakkan ID bagan unik, referensi valid, sitasi yang diizinkan, dan metrik yang valid sehingga model tidak bisa mengarang kolom.
Langkah 8: Menghasilkan rencana dengan LLM
Pada titik ini, kita memiliki data tabular bersih, yang akan kita ubah menjadi rencana pembuatan skema (bagan, slide, dan sitasi) yang dapat dieksekusi perender.
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
Fungsi generate_plan_with_llm() adalah otak dari demo ini. Begini cara kita menambahkannya ke pipeline:
-
JSON Schema eksplisit:
plan_schemamendefinisikan satu-satunya bentuk respons yang boleh dihasilkan model,charts[]danslides[], dengan field wajib dan enum. -
Penjagaan dalam system prompt: Kami menanamkan hal-hal non-negotiable untuk memastikan rencana tetap dapat dirender dan diaudit. Kami juga membatasi penempatan bagan ke sel polos, sehingga tata letak bagan tetap deterministik alih-alih diimprovisasi model. Kontrol multi-langkah semacam ini memang dirancang untuk ditangani GPT-5.2.
-
Pesan pengguna: Pesan pengguna memberikan tujuan, jumlah yang diminta, fakta yang dihitung, dan kunci metrik yang diizinkan. Karena model tidak melihat apa pun selain ini, ia memiliki jauh lebih sedikit cara untuk mengarang kolom, rentang, atau klaim yang tidak didukung.
-
Dekode patuh skema: Kita memanggil
chat_json_schema()denganresponse_formatdiatur kejson_schemadanstrict: true, yang menegakkan kepatuhan skema saat dekode. -
Validasi dua lapis: Pertama, fungsi
ArtifactPlan.model_validate(plan_json)memastikan respons cocok dengan model Pydantic kita. Lalu,validate_plan()menegakkan batasan domain.
Langkah 9: Menjalankan pipeline
Akhirnya, kita menghubungkan pemuatan CSV, pembuatan Excel, perencanaan LLM, perenderan bagan, dan ekspor PPTX final, lalu mengembalikan path output.
@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)
Mari gabungkan semua komponen pipeline kita selangkah demi selangkah:
- Output pipeline terstruktur:
@dataclassmembuat kontainer ringan untukxlsx_path,pptx_path,plan_path, danlogs, sehingga UI Streamlit dapat memperlakukan hasil pipeline sebagai satu nilai terstruktur alih-alih mengelola tuple atau global. - Perutean kunci API: Metode
load_dotenv()memuat berkas.envke variabel lingkungan sehinggaOPENAI_API_KEYdanOPENROUTER_API_KEYdapat dibaca viaos.getenv(). Lalu kita memilih kunci penyedia dengan aturan sederhana, yakni jikaOPENROUTER_API_KEYada, gunakan OpenRouter; jika tidak, kembali ke OpenAI jika tersedia. - Menjalankan persiapan data: Kita memuat dan memvalidasi CSV, menghitung tabel model turunan, lalu membuat kerangka Excel melalui
build_workbook_base(). Fungsigenerate_plan_with_llm()kemudian mengembalikanArtifactPlanyang disimpan sebagai JSON menggunakanjson.dump()sehingga mudah diperiksa dan di-debug. - Merender artefak: Fungsi add_charts_to_workbook() kemudian mematerialkan bagan Excel ke sheet Charts, dan
build_pptx()membangun deck slide dari rencana dan tabel.
Setelah langkah ini, aplikasi Streamlit kita dapat memanggil run_pipeline(), menampilkan logs, dan menyediakan tiga output deterministik untuk diunduh, termasuk workbook Excel, deck PPTX, dan rencana JSON.
Langkah 10: UI Streamlit
Kini pipeline inti telah siap, langkah terakhir adalah membungkusnya dalam aplikasi Streamlit agar siapa pun dapat mengunggah berkas, menetapkan tujuan, memilih model, dan mengunduh artefak yang dihasilkan.
load_dotenv()
st.set_page_config(
page_title="GPT-5.2 PPT and Excel Generator",
page_icon="",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1f77b4;
margin-bottom: 1rem;
}
.sub-header {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.success-box {
padding: 1rem;
background-color: #d4edda;
border-left: 4px solid #28a745;
margin: 1rem 0;
}
.error-box {
padding: 1rem;
background-color: #f8d7da;
border-left: 4px solid #dc3545;
margin: 1rem 0;
}
.info-box {
padding: 1rem;
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
margin: 1rem 0;
}
.slide-preview {
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
""", unsafe_allow_html=True)
def main():
try:
if 'outputs' not in st.session_state:
st.session_state.outputs = None
if 'generated' not in st.session_state:
st.session_state.generated = False
st.markdown('<div class="main-header">Spreadsheet to PowerPoint Generator</div>', unsafe_allow_html=True)
with st.sidebar:
# API Provider selection
api_provider = st.selectbox(
"API Provider",
["OpenAI", "OpenRouter"],
help="Select which API to use. Make sure the corresponding API key is set in your .env file"
)
# Model selection based on provider
if api_provider == "OpenAI":
model = st.selectbox(
"AI Model",
[
"gpt-4o",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
],
help="OpenAI models. Requires OPENAI_API_KEY in .env file"
)
else: # OpenRouter
model = st.selectbox(
"AI Model",
[
"openai/gpt-5.2",
"openai/gpt-4-turbo",
"openai/gpt-4o",
"anthropic/claude-3-opus",
"anthropic/claude-3-sonnet",
],
help="OpenRouter models. Requires OPENROUTER_API_KEY in .env file"
)
with st.expander("Advanced Settings"):
num_slides = st.slider("Number of Slides", 4, 10, 6)
num_charts = st.slider("Number of Charts", 3, 6, 4)
col1, col2 = st.columns([1, 1])
with col1:
st.markdown("### Input")
uploaded_file = st.file_uploader(
"Upload CSV",
type=['csv'],
help="Upload any CSV file. The app will automatically detect columns and create appropriate charts and slides."
)
use_example = st.checkbox("Use example data (kpis.csv)", value=True if not uploaded_file else False)
if uploaded_file or use_example:
if use_example:
csv_path = "kpis.csv"
df = pd.read_csv(csv_path)
else:
df = pd.read_csv(uploaded_file)
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w') as f:
df.to_csv(f, index=False)
csv_path = f.name
with st.expander(" Preview Data", expanded=False):
try:
st.dataframe(df, width='stretch')
except Exception as e:
st.write("**Data Preview:**")
st.table(df.head(20))
if len(df) > 20:
st.caption(f"Showing first 20 of {len(df)} rows")
st.caption(f"Rows: {len(df)} | Columns: {len(df.columns)}")
else:
csv_path = None
st.info("Upload a CSV or use example data to continue")
goal = st.text_area(
"Goal",
value="Create a 6-slide investor update for Apr–Sep 2025. Focus on growth, retention, unit economics, and runway.",
height=100,
help="Describe what you want the AI to create"
)
generate_btn = st.button("Generate Artifacts", type="primary", use_container_width=True)
with col2:
st.markdown("### Output")
status_container = st.empty()
progress_bar = st.progress(0)
if not generate_btn:
status_container.info("Configure settings and click 'Generate Artifacts'")
if generate_btn:
if not csv_path:
st.error("Please upload a CSV or select 'Use example data'")
st.stop()
try:
status_container.info("Processing... This may take 30-60 seconds")
progress_bar.progress(10)
output_dir = Path("out")
output_dir.mkdir(exist_ok=True)
progress_bar.progress(20)
with st.spinner("Generating artifacts..."):
outputs: RunOutputs = run_pipeline(
csv_path=csv_path,
goal=goal,
outdir=str(output_dir),
model=model,
num_slides=num_slides,
num_charts=num_charts
)
st.session_state.outputs = outputs
st.session_state.generated = True
progress_bar.progress(100)
except Exception as e:
status_container.markdown(
f'<div class="error-box"> <b>Error:</b> {str(e)}</div>',
unsafe_allow_html=True
)
st.exception(e)
st.session_state.generated = False
if st.session_state.generated and st.session_state.outputs:
outputs = st.session_state.outputs
try:
if "Error" not in outputs.logs:
status_container.markdown(
'<div class="success-box"><b>Success!</b> Artifacts generated successfully</div>',
unsafe_allow_html=True
)
tab1, tab2, tab3, tab4 = st.tabs(["PowerPoint", "Excel", "JSON Plan", "Logs"])
with tab1:
st.markdown("### PowerPoint Slides")
pptx_path = Path(outputs.pptx_path)
if pptx_path.exists():
try:
prs = Presentation(str(pptx_path))
slide_count = len(prs.slides)
except:
slide_count = "unknown"
with open(pptx_path, "rb") as f:
st.download_button(
label="Download PowerPoint",
data=f.read(),
file_name="investor_update.pptx",
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation",
width='stretch',
key="pptx_download"
)
st.markdown("---")
col_info1, col_info2 = st.columns(2)
with col_info1:
st.metric("Slides", slide_count)
with col_info2:
file_size = pptx_path.stat().st_size / 1024 # KB
st.metric("File Size", f"{file_size:.1f} KB")
else:
st.error("PowerPoint file not found")
with tab2:
st.markdown("### Excel Workbook")
xlsx_path = Path(outputs.xlsx_path)
if xlsx_path.exists():
with open(xlsx_path, "rb") as f:
st.download_button(
label="Download Excel",
data=f.read(),
file_name="investor_update.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
width='stretch',
key="xlsx_download"
)
wb = openpyxl.load_workbook(xlsx_path)
st.markdown("#### Data Preview")
ws = wb["Model"]
data = []
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=2, values_only=True):
data.append(row)
df_preview = pd.DataFrame(data, columns=headers)
try:
st.dataframe(df_preview.head(10), width='stretch')
except Exception:
st.table(df_preview.head(10))
if len(df_preview) > 10:
st.caption(f"Showing first 10 of {len(df_preview)} rows")
else:
st.error("Excel file not found")
with tab3:
st.markdown("### Schema-Locked JSON Plan")
plan_path = Path(outputs.plan_path)
if plan_path.exists():
with open(plan_path, "r") as f:
plan_data = json.load(f)
st.download_button(
label="Download JSON",
data=json.dumps(plan_data, indent=2),
file_name="plan.json",
mime="application/json",
key="json_download"
)
st.markdown("---")
st.json(plan_data)
col_a, col_b = st.columns(2)
with col_a:
st.metric("Charts Generated", len(plan_data.get("charts", [])))
with col_b:
st.metric("Slides Generated", len(plan_data.get("slides", [])))
else:
st.error("JSON plan not found")
with tab4:
st.markdown("### Generation Logs")
st.code(outputs.logs, language="text")
else:
status_container.markdown(
f'<div class="error-box"> <b>Error occurred</b><br>{outputs.logs}</div>',
unsafe_allow_html=True
)
except Exception as e:
st.error(f"Error displaying results: {str(e)}")
st.exception(e)
except Exception as e:
st.error(f"Fatal Error: {str(e)}")
st.exception(e)
st.info("Try installing dependencies: pip install -r requirements.txt")
if __name__ == "__main__":
try:
main()
except Exception as e:
import sys
print(f"Fatal error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
Pada lapisan Streamlit, kita menyiapkan kerangka aplikasi dengan st.set_page_config( layout="wide"), lalu membagi layar menggunakan st.columns() sehingga sisi kiri mengumpulkan input sementara sisi kanan menampilkan kontrol proses dan output. Pipeline hanya berjalan saat pengguna mengeklik st.button(), di mana kita terlebih dahulu mengantisipasi unggahan yang hilang, membungkus st.spinner() agar UI tidak terlihat beku, lalu mengirimkan artefak kembali ke pengguna dengan st.download_button() untuk berkas Excel, PowerPoint, dan rencana.
Dengan langkah ini selesai, kita simpan semuanya sebagai app.py dan luncurkan pengalaman penuh dengan:
streamlit run app.pyKesimpulan
Dalam tutorial ini, kita membangun pipeline lengkap “CSV ke artefak” yang menggunakan GPT-5.2 untuk mengembalikan rencana berisi bagan, struktur slide, dan sitasi rentang sel. Kita kemudian memvalidasi rencana itu sebelum menghasilkan apa pun. Pendekatan rencana-lalu-render ini selaras dengan kekuatan GPT-5.2 pada alur kerja multi-langkah seperti spreadsheet dan presentasi.
Hasilnya adalah alur kerja yang dapat diulang di mana pengguna mengunggah CSV, kode membangun fondasi workbook yang bersih, GPT-5.2 menghasilkan rencana yang hanya dapat menyitir rentang Excel pada daftar-izin, dan perender mematerialkan bagan dan slide menggunakan openpyxl dan python-pptx.
Dari sini, Anda dapat memperluas demo tanpa mengubah logika inti seperti menambah jenis bagan, memperkenalkan template slide, mendukung penggabungan multi-CSV, dan sebagainya. Pola kuncinya tetap sama, yaitu perlakukan model sebagai perencana yang harus mematuhi skema, dan pertahankan semua logika rendering di kode agar output tetap konsisten dan mudah di-debug.
Saya adalah Google Developers Expert di ML (Gen AI), Kaggle 3x Expert, dan Women Techmakers Ambassador dengan pengalaman lebih dari 3 tahun di bidang teknologi. Saya ikut mendirikan startup health-tech pada 2020 dan sedang menempuh studi magister ilmu komputer di Georgia Tech dengan spesialisasi machine learning.
FAQ
Mengapa memasangkan GPT-5.2 dengan Streamlit?
Streamlit memudahkan mengubah alur kerja AI menjadi aplikasi interaktif tanpa membangun frontend penuh. Dipadukan dengan GPT-5.2, ini memungkinkan pengembang bergerak cepat dari eksperimen ke alat siap pakai yang menerima input, menjalankan proses berbasis model, dan mengembalikan output nyata.
Apa itu Structured Outputs dan mengapa penting di sini?
Structured Outputs memaksa model mengembalikan data yang sesuai persis dengan JSON Schema yang telah ditentukan. Ini memastikan bagan, slide, metrik, dan sitasi mengikuti aturan ketat, memungkinkan rencana divalidasi sebelum berkas Excel atau PowerPoint dibuat.
Apakah saya perlu pengalaman frontend mendalam untuk membangun aplikasi seperti ini?
Tidak. Streamlit dirancang agar sebagian besar pekerjaan terjadi di Python, menggunakan alur kontrol dan pustaka yang familier. Ini menurunkan hambatan bagi data scientist dan analis yang ingin menggelar alat bertenaga AI tanpa mempelajari JavaScript atau kerangka kerja web yang kompleks.

