GPT-5.2 to mocny model do generowania przepływów pracy i wieloetapowych pipeline’ów, które wymagają niezawodności, struktury i mniejszej liczby halucynacji. OpenAI podkreśla ulepszenia w pracy na długich kontekstach oraz jeszcze lepszą wydajność w zadaniach w stylu arkuszy kalkulacyjnych.
W tym samouczku zbudujemy aplikację Streamlit, która zachowuje się jak młodszy analityk:
Prześle plik CSV i zapisze cel (na przykład: „aktualizacja na 6 slajdów z naciskiem na wzrost, retencję, unit economics, runway”)
Wygeneruje plan JSON z blokadą schematu
Utworzy skoroszyt Excel z danymi surowymi, metrykami pochodnymi i wykresami
Na końcu wygeneruje odpowiadającą prezentację PowerPoint z poprawnymi cytowaniami
Kluczowe jest to, że GPT-5.2 nie „tworzy slajdów” bezpośrednio. Generuje ścisły plan, który Pana/Pani kod wykonuje w sposób deterministyczny.
GPT-5.2 to naj nowszy model z serii GPT-5z ulepszeniami dla zadań end-to-end, takich jak tworzenie arkuszy kalkulacyjnych, budowanie prezentacji, pisanie i debugowanie kodu, rozumienie długich kontekstów oraz korzystanie z narzędzi.
Oto właściwości GPT-5.2, które są najistotniejsze dla tego projektu:
Wnioskowanie na długim kontekście: GPT-5.2 Thinking jest opisywany jako najnowocześniejszy w ewaluacjach długiego kontekstu (OpenAI MRCRv2), co w praktyce przekłada się na spójną pracę na bardzo długich dokumentach i wejściach wieloplikowych.
Wieloetapowe wykonanie: Ten model wyróżnia się w złożonych, wieloetapowych projektach, w tym w silniejszym wywoływaniu narzędzi i zachowaniach długoterminowych, co bezpośrednio wspiera nasze planowanie z blokadą schematu i deterministyczną pętlę renderowania.
Lepsza faktualność: GPT-5.2 Thinking popełnia mniej błędów niż GPT-5.1 Thinking, co ma znaczenie, ponieważ zmniejsza ryzyko generowania tekstów/liczb odbiegających od cytowanych zakresów w Excelu.
Kompro misy dot. opóźnień: W API można iterować z GPT-5.2 Instant dla szybkiego feedbacku, a następnie przełączyć się na GPT-5.2 Thinking lub Pro, gdy potrzebne jest silne planowanie end-to-end i wyższa jakość artefaktów, pamiętając, że bardziej złożone generacje mogą trwać minuty.
Przykładowy projekt GPT-5.2: Zbuduj generator ustrukturyzowanych artefaktów
W tej części zbudujemy generator ustrukturyzowanych artefaktów (Excel i PowerPoint) korzystający z modelu GPT 5.2 opakowanego w aplikację Streamlit. W skrócie, finalna aplikacja Streamlit robi to:
Czyta CSV i buduje kanoniczny skoroszyt
Następnie wywołuje GPT-5.2 przez OpenAI lub OpenRouter z ustrukturyzowanymi wynikami (response_format: json_schema)
Weryfikuje zwrócony plan względem naszego schematu
Renderuje wykresy w Excelu i styluje je zgodnie z paletą marki
Na koniec generuje PowerPoint z osadzonymi wykresami i cytowaniami
Zbudujmy to krok po kroku.
Krok 1: Wymagania wstępne i konfiguracja
Zanim zaczniemy generować skoroszyty Excel i prezentacje PowerPoint z GPT-5.2, musimy przygotować środowisko lokalne. Zaczniemy od instalacji podstawowych bibliotek i ustawienia kluczy API jako zmiennych środowiskowych.
pip install streamlit pandas pydantic python-dotenv requests openpyxl python-pptxObsługiwane przez
Użyjemy Streamlit do zbudowania interfejsu webowego, Pandas do wczytywania i przygotowania CSV, Pydantic do walidacji planu JSON z blokadą schematu od GPT-5.2. Skorzystamy też z python-dotenv do bezpiecznego wczytywania kluczy, takich jak OPENROUTER_API_KEY/OPENAI_API_KEY, z pliku .env. Biblioteka requests wywołuje endpoint zgodny z OpenRouter/OpenAI, natomiast openpyxl generuje skoroszyt Excel i wykresy, a python-pptx tworzy prezentację PowerPoint z natywnymi (edytowalnymi) wykresami.
Następnie ustawiamy klucz(e) API:
exportOPENAI_API_KEY="your_key"exportOPENROUTER_API_KEY="your_key"Obsługiwane przez
Proszę zauważyć, że do GPT 5.2 można uzyskać dostęp przez wiele usług, w tym oficjalne API OpenAI oraz usługi takie jak OpenRouter i inne.
Zalecane jest użycie pliku .env do lokalnego przechowywania kluczy API:
W pliku .env proszę zapisać następujące wpisy:
OPENROUTER_API_KEY="your_key"OPENAI_API_KEY="your_key"Obsługiwane przez
Następnie proszę wczytać te klucze API za pomocą biblioteki load_dotenv:
Na tym etapie środowisko jest gotowe do uwierzytelnienia w API OpenAI.
Krok 2: Definiowanie schematu planu
Następnie definiujemy plan artefaktów z blokadą schematu, czyli ścisły szablon JSON, który mówi GPT-5.2 dokładnie, jakie wykresy utworzyć i jak ustrukturyzować slajdy, aby wynik modelu można było zdekodować i zweryfikować przy użyciu Structured Outputs poprzez response_format: { type: "json_schema", strict: true }, zamiast swobodnego tekstu.
import os
import io
import re
import json
import math
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Literal, Optional, Tuple
import requests
import pandas as pd
import numpy as np
import streamlit as st
import openpyxl
from dotenv import load_dotenv
from pydantic import BaseModel, Field, ValidationError
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter
from openpyxl.chart import LineChart, BarChart, Reference
from openpyxl.chart.series import SeriesLabel
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
ChartKind = Literal["line","bar"]defsafe_float(x)->float:try:if x isNone:return0.0ifisinstance(x,(int,float)):returnfloat(x)returnfloat(str(x).replace(",","").replace("$",""))except Exception:return0.0deffmt_money(v:float)->str:
sign ="-"if v <0else""
v =abs(v)if v >=1_000_000:returnf"{sign}${v/1_000_000:.2f}M"if v >=1_000:returnf"{sign}${v/1_000:.1f}K"returnf"{sign}${v:.0f}"deffmt_pct(v:float)->str:if v isNoneor(isinstance(v,float)and(math.isnan(v)or math.isinf(v))):return"n/a"returnf"{v*100:.1f}%"classChartSeriesSpec(BaseModel):
name:str
metric:strclassChartSpec(BaseModel):id:str
kind: ChartKind
title:str
x_metric:str
series: List[ChartSeriesSpec]
anchor:strclassSlideBullet(BaseModel):
text:str
citations: List[str]= Field(min_length=1, max_length=4)classSlideSpec(BaseModel):
title:str
chart_id: Optional[str]=None
bullets: List[SlideBullet]= Field(min_length=2, max_length=4)classArtifactPlan(BaseModel):
charts: List[ChartSpec]= Field(min_length=3, max_length=6)
slides: List[SlideSpec]= Field(min_length=4, max_length=10)Obsługiwane przez
Powyższy fragment kodu definiuje plan z użyciem modeli Pydantic, które robią trzy ważne rzeczy:
Struktura planu:ChartKind = Literal["line", "bar"] ogranicza typy wykresów do małej listy, ChartSpec opisuje pełny kontrakt wykresu, a SlideSpec definiuje układ slajdu, opcjonalnie łącząc slajd z wykresem przez chart_id.
Automatyczne egzekwowanie jakości: Ograniczenia jak Field() gwarantują, że zawsze otrzymamy wystarczającą liczbę wykresów/slajdów, każdy slajd ma odpowiednią liczbę punktów, a każdy punkt zawiera cytowania.
Planowanie i renderowanie: GPT-5.2 tworzy wyłącznie plan, tzn. wykresy, slajdy i cytowania jako ustrukturyzowany JSON, a reszta kodu odpowiada za wykonanie, formatowanie Excela, renderowanie wykresów i układ decku.
Dzięki temu schematowi GPT-5.2 staje się planerem, który dostarcza zweryfikowany plan JSON.
Krok 3: Wywołanie GPT-5.2 z ustrukturyzowanymi wynikami
Mając zdefiniowany schemat planu, kolejnym krokiem jest wywołanie GPT-5.2 w „trybie schematu” — tak, aby zwrócił szablon JSON, który można zweryfikować i wykonać. Zamiast liczyć, że model się „zachowa”, mówimy API, aby dekodował bezpośrednio do Pana/Pani JSON Schema przy użyciu response_format: { type: "json_schema", ... strict: true }, co jest dokładnie celem Structured Outputs.
Funkcja chat_json_schema() to jedyna brama zamieniająca prompt na zweryfikowany plan:
Routing dostawcy (OpenRouter vs OpenAI): Jeśli nazwa modelu ma przestrzeń nazw (openai/gpt-5.2), domyślnie wybierasz OpenRouter, ponieważ to format, którego używa OpenRouter do routingu modeli. Jeśli jawnie ustawisz api_provider="openai", wyślesz ten sam payload Chat Completions do endpointu OpenAI. Można całkowicie pominąć OpenRouter.
Ustawianie endpointów: Dla OpenAI wywołujesz endpoint https://api.openai.com/v1/chat/completions ze standardową autoryzacją Bearer. Dla OpenRouter wywołujesz https://openrouter.ai/api/v1/chat/completions i opcjonalnie uwzględniasz nagłówki atrybucji, takie jak HTTP-Referer i X-Title, które OpenRouter zaleca jako metadane.
Dekodowanie schematu przez response_format: Kluczowy blok to response_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, który instruuje model, aby produkował wynik ograniczony do naszego JSON Schema.
Następnie zweryfikujemy plan JSON modelu przy pomocy Pydantic i tylko jeśli przejdzie kontrolę ograniczeń, przystąpimy do renderowania wykresów w Excelu i generowania prezentacji PowerPoint.
Krok 4: Funkcje pomocnicze
Zanim cokolwiek wyrenderujemy, potrzebujemy niewielkiej warstwy, która sprawi, że każdy przesłany CSV będzie bezpieczny do analizy i spójny do odwołań. Te funkcje pomocnicze wykonują cztery zadania: wczytują i czyszczą CSV, tworzą tabelę modelową z metrykami pochodnymi, wyliczają zwięzły zestaw faktów do promptu i sprawiają, że wynik w Excelu jest czytelny dzięki automatycznemu dostosowaniu szerokości kolumn.
Krok 4.1: Wczytanie i walidacja CSV
GPT 5.2 nie zaplanuje wiarygodnie wykresów i slajdów, jeśli tabela wejściowa jest pusta albo pola liczbowe są przechowywane jako ciągi znaków. Ta funkcja standaryzuje zbiór danych, aby dalsze kroki nie musiały zgadywać.
defload_and_validate_csv(csv_path:str)-> pd.DataFrame:
df = pd.read_csv(csv_path)iflen(df)==0:raise ValueError("CSV is empty")iflen(df.columns)==0:raise ValueError("CSV has no columns")
date_cols =[c for c in df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]if date_cols:
date_col = date_cols[0]
df[date_col]= df[date_col].astype(str)for col in df.columns:if col notin date_cols:try:
df[col]= pd.to_numeric(df[col])if df[col].dtype in['float64','int64']:
df[col]= df[col].fillna(0)except(ValueError, TypeError):passreturn dfObsługiwane przez
Powyższa funkcja:
Wczytuje plik do DataFrame przy użyciu pd.read_csv.
Wykrywa kolumny na podstawie heurystyk nazewniczych (date, month, week, period) i wymusza ich typ na string. Dzięki temu etykiety na wykresach i w tekście slajdów są stabilne.
Na koniec konwertuje kolumny inne niż daty na numeryczne przy użyciu pd.to_numeric, a brakujące wartości liczbowe wypełnia zerem, by uniknąć propagacji NaN.
Po tym kroku ma Pan/i DataFrame, który nie jest pusty i zawiera kolumny liczbowe.
Krok 4.2: Obliczenie DataFrame modelu
Surowe CSV bywają niechlujne i niespójne między domenami. Arkusz modelu działa jako warstwa normalizacji, w której dodajemy metryki pochodne przydatne do wykresów, bez wymagania od użytkownika ręcznych obliczeń.
defcompute_model_df(raw_df: pd.DataFrame)-> pd.DataFrame:
df = raw_df.copy()
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()if"new_mrr"in df.columns and"churned_mrr"in df.columns:
df["net_new_mrr"]= df["new_mrr"]- df["churned_mrr"]
revenue_cols =[c for c in df.columns ifany(x in c.lower()for x in['revenue','mrr','income','sales'])]
cost_cols =[c for c in df.columns ifany(x in c.lower()for x in['cost','cogs','expense'])]if revenue_cols and cost_cols:
revenue = df[revenue_cols[0]]
cost = df[cost_cols[0]]
df["gross_margin"]=((revenue - cost)/ revenue.replace(0, pd.NA)).fillna(0)for col in numeric_cols[:5]:if col notin df.columns:continue
df[f"{col}_change"]= df[col].pct_change().fillna(0)return dfObsługiwane przez
Funkcja compute_model_df() robi dwie ogólnie użyteczne rzeczy:
Wyszukuje główną kolumnę wyniku i drugorzędną kolumnę wejściową i oblicza prosty iloraz, aby uchwycić, jaka część sygnału podstawowego pozostaje po uwzględnieniu sygnału drugorzędnego.
Dla kilku kolumn liczbowych oblicza pct_change(), które domyślnie zwraca ułamek zmiany względem poprzedniego wiersza — to wygodna baza do uchwycenia ruchu w dowolnej metryce szeregów czasowych.
Krok 4.3: Obliczenie faktów
Nawet przy Structured Outputs chcemy dostarczyć GPT-5.2 zwięzłe podsumowanie tego, co się zmieniło w analizowanym okresie. Te fakty działają też jako prompt, który model może wykorzystać w punktach na slajdach.
defcompute_facts(model_df: pd.DataFrame)-> Dict[str,str]:defdelta(a:float, b:float)->float:return0.0if a ==0else(b - a)/ a
iflen(model_df)==0:return{"error":"No data"}
first = model_df.iloc[0]
last = model_df.iloc[-1]
date_cols =[c for c in model_df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]
period_col = date_cols[0]if date_cols else model_df.columns[0]
facts ={"period":f"{first[period_col]} → {last[period_col]}","row_count":len(model_df),"column_count":len(model_df.columns),}
numeric_cols = model_df.select_dtypes(include=[np.number]).columns.tolist()for i, col inenumerate(numeric_cols[:5]):if col in first.index and col in last.index:
start_val = safe_float(first[col])
end_val = safe_float(last[col])
avg_val = safe_float(model_df[col].mean())
facts[f"{col}_start"]= fmt_money(start_val)if start_val >=100elsef"{start_val:.2f}"
facts[f"{col}_end"]= fmt_money(end_val)if end_val >=100elsef"{end_val:.2f}"
facts[f"{col}_change_pct"]= fmt_pct(delta(start_val, end_val))
facts[f"{col}_avg"]= fmt_money(avg_val)if avg_val >=100elsef"{avg_val:.2f}"return factsObsługiwane przez
Najpierw wybieramy pierwszy i ostatni wiersz, aby zdefiniować okno okresu. Następnie wybieramy kolumnę (date, month, week, period) do czytelnego raportowania dla człowieka.
Dla niewielkiego zestawu kolumn liczbowych wyliczamy migawkę wartości początkowej, końcowej, średniej i całkowitej zmiany procentowej od początku do końca. Powstaje lekki słownik sygnałów, który pomaga modelowi pisać punkty na slajdach.
Krok 4.4: Automatyczne dopasowanie szerokości kolumn w Excelu
Domyślne szerokości kolumn Excela sprawiają, że generowane skoroszyty wyglądają surowo. Autosizing to prosty zabieg, który od razu poprawia użyteczność.
defautosize_columns(ws, max_width:int=45):for col in ws.columns:
max_len =0
col_letter = get_column_letter(col[0].column)for cell in col:if cell.value isNone:continue
max_len =max(max_len,len(str(cell.value)))
ws.column_dimensions[col_letter].width =min(max_len +2, max_width)Obsługiwane przez
Funkcja autosize_columns() skanuje każdą kolumnę, znajduje maksymalną długość ciągu i ustawia ws.column_dimensions[col_letter].width. Biblioteka OpenPyXL udostępnia column_dimensions i jej właściwość width do kontrolowania szerokości kolumn w Excelu.
Następnie podłączymy oczyszczoną tabelę modelu i fakty do promptu planowania GPT-5.2, zweryfikujemy zwrócony plan, a potem wyrenderujemy wykresy i slajdy.
Krok 5: Zbudowanie skoroszytu
Korzystając z oczyszczonej tabeli modelu i lekkiego zestawu faktów z poprzedniego kroku, generujemy teraz deterministyczny skoroszyt Excel w openpyxl.
defbuild_workbook_base(raw_df: pd.DataFrame, model_df: pd.DataFrame, out_xlsx:str)-> Tuple[str,int]:
wb = Workbook()
wb.remove(wb.active)
header_fill = PatternFill("solid", fgColor="F2F2F2")
header_font = Font(bold=True)
ws_raw = wb.create_sheet("Raw")for j, col inenumerate(raw_df.columns, start=1):
c = ws_raw.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")for i, row inenumerate(raw_df.itertuples(index=False), start=2):for j, val inenumerate(row, start=1):
ws_raw.cell(row=i, column=j, value=val)
ws_raw.freeze_panes ="A2"
autosize_columns(ws_raw)
row_end =1+len(raw_df)
ws_model = wb.create_sheet("Model")for j, col inenumerate(model_df.columns, start=1):
c = ws_model.cell(row=1, column=j, value=col)
c.font = header_font
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center")for i, row inenumerate(model_df.itertuples(index=False), start=2):for j, val inenumerate(row, start=1):
ws_model.cell(row=i, column=j, value=val)
ws_model.freeze_panes ="A2"
autosize_columns(ws_model)
col_index ={name: idx +1for idx, name inenumerate(model_df.columns)}
money_keywords =['mrr','revenue','income','sales','cost','expense','cash','balance','burn','price','amount','fee']
pct_keywords =['margin','rate','ratio','pct','percent','change','growth']for col_name, col_idx in col_index.items():
col_lower = col_name.lower()
is_money =any(kw in col_lower for kw in money_keywords)
is_pct =any(kw in col_lower for kw in pct_keywords)or'margin'in col_lower
is_large_number =Falseif col_name in model_df.columns:
sample_vals = model_df[col_name].dropna()iflen(sample_vals)>0and pd.api.types.is_numeric_dtype(sample_vals):
max_val =abs(sample_vals.max())
is_large_number = max_val >1000for r inrange(2, row_end +1):if is_money or is_large_number:
ws_model.cell(row=r, column=col_idx).number_format ="$#,##0"elif is_pct:
ws_model.cell(row=r, column=col_idx).number_format ="0.0%"elif pd.api.types.is_numeric_dtype(model_df[col_name]):
ws_model.cell(row=r, column=col_idx).number_format ="0.0"
ws_charts = wb.create_sheet("Charts")
ws_charts["A1"]="Charts"
ws_charts["A1"].font = Font(bold=True)
wb.save(out_xlsx)return out_xlsx, row_endObsługiwane przez
Oto co robi ten budowniczy skoroszytu:
Tworzy szkielet skoroszytu: Wywołanie Workbook() instancjuje nowy obiekt skoroszytu, a wb.remove(wb.active) usuwa domyślny arkusz, abyśmy mieli pełną kontrolę nad kolejnością arkuszy. Nowe arkusze dodajemy jawnie: wb.create_sheet("Raw"), wb.create_sheet("Model") i wb.create_sheet("Charts").
Zapisuje arkusz Raw: Zapisujemy wiersz nagłówka przy użyciu Font, PatternFill i Alignment, a następnie dodajemy każdy wiersz z raw_df. Funkcja .autosize_columns(ws_raw) poprawia czytelność bez ręcznego zmieniania szerokości.
Wyznacza granice: Używamy row_end = 1 + len(raw_df), co daje stabilny ostatni wiersz, aby bezpiecznie generować zakresy, jak A2:A{row_end} dla kategorii i cytowań wykresu.
Stosuje heurystyki formatowania liczb: Następnie ustawiamy cell.number_format na podstawie słów-kluczy w nazwach kolumn i dtype, aby Excel wyświetlał wartości jako waluty, procenty lub zwykłe liczby.
Tworzy arkusz Charts: Wywołaniewb.create_sheet("Charts") daje nam stabilne płótno dla zakotwiczeń wykresów. To odpowiada przepływowi pracy openpyxl dla wykresów: tworzymy LineChart lub BarChart, wiążemy zakresy przez Reference, ustawiamy kategorie przez set_categories, a następnie umieszczamy wykres metodą ws.add_chart().
Po tym kroku ma Pan/i spójny plik Excel, do którego kolejny etap może bezpiecznie odwoływać się przez dokładne zakresy komórek. Krok 6: Renderowanie wykresów
Skoro struktura skoroszytu jest ustalona, możemy wyrenderować wykresy w arkuszu Charts i zastosować spójny wygląd marki. Zastosowałem paletę kolorów DataCamp:
Kolor podstawowy: #01ef63
Kolor dodatkowy: #203147
defadd_charts_to_workbook(xlsx_path:str, charts: List[ChartSpec], metric_to_range: Dict[str,str]):
wb = load_workbook(xlsx_path)
ws_model = wb["Model"]
ws_charts = wb["Charts"]
header_map ={ws_model.cell(1, c).value: c for c inrange(1, ws_model.max_column +1)}defref_from_metric(metric:str, row_start:int, row_end:int)-> Reference:if metric notin header_map:raise ValueError(f"Unknown metric in Model sheet: {metric}")
col = header_map[metric]return Reference(ws_model, min_col=col, min_row=row_start, max_col=col, max_row=row_end)for ch in charts:
chart = LineChart()if ch.kind =="line"else BarChart()
chart.title = ch.title
x_metric = ch.x_metric
if x_metric notin header_map:
date_keywords =['date','time','month','year','period','week']
date_cols =[col for col in header_map.keys()if col andany(kw instr(col).lower()for kw in date_keywords)]if date_cols:
x_metric = date_cols[0]else:
x_metric =list(header_map.keys())[0]if header_map else"month"
x_ref = ref_from_metric(x_metric,2, ws_model.max_row)
chart.set_categories(x_ref)
chart_colors =["01EF63","203147"]for i, s inenumerate(ch.series):
y_ref = ref_from_metric(s.metric,2, ws_model.max_row)
chart.add_data(y_ref, titles_from_data=False)
chart.series[-1].title = SeriesLabel(v=s.name)
series = chart.series[-1]
color_hex = chart_colors[i %len(chart_colors)]try:if ch.kind =="line":
series.graphicalProperties.line.solidFill = color_hex
series.graphicalProperties.line.width =30000else:
series.graphicalProperties.solidFill = color_hex
except AttributeError:pass
anchor = ch.anchor.strip()if"!"in anchor:
anchor = anchor.split("!")[-1]if anchor and anchor.isalpha():
anchor =f"{anchor}2"ifnot re.match(r'^[A-Z]+[0-9]+Obsługiwane przez
Funkcja add_charts_to_workbook() to silnik renderowania wykresów zamieniający nasz plan schematu w markowe wizualizacje w Excelu.
Wczytanie skoroszytu: Zaczynamy od otwarcia wygenerowanego pliku Excela za pomocą load_workbook(xlsx_path). Następnie budujemy header_map z pierwszego wiersza, mapując każdą nazwę kolumny na jej indeks kolumny w Excelu.
Budowa referencji zakresów komórek: Funkcja Reference() definiuje dokładny zakres do wykreślenia, a chart.set_categories(x_ref) podłącza kategorie osi X. Standardowy wzorzec to tworzenie obiektów referencji, dodawanie danych, a potem ustawianie kategorii.
Inicjalizacja typu wykresu: Dla każdego specyfikatora wykresu tworzymy LineChart() lub BarChart() i ustawiamy chart.title.
Dodanie palety marki: Dla każdej serii w specyfikacji wiążemy zakres Y za pomocą chart.add_data(), a następnie ustawiamy etykietę wyświetlania przy użyciu SeriesLabel().
Na końcu funkcja ws_charts.add_chart() umieszcza każdy wykres w określonej komórce lewego górnego rogu.
Krok 7: Renderowanie prezentacji PowerPoint
W tym kroku plan staje się rzeczywistą prezentacją. Tworzymy PowerPoint, układamy tytuły i punkty, osadzamy wykresy zbudowane z tej samej tabeli co Excel i dodajemy stopkę zachowującą cytowania zakresów komórek.
defbuild_pptx(out_pptx:str, model_df: pd.DataFrame, plan: ArtifactPlan):
prs = Presentation()
blank = prs.slide_layouts[6]
chart_by_id ={c.id: c for c in plan.charts}defadd_title(slide, title:str):
box = slide.shapes.add_textbox(Inches(0.5), Inches(0.2), Inches(9.0), Inches(0.6))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = title
p.font.size = Pt(32)
p.font.bold =Truedefadd_bullets(slide, bullets: List[SlideBullet], left, top, width, height):
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.word_wrap =True
tf.clear()for i, b inenumerate(bullets):
p = tf.paragraphs[0]if i ==0else tf.add_paragraph()
p.text = b.text
p.font.size = Pt(18)
p.level =0
p.space_before = Pt(4)
p.space_after = Pt(8)defadd_sources_footer(slide, bullets: List[SlideBullet]):
sources =[]
seen =set()for b in bullets:for c in b.citations:if c notin seen:
seen.add(c)
sources.append(c)ifnot sources:return
text ="Sources: "+"; ".join(sources)
box = slide.shapes.add_textbox(Inches(0.5), Inches(6.8), Inches(9.0), Inches(0.4))
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(9)
p.font.italic =Truedefadd_chart(slide, chart_spec: ChartSpec, left, top, width, height):
date_cols =[c for c in model_df.columns ifany(x in c.lower()for x in['date','time','month','year','period','week'])]
x_col = date_cols[0]if date_cols else model_df.columns[0]
categories = model_df[x_col].astype(str).tolist()
chart_data = CategoryChartData()
chart_data.categories = categories
chart_colors =["01EF63","203147"]for i, s inenumerate(chart_spec.series):
values = model_df[s.metric].apply(safe_float).tolist()
chart_data.add_series(s.name, values)
chart_type = XL_CHART_TYPE.LINE if chart_spec.kind =="line"else XL_CHART_TYPE.COLUMN_CLUSTERED
graphic_frame = slide.shapes.add_chart(chart_type, left, top, width, height, chart_data)
chart = graphic_frame.chart
chart.has_title =True
chart.chart_title.text_frame.text = chart_spec.title
chart.has_legend =True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout =Falsefrom pptx.dml.color import RGBColor
for i, series inenumerate(chart.series):
color_hex = chart_colors[i %len(chart_colors)]
color_rgb = RGBColor(int(color_hex[0:2],16),int(color_hex[2:4],16),int(color_hex[4:6],16))
fill = series.format.fill
fill.solid()
fill.fore_color.rgb = color_rgb
if chart_spec.kind =="line":
line = series.format.line
line.color.rgb = color_rgb
line.width = Pt(3)for s in plan.slides:
slide = prs.slides.add_slide(blank)
add_title(slide, s.title)
has_chart =bool(s.chart_id and s.chart_id in chart_by_id)if has_chart:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),)
add_chart(
slide,
chart_by_id[s.chart_id],
left=Inches(5.2),
top=Inches(1.0),
width=Inches(4.5),
height=Inches(5.5),)else:
add_bullets(
slide,
s.bullets,
left=Inches(0.5),
top=Inches(1.0),
width=Inches(9.0),
height=Inches(5.5),)
add_sources_footer(slide, s.bullets)
prs.save(out_pptx)defbuild_allowed_ranges(model_df: pd.DataFrame, row_end:int)-> Dict[str,str]:
ranges ={}for i, col inenumerate(model_df.columns, start=1):
col_letter = get_column_letter(i)
ranges[col]=f"Model!{col_letter}2:{col_letter}{row_end}"return ranges
defvalidate_plan(plan: ArtifactPlan, allowed_ranges: Dict[str,str])->None:
allowed_set =set(allowed_ranges.values())
chart_ids ={c.idfor c in plan.charts}iflen(chart_ids)!=len(plan.charts):raise ValueError("Duplicate chart ids in plan.")for s in plan.slides:if s.chart_id and s.chart_id notin chart_ids:raise ValueError(f"Slide references unknown chart_id: {s.chart_id}")for s in plan.slides:for b in s.bullets:for c in b.citations:if c notin allowed_set:raise ValueError(f"Citation not in allowed set: {c}")
allowed_metrics =set(allowed_ranges.keys())for c in plan.charts:if c.x_metric notin allowed_metrics:passfor s in c.series:if s.metric notin allowed_metrics:raise ValueError(f"Unknown series metric: {s.metric}")Obsługiwane przez
Przyjrzyjmy się powyższemu kodowi krok po kroku:
Tworzenie decku i indeksowanie wykresów: prs = Presentation() rozpoczyna nowy plik PPTX, a blank = prs.slide_layouts[6] daje pusty szablon do precyzyjnego rozmieszczenia.
Tytuły i punkty: Funkcja add_title() zapisuje tekst do text_frame w polu tekstowym, a add_bullets() dodaje po jednym akapicie na każdy punkt, kontrolując rozmiar czcionki i odstępy przez formatowanie akapitów.
Stopka z cytowaniami: Funkcja add_sources_footer() usuwa duplikaty cytowań z punktów i renderuje zwartą linię Sources: na dole, by zachować możliwość śledzenia bez zaśmiecania slajdu.
Osadzanie wykresów: add_chart() buduje obiekt CategoryChartData() używając kolumny czasu jako osi X i zaplanowanych serii, po czym wstawia go poprzez slide.shapes.add_chart(), które zwraca GraphicFrame.
Styl marki: Na końcu konwertujemy hex na RGBColor i stosujemy go dla każdej serii.
Zasady układu i zabezpieczenia: Jeśli slajd ma poprawny chart_id, używamy układu dwukolumnowego, w przeciwnym razie — pełnej szerokości na punkty. Funkcja build_allowed_ranges() tworzy listę dozwolonych zakresów do cytowania, a validate_plan() egzekwuje unikalne ID wykresów, poprawne referencje, dozwolone cytowania i poprawne metryki, aby model nie mógł wymyślać pól.
Krok 8: Wygenerowanie planu przez LLM
Na tym etapie mamy czyste dane tabelaryczne, które przekształcimy w plan budowy schematu (wykresy, slajdy i cytowania), możliwy do wykonania przez renderer.
defgenerate_plan_with_llm(
api_key:str,
model:str,
goal:str,
allowed_ranges: Dict[str,str],
facts: Dict[str,str],
num_slides:int=6,
num_charts:int=4,)-> ArtifactPlan:
plan_schema ={"type":"object","properties":{"charts":{"type":"array","minItems":3,"maxItems":6,"items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["line","bar"]},"title":{"type":"string"},"x_metric":{"type":"string"},"series":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","properties":{"name":{"type":"string"},"metric":{"type":"string"},},"required":["name","metric"],"additionalProperties":False,},},"anchor":{"type":"string"},},"required":["id","kind","title","x_metric","series","anchor"],"additionalProperties":False,},},"slides":{"type":"array","minItems":4,"maxItems":10,"items":{"type":"object","properties":{"title":{"type":"string"},"chart_id":{"type":["string","null"]},"bullets":{"type":"array","minItems":2,"maxItems":4,"items":{"type":"object","properties":{"text":{"type":"string"},"citations":{"type":"array","minItems":1,"maxItems":4,"items":{"type":"string"},},},"required":["text","citations"],"additionalProperties":False,},},},"required":["title","chart_id","bullets"],"additionalProperties":False,},},},"required":["charts","slides"],"additionalProperties":False,}
system =("You produce investor-grade slides with traceability.\n""Output MUST match the JSON schema exactly.\n""Rules:\n""1) Use ONLY the allowed citation ranges (exact strings).\n""2) No generic filler. Every bullet must contain at least ONE number (e.g., $X, X%, X mo).\n""3) Keep bullets short: <= 12 words.\n""4) Prefer trends (start→end) or averages; avoid speculation.\n""5) Provide exactly the requested number of charts and slides.\n""6) Use chart_id to attach the correct chart to each slide.\n""7) Chart anchors: Use cell references ONLY (e.g., 'A2', 'A20', 'A38'). Do NOT include sheet name like 'CHARTS!A'.\n")
user =(f"Goal: {goal}\n"f"Requested: {num_charts} charts and {num_slides} slides.\n\n"f"Facts (use these; do not invent):\n{json.dumps(facts, indent=2)}\n\n"f"Allowed citation ranges (use exact values only):\n{json.dumps(list(allowed_ranges.values()), indent=2)}\n\n""Metric keys available for charts:\n"f"{json.dumps(list(allowed_ranges.keys()), indent=2)}\n\n""Chart requirements:\n""- x_metric: Use a date/time column (e.g., 'month', 'date', 'period', 'week') for the X-axis.\n"" If no date column exists, use the first column from the available metrics.\n""- Create meaningful charts based on available metrics in the data.\n""- Focus on trends, comparisons, and key business metrics.\n""- Chart colors: Use these specific shades for all chart series:\n"" * Primary color: #01ef63 (bright green)\n"" * Secondary color: #203147 (dark blue/navy)\n"" * Alternate between these two colors for multiple series in the same chart.\n""Slide requirements:\n""- Create slides that tell a story with the available data.\n""- Suggested structure: Overview, Key Metrics, Trends, Analysis, Summary, Next Steps.\n""- Adapt slide titles and content to match the data domain (e.g., sales, marketing, operations, finance).\n")
messages =[{"role":"system","content": system},{"role":"user","content": user}]
plan_json = chat_json_schema(
model=model,
messages=messages,
json_schema=plan_schema,
api_key=api_key,
temperature=0.2,
max_tokens=6000,)try:
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)iflen(plan.slides)!= num_slides:raise ValueError(f"Expected {num_slides} slides, got {len(plan.slides)}")iflen(plan.charts)!= num_charts:raise ValueError(f"Expected {num_charts} charts, got {len(plan.charts)}")return plan
except(ValidationError, ValueError)as e:
repair = messages +[{"role":"assistant","content": json.dumps(plan_json)},{"role":"user","content":f"Fix JSON to satisfy schema + rules. Error:\n{str(e)}"},]
plan_json = chat_json_schema(
model=model,
messages=repair,
json_schema=plan_schema,
api_key=api_key,
temperature=0.0,
max_tokens=6000,)
plan = ArtifactPlan.model_validate(plan_json)
validate_plan(plan, allowed_ranges)iflen(plan.slides)!= num_slides orlen(plan.charts)!= num_charts:raise RuntimeError("Plan repaired but counts still wrong. Tighten prompt or increase max_tokens.")return planObsługiwane przez
Funkcja generate_plan_with_llm() to mózg tego dema. Tak wpinamy ją w pipeline:
Jawne JSON Schema: plan_schema definiuje jedyną dozwoloną postać odpowiedzi, charts[] i slides[], z polami wymaganymi i enumeracjami.
Szyny ochronne w promptach systemowych: Ustalamy niepodlegające negocjacji zasady, aby plan pozostał renderowalny i audytowalny. Ograniczamy też pozycjonowanie wykresów do zwykłych komórek, aby układ pozostawał deterministyczny, a nie improwizowany przez model. Taki wieloetapowy nadzór to dokładnie to, do czego zaprojektowano GPT-5.2.
Wiadomość użytkownika: Zawiera cel, żądane liczby, obliczone fakty i dozwolone klucze metryk. Ponieważ model nie widzi niczego innego, ma znacznie mniej sposobów na wymyślanie kolumn, zakresów czy niepopartych twierdzeń.
Dekodowanie zgodne ze schematem: Wywołujemy chat_json_schema() z response_format ustawionym na json_schema i strict: true, co wymusza zgodność ze schematem podczas dekodowania.
Walidacja w dwóch warstwach: Najpierw ArtifactPlan.model_validate(plan_json) sprawdza zgodność odpowiedzi z naszymi modelami Pydantic. Następnie validate_plan() egzekwuje ograniczenia domenowe.
Krok 9: Uruchomienie pipeline’u
Na końcu łączymy w całość wczytywanie CSV, generowanie Excela, planowanie przez LLM, renderowanie wykresów i finalny eksport do PPTX, a następnie zwracamy ścieżki wynikowe.
Połączmy wszystkie komponenty naszego pipeline’u krok po kroku:
Ustrukturyzowane wyniki pipeline’u: @dataclass tworzy lekki kontener dla xlsx_path, pptx_path, plan_path i logs, aby interfejs Streamlit mógł traktować wynik pipeline’u jako jedną ustrukturyzowaną wartość zamiast żonglować krotkami czy globalnymi zmiennymi.
Routing kluczy API: Metoda load_dotenv() wczytuje nasz plik .env do zmiennych środowiskowych, więc OPENAI_API_KEY i OPENROUTER_API_KEY można odczytać przez os.getenv(). Następnie wybieramy klucz dostawcy prostą regułą: jeśli istnieje OPENROUTER_API_KEY, używamy OpenRouter; w przeciwnym razie, jeśli dostępny, korzystamy z OpenAI.
Uruchomienie przygotowania danych: Wczytujemy i walidujemy CSV, obliczamy naszą tabelę modelową, a następnie tworzymy szkielet Excela przez build_workbook_base(). Funkcja generate_plan_with_llm() zwraca ArtifactPlan, który zapisujemy do JSON przy użyciu json.dump(), aby ułatwić inspekcję i debugowanie.
Renderowanie artefaktów: Funkcja add_charts_to_workbook() materializuje wykresy w arkuszu Charts, a build_pptx() buduje deck slajdów z planu i tabeli.
Po tym kroku nasza aplikacja Streamlit może wywołać run_pipeline(), wyświetlić logs i udostępnić trzy deterministyczne wyniki do pobrania: skoroszyt Excel, deck PPTX oraz plan JSON.
Krok 10: Interfejs Streamlit
Skoro rdzeń pipeline’u jest gotowy, ostatnim krokiem jest opakowanie go w aplikację Streamlit, aby każdy mógł przesłać plik, ustawić cel, wybrać model i pobrać wygenerowane artefakty.
load_dotenv()
st.set_page_config(
page_title="GPT-5.2 PPT and Excel Generator",
page_icon="",
layout="wide",
initial_sidebar_state="expanded")
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1f77b4;
margin-bottom: 1rem;
}
.sub-header {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.success-box {
padding: 1rem;
background-color: #d4edda;
border-left: 4px solid #28a745;
margin: 1rem 0;
}
.error-box {
padding: 1rem;
background-color: #f8d7da;
border-left: 4px solid #dc3545;
margin: 1rem 0;
}
.info-box {
padding: 1rem;
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
margin: 1rem 0;
}
.slide-preview {
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
""", unsafe_allow_html=True)defmain():try:if'outputs'notin st.session_state:
st.session_state.outputs =Noneif'generated'notin st.session_state:
st.session_state.generated =False
st.markdown('<div class="main-header">Spreadsheet to PowerPoint Generator</div>', unsafe_allow_html=True)with st.sidebar:# API Provider selection
api_provider = st.selectbox("API Provider",["OpenAI","OpenRouter"],help="Select which API to use. Make sure the corresponding API key is set in your .env file")# Model selection based on providerif api_provider =="OpenAI":
model = st.selectbox("AI Model",["gpt-4o","gpt-4-turbo","gpt-4","gpt-3.5-turbo",],help="OpenAI models. Requires OPENAI_API_KEY in .env file")else:# OpenRouter
model = st.selectbox("AI Model",["openai/gpt-5.2","openai/gpt-4-turbo","openai/gpt-4o","anthropic/claude-3-opus","anthropic/claude-3-sonnet",],help="OpenRouter models. Requires OPENROUTER_API_KEY in .env file")with st.expander("Advanced Settings"):
num_slides = st.slider("Number of Slides",4,10,6)
num_charts = st.slider("Number of Charts",3,6,4)
col1, col2 = st.columns([1,1])with col1:
st.markdown("### Input")
uploaded_file = st.file_uploader("Upload CSV",type=['csv'],help="Upload any CSV file. The app will automatically detect columns and create appropriate charts and slides.")
use_example = st.checkbox("Use example data (kpis.csv)", value=Trueifnot uploaded_file elseFalse)if uploaded_file or use_example:if use_example:
csv_path ="kpis.csv"
df = pd.read_csv(csv_path)else:
df = pd.read_csv(uploaded_file)with tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w')as f:
df.to_csv(f, index=False)
csv_path = f.name
with st.expander(" Preview Data", expanded=False):try:
st.dataframe(df, width='stretch')except Exception as e:
st.write("**Data Preview:**")
st.table(df.head(20))iflen(df)>20:
st.caption(f"Showing first 20 of {len(df)} rows")
st.caption(f"Rows: {len(df)} | Columns: {len(df.columns)}")else:
csv_path =None
st.info("Upload a CSV or use example data to continue")
goal = st.text_area("Goal",
value="Create a 6-slide investor update for Apr–Sep 2025. Focus on growth, retention, unit economics, and runway.",
height=100,help="Describe what you want the AI to create")
generate_btn = st.button("Generate Artifacts",type="primary", use_container_width=True)with col2:
st.markdown("### Output")
status_container = st.empty()
progress_bar = st.progress(0)ifnot generate_btn:
status_container.info("Configure settings and click 'Generate Artifacts'")if generate_btn:ifnot csv_path:
st.error("Please upload a CSV or select 'Use example data'")
st.stop()try:
status_container.info("Processing... This may take 30-60 seconds")
progress_bar.progress(10)
output_dir = Path("out")
output_dir.mkdir(exist_ok=True)
progress_bar.progress(20)with st.spinner("Generating artifacts..."):
outputs: RunOutputs = run_pipeline(
csv_path=csv_path,
goal=goal,
outdir=str(output_dir),
model=model,
num_slides=num_slides,
num_charts=num_charts
)
st.session_state.outputs = outputs
st.session_state.generated =True
progress_bar.progress(100)except Exception as e:
status_container.markdown(f'<div class="error-box"> <b>Error:</b> {str(e)}</div>',
unsafe_allow_html=True)
st.exception(e)
st.session_state.generated =Falseif st.session_state.generated and st.session_state.outputs:
outputs = st.session_state.outputs
try:if"Error"notin outputs.logs:
status_container.markdown('<div class="success-box"><b>Success!</b> Artifacts generated successfully</div>',
unsafe_allow_html=True)
tab1, tab2, tab3, tab4 = st.tabs(["PowerPoint","Excel","JSON Plan","Logs"])with tab1:
st.markdown("### PowerPoint Slides")
pptx_path = Path(outputs.pptx_path)if pptx_path.exists():try:
prs = Presentation(str(pptx_path))
slide_count =len(prs.slides)except:
slide_count ="unknown"withopen(pptx_path,"rb")as f:
st.download_button(
label="Download PowerPoint",
data=f.read(),
file_name="investor_update.pptx",
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation",
width='stretch',
key="pptx_download")
st.markdown("---")
col_info1, col_info2 = st.columns(2)with col_info1:
st.metric("Slides", slide_count)with col_info2:
file_size = pptx_path.stat().st_size /1024# KB
st.metric("File Size",f"{file_size:.1f} KB")else:
st.error("PowerPoint file not found")with tab2:
st.markdown("### Excel Workbook")
xlsx_path = Path(outputs.xlsx_path)if xlsx_path.exists():withopen(xlsx_path,"rb")as f:
st.download_button(
label="Download Excel",
data=f.read(),
file_name="investor_update.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
width='stretch',
key="xlsx_download")
wb = openpyxl.load_workbook(xlsx_path)
st.markdown("#### Data Preview")
ws = wb["Model"]
data =[]
headers =[cell.value for cell in ws[1]]for row in ws.iter_rows(min_row=2, values_only=True):
data.append(row)
df_preview = pd.DataFrame(data, columns=headers)try:
st.dataframe(df_preview.head(10), width='stretch')except Exception:
st.table(df_preview.head(10))iflen(df_preview)>10:
st.caption(f"Showing first 10 of {len(df_preview)} rows")else:
st.error("Excel file not found")with tab3:
st.markdown("### Schema-Locked JSON Plan")
plan_path = Path(outputs.plan_path)if plan_path.exists():withopen(plan_path,"r")as f:
plan_data = json.load(f)
st.download_button(
label="Download JSON",
data=json.dumps(plan_data, indent=2),
file_name="plan.json",
mime="application/json",
key="json_download")
st.markdown("---")
st.json(plan_data)
col_a, col_b = st.columns(2)with col_a:
st.metric("Charts Generated",len(plan_data.get("charts",[])))with col_b:
st.metric("Slides Generated",len(plan_data.get("slides",[])))else:
st.error("JSON plan not found")with tab4:
st.markdown("### Generation Logs")
st.code(outputs.logs, language="text")else:
status_container.markdown(f'<div class="error-box"> <b>Error occurred</b><br>{outputs.logs}</div>',
unsafe_allow_html=True)except Exception as e:
st.error(f"Error displaying results: {str(e)}")
st.exception(e)except Exception as e:
st.error(f"Fatal Error: {str(e)}")
st.exception(e)
st.info("Try installing dependencies: pip install -r requirements.txt")if __name__ =="__main__":try:
main()except Exception as e:import sys
print(f"Fatal error: {e}",file=sys.stderr)import traceback
traceback.print_exc()Obsługiwane przez
W warstwie Streamlit ustawiamy ramę aplikacji przy użyciu st.set_page_config( layout="wide"), a następnie dzielimy ekran przez st.columns(), tak aby lewa strona zbierała dane wejściowe, a prawa pokazywała sterowanie uruchomieniem i wyniki. Pipeline uruchamia się dopiero po kliknięciu st.button(), gdzie najpierw zabezpieczamy się przed brakującymi plikami, owijamy st.spinner(), aby UI nie wyglądał na zawieszony, a następnie dostarczamy użytkownikowi artefakty przy pomocy st.download_button() dla Excela, PowerPointa i pliku planu.
Po zakończeniu tego kroku zapisujemy całość jako app.py i uruchamiamy pełne doświadczenie poleceniem:
W tym samouczku zbudowaliśmy pełny pipeline „CSV do artefaktów”, który wykorzystuje GPT-5.2 do zwrócenia planu obejmującego wykresy, strukturę slajdów i cytowania zakresów komórek. Następnie walidujemy ten plan, zanim cokolwiek wygenerujemy. Takie podejście „najpierw plan, potem render” dobrze odpowiada mocnym stronom GPT-5.2 w wieloetapowych przepływach pracy, takich jak arkusze i prezentacje.
Wynikiem jest powtarzalny przepływ: użytkownik przesyła CSV, kod buduje czystą bazę skoroszytu, GPT-5.2 tworzy plan, który może cytować wyłącznie dozwolone zakresy Excela, a renderer materializuje wykresy i slajdy przy użyciu openpyxl i python-pptx.
Od tego miejsca można rozszerzać demo bez zmiany logiki rdzeniowej — dodając więcej typów wykresów, wprowadzając szablony slajdów, wspierając łączenie wielu CSV itd. Kluczowy wzorzec pozostaje ten sam: traktować model jako planera, który musi przestrzegać schematu, a całą logikę renderowania trzymać w kodzie, aby wyniki były spójne i możliwe do debugowania.
FAQs
Dlaczego łączyć GPT-5.2 ze Streamlit?⌄
Streamlit ułatwia przekształcenie przepływu pracy AI w interaktywną aplikację bez tworzenia pełnego frontendu. W połączeniu z GPT-5.2 pozwala deweloperom szybko przejść od eksperymentów do użytecznego narzędzia, które przyjmuje dane wejściowe, uruchamia proces sterowany modelem i zwraca namacalne wyniki.
Czym są Structured Outputs i dlaczego są tu ważne?⌄
Structured Outputs wymuszają na modelu zwrócenie danych dokładnie zgodnych z predefiniowanym JSON Schema. Dzięki temu wykresy, slajdy, metryki i cytowania podążają za ścisłymi zasadami, co pozwala zweryfikować plan, zanim zostaną utworzone jakiekolwiek pliki Excel lub PowerPoint.
Czy potrzebuję głębokiego doświadczenia frontendowego, aby tworzyć takie aplikacje?⌄
Nie. Streamlit zaprojektowano tak, aby większość pracy odbywała się w Pythonie, z wykorzystaniem znanego przepływu sterowania i bibliotek. Obniża to próg wejścia dla data scientistów i analityków, którzy chcą wdrażać narzędzia zasilane AI bez nauki JavaScriptu lub złożonych frameworków webowych.
GPT-5.2 to mocny model do generowania przepływów pracy i wieloetapowych pipeline’ów, które wymagają niezawodności, struktury i mniejszej liczby halucynacji. OpenAI podkreśla ulepszenia w pracy na długich kontekstach oraz jeszcze lepszą wydajność w zadaniach w stylu arkuszy kalkulacyjnych.
W tym samouczku zbudujemy aplikację Streamlit, która zachowuje się jak młodszy analityk:
Kluczowe jest to, że GPT-5.2 nie „tworzy slajdów” bezpośrednio. Generuje ścisły plan, który Pana/Pani kod wykonuje w sposób deterministyczny.
Proszę także zapoznać się z naszymi przewodnikami po nowszych modelach OpenAI: GPT-5.5, GPT-5.4, GPT-5.3 Codex oraz GPT-5.3 Instant
Czym jest GPT-5.2?
GPT-5.2 to naj nowszy model z serii GPT-5 z ulepszeniami dla zadań end-to-end, takich jak tworzenie arkuszy kalkulacyjnych, budowanie prezentacji, pisanie i debugowanie kodu, rozumienie długich kontekstów oraz korzystanie z narzędzi.
Oto właściwości GPT-5.2, które są najistotniejsze dla tego projektu:
Przykładowy projekt GPT-5.2: Zbuduj generator ustrukturyzowanych artefaktów
W tej części zbudujemy generator ustrukturyzowanych artefaktów (Excel i PowerPoint) korzystający z modelu GPT 5.2 opakowanego w aplikację Streamlit. W skrócie, finalna aplikacja Streamlit robi to:
Czyta CSV i buduje kanoniczny skoroszyt
Następnie wywołuje GPT-5.2 przez OpenAI lub OpenRouter z ustrukturyzowanymi wynikami (
response_format: json_schema)Weryfikuje zwrócony plan względem naszego schematu
Renderuje wykresy w Excelu i styluje je zgodnie z paletą marki
Na koniec generuje PowerPoint z osadzonymi wykresami i cytowaniami
Zbudujmy to krok po kroku.
Krok 1: Wymagania wstępne i konfiguracja
Zanim zaczniemy generować skoroszyty Excel i prezentacje PowerPoint z GPT-5.2, musimy przygotować środowisko lokalne. Zaczniemy od instalacji podstawowych bibliotek i ustawienia kluczy API jako zmiennych środowiskowych.
Użyjemy Streamlit do zbudowania interfejsu webowego, Pandas do wczytywania i przygotowania CSV, Pydantic do walidacji planu JSON z blokadą schematu od GPT-5.2. Skorzystamy też z python-dotenv do bezpiecznego wczytywania kluczy, takich jak
OPENROUTER_API_KEY/OPENAI_API_KEY, z pliku.env. Biblioteka requests wywołuje endpoint zgodny z OpenRouter/OpenAI, natomiast openpyxl generuje skoroszyt Excel i wykresy, a python-pptx tworzy prezentację PowerPoint z natywnymi (edytowalnymi) wykresami.Następnie ustawiamy klucz(e) API:
Proszę zauważyć, że do GPT 5.2 można uzyskać dostęp przez wiele usług, w tym oficjalne API OpenAI oraz usługi takie jak OpenRouter i inne.
Zalecane jest użycie pliku
.envdo lokalnego przechowywania kluczy API:W pliku
.envproszę zapisać następujące wpisy:Następnie proszę wczytać te klucze API za pomocą biblioteki load_dotenv:
Na tym etapie środowisko jest gotowe do uwierzytelnienia w API OpenAI.
Krok 2: Definiowanie schematu planu
Następnie definiujemy plan artefaktów z blokadą schematu, czyli ścisły szablon JSON, który mówi GPT-5.2 dokładnie, jakie wykresy utworzyć i jak ustrukturyzować slajdy, aby wynik modelu można było zdekodować i zweryfikować przy użyciu Structured Outputs poprzez
response_format: { type: "json_schema", strict: true }, zamiast swobodnego tekstu.Powyższy fragment kodu definiuje plan z użyciem modeli Pydantic, które robią trzy ważne rzeczy:
Struktura planu:
ChartKind = Literal["line", "bar"]ogranicza typy wykresów do małej listy,ChartSpecopisuje pełny kontrakt wykresu, aSlideSpecdefiniuje układ slajdu, opcjonalnie łącząc slajd z wykresem przezchart_id.Automatyczne egzekwowanie jakości: Ograniczenia jak
Field()gwarantują, że zawsze otrzymamy wystarczającą liczbę wykresów/slajdów, każdy slajd ma odpowiednią liczbę punktów, a każdy punkt zawiera cytowania.Planowanie i renderowanie: GPT-5.2 tworzy wyłącznie plan, tzn. wykresy, slajdy i cytowania jako ustrukturyzowany JSON, a reszta kodu odpowiada za wykonanie, formatowanie Excela, renderowanie wykresów i układ decku.
Dzięki temu schematowi GPT-5.2 staje się planerem, który dostarcza zweryfikowany plan JSON.
Krok 3: Wywołanie GPT-5.2 z ustrukturyzowanymi wynikami
Mając zdefiniowany schemat planu, kolejnym krokiem jest wywołanie GPT-5.2 w „trybie schematu” — tak, aby zwrócił szablon JSON, który można zweryfikować i wykonać. Zamiast liczyć, że model się „zachowa”, mówimy API, aby dekodował bezpośrednio do Pana/Pani JSON Schema przy użyciu
response_format: { type: "json_schema", ... strict: true }, co jest dokładnie celem Structured Outputs.Funkcja
chat_json_schema()to jedyna brama zamieniająca prompt na zweryfikowany plan:Routing dostawcy (OpenRouter vs OpenAI): Jeśli nazwa modelu ma przestrzeń nazw (openai/gpt-5.2), domyślnie wybierasz OpenRouter, ponieważ to format, którego używa OpenRouter do routingu modeli. Jeśli jawnie ustawisz api_provider="openai", wyślesz ten sam payload Chat Completions do endpointu OpenAI. Można całkowicie pominąć OpenRouter.
Ustawianie endpointów: Dla OpenAI wywołujesz endpoint
https://api.openai.com/v1/chat/completionsze standardową autoryzacją Bearer. Dla OpenRouter wywołujeszhttps://openrouter.ai/api/v1/chat/completionsi opcjonalnie uwzględniasz nagłówki atrybucji, takie jak HTTP-Referer i X-Title, które OpenRouter zaleca jako metadane.Dekodowanie schematu przez
response_format: Kluczowy blok toresponse_format: { type: "json_schema", json_schema: { strict: true, schema: ... } }, który instruuje model, aby produkował wynik ograniczony do naszego JSON Schema.Następnie zweryfikujemy plan JSON modelu przy pomocy Pydantic i tylko jeśli przejdzie kontrolę ograniczeń, przystąpimy do renderowania wykresów w Excelu i generowania prezentacji PowerPoint.
Krok 4: Funkcje pomocnicze
Zanim cokolwiek wyrenderujemy, potrzebujemy niewielkiej warstwy, która sprawi, że każdy przesłany CSV będzie bezpieczny do analizy i spójny do odwołań. Te funkcje pomocnicze wykonują cztery zadania: wczytują i czyszczą CSV, tworzą tabelę modelową z metrykami pochodnymi, wyliczają zwięzły zestaw faktów do promptu i sprawiają, że wynik w Excelu jest czytelny dzięki automatycznemu dostosowaniu szerokości kolumn.
Krok 4.1: Wczytanie i walidacja CSV
GPT 5.2 nie zaplanuje wiarygodnie wykresów i slajdów, jeśli tabela wejściowa jest pusta albo pola liczbowe są przechowywane jako ciągi znaków. Ta funkcja standaryzuje zbiór danych, aby dalsze kroki nie musiały zgadywać.
Powyższa funkcja:
pd.read_csv.pd.to_numeric, a brakujące wartości liczbowe wypełnia zerem, by uniknąć propagacji NaN.Po tym kroku ma Pan/i DataFrame, który nie jest pusty i zawiera kolumny liczbowe.
Krok 4.2: Obliczenie DataFrame modelu
Surowe CSV bywają niechlujne i niespójne między domenami. Arkusz modelu działa jako warstwa normalizacji, w której dodajemy metryki pochodne przydatne do wykresów, bez wymagania od użytkownika ręcznych obliczeń.
Funkcja
compute_model_df()robi dwie ogólnie użyteczne rzeczy:Wyszukuje główną kolumnę wyniku i drugorzędną kolumnę wejściową i oblicza prosty iloraz, aby uchwycić, jaka część sygnału podstawowego pozostaje po uwzględnieniu sygnału drugorzędnego.
Dla kilku kolumn liczbowych oblicza
pct_change(), które domyślnie zwraca ułamek zmiany względem poprzedniego wiersza — to wygodna baza do uchwycenia ruchu w dowolnej metryce szeregów czasowych.Krok 4.3: Obliczenie faktów
Nawet przy Structured Outputs chcemy dostarczyć GPT-5.2 zwięzłe podsumowanie tego, co się zmieniło w analizowanym okresie. Te fakty działają też jako prompt, który model może wykorzystać w punktach na slajdach.
Najpierw wybieramy pierwszy i ostatni wiersz, aby zdefiniować okno okresu. Następnie wybieramy kolumnę (date, month, week, period) do czytelnego raportowania dla człowieka.
Dla niewielkiego zestawu kolumn liczbowych wyliczamy migawkę wartości początkowej, końcowej, średniej i całkowitej zmiany procentowej od początku do końca. Powstaje lekki słownik sygnałów, który pomaga modelowi pisać punkty na slajdach.
Krok 4.4: Automatyczne dopasowanie szerokości kolumn w Excelu
Domyślne szerokości kolumn Excela sprawiają, że generowane skoroszyty wyglądają surowo. Autosizing to prosty zabieg, który od razu poprawia użyteczność.
Funkcja
autosize_columns()skanuje każdą kolumnę, znajduje maksymalną długość ciągu i ustawiaws.column_dimensions[col_letter].width. Biblioteka OpenPyXL udostępniacolumn_dimensionsi jej właściwośćwidthdo kontrolowania szerokości kolumn w Excelu.Następnie podłączymy oczyszczoną tabelę modelu i fakty do promptu planowania GPT-5.2, zweryfikujemy zwrócony plan, a potem wyrenderujemy wykresy i slajdy.
Krok 5: Zbudowanie skoroszytu
Korzystając z oczyszczonej tabeli modelu i lekkiego zestawu faktów z poprzedniego kroku, generujemy teraz deterministyczny skoroszyt Excel w openpyxl.
Oto co robi ten budowniczy skoroszytu:
Tworzy szkielet skoroszytu: Wywołanie
Workbook()instancjuje nowy obiekt skoroszytu, awb.remove(wb.active)usuwa domyślny arkusz, abyśmy mieli pełną kontrolę nad kolejnością arkuszy. Nowe arkusze dodajemy jawnie:wb.create_sheet("Raw"),wb.create_sheet("Model")iwb.create_sheet("Charts").Zapisuje arkusz Raw: Zapisujemy wiersz nagłówka przy użyciu Font, PatternFill i Alignment, a następnie dodajemy każdy wiersz z raw_df. Funkcja
.autosize_columns(ws_raw)poprawia czytelność bez ręcznego zmieniania szerokości.Wyznacza granice: Używamy
row_end = 1 + len(raw_df), co daje stabilny ostatni wiersz, aby bezpiecznie generować zakresy, jakA2:A{row_end}dla kategorii i cytowań wykresu.Stosuje heurystyki formatowania liczb: Następnie ustawiamy
cell.number_formatna podstawie słów-kluczy w nazwach kolumn i dtype, aby Excel wyświetlał wartości jako waluty, procenty lub zwykłe liczby.Tworzy arkusz Charts: Wywołanie
wb.create_sheet("Charts") daje nam stabilne płótno dla zakotwiczeń wykresów. To odpowiada przepływowi pracy openpyxl dla wykresów: tworzymy LineChart lub BarChart, wiążemy zakresy przezReference, ustawiamy kategorie przezset_categories, a następnie umieszczamy wykres metodąws.add_chart().Po tym kroku ma Pan/i spójny plik Excel, do którego kolejny etap może bezpiecznie odwoływać się przez dokładne zakresy komórek.
Krok 6: Renderowanie wykresów
Skoro struktura skoroszytu jest ustalona, możemy wyrenderować wykresy w arkuszu Charts i zastosować spójny wygląd marki. Zastosowałem paletę kolorów DataCamp:
Funkcja
add_charts_to_workbook()to silnik renderowania wykresów zamieniający nasz plan schematu w markowe wizualizacje w Excelu.Wczytanie skoroszytu: Zaczynamy od otwarcia wygenerowanego pliku Excela za pomocą
load_workbook(xlsx_path). Następnie budujemyheader_mapz pierwszego wiersza, mapując każdą nazwę kolumny na jej indeks kolumny w Excelu.Budowa referencji zakresów komórek: Funkcja
Reference()definiuje dokładny zakres do wykreślenia, achart.set_categories(x_ref)podłącza kategorie osi X. Standardowy wzorzec to tworzenie obiektów referencji, dodawanie danych, a potem ustawianie kategorii.Inicjalizacja typu wykresu: Dla każdego specyfikatora wykresu tworzymy
LineChart()lubBarChart()i ustawiamychart.title.Dodanie palety marki: Dla każdej serii w specyfikacji wiążemy zakres Y za pomocą
chart.add_data(), a następnie ustawiamy etykietę wyświetlania przy użyciuSeriesLabel().Na końcu funkcja
ws_charts.add_chart()umieszcza każdy wykres w określonej komórce lewego górnego rogu.Krok 7: Renderowanie prezentacji PowerPoint
W tym kroku plan staje się rzeczywistą prezentacją. Tworzymy PowerPoint, układamy tytuły i punkty, osadzamy wykresy zbudowane z tej samej tabeli co Excel i dodajemy stopkę zachowującą cytowania zakresów komórek.
Przyjrzyjmy się powyższemu kodowi krok po kroku:
Tworzenie decku i indeksowanie wykresów:
prs = Presentation()rozpoczyna nowy plik PPTX, ablank = prs.slide_layouts[6]daje pusty szablon do precyzyjnego rozmieszczenia.Tytuły i punkty: Funkcja
add_title()zapisuje tekst dotext_framew polu tekstowym, aadd_bullets()dodaje po jednym akapicie na każdy punkt, kontrolując rozmiar czcionki i odstępy przez formatowanie akapitów.Stopka z cytowaniami: Funkcja
add_sources_footer()usuwa duplikaty cytowań z punktów i renderuje zwartą linięSources:na dole, by zachować możliwość śledzenia bez zaśmiecania slajdu.Osadzanie wykresów:
add_chart()buduje obiektCategoryChartData()używając kolumny czasu jako osi X i zaplanowanych serii, po czym wstawia go poprzezslide.shapes.add_chart(), które zwracaGraphicFrame.Styl marki: Na końcu konwertujemy hex na
RGBColori stosujemy go dla każdej serii.Zasady układu i zabezpieczenia: Jeśli slajd ma poprawny
chart_id, używamy układu dwukolumnowego, w przeciwnym razie — pełnej szerokości na punkty. Funkcjabuild_allowed_ranges()tworzy listę dozwolonych zakresów do cytowania, avalidate_plan()egzekwuje unikalne ID wykresów, poprawne referencje, dozwolone cytowania i poprawne metryki, aby model nie mógł wymyślać pól.Krok 8: Wygenerowanie planu przez LLM
Na tym etapie mamy czyste dane tabelaryczne, które przekształcimy w plan budowy schematu (wykresy, slajdy i cytowania), możliwy do wykonania przez renderer.
Funkcja
generate_plan_with_llm()to mózg tego dema. Tak wpinamy ją w pipeline:Jawne JSON Schema:
plan_schemadefiniuje jedyną dozwoloną postać odpowiedzi,charts[]islides[], z polami wymaganymi i enumeracjami.Szyny ochronne w promptach systemowych: Ustalamy niepodlegające negocjacji zasady, aby plan pozostał renderowalny i audytowalny. Ograniczamy też pozycjonowanie wykresów do zwykłych komórek, aby układ pozostawał deterministyczny, a nie improwizowany przez model. Taki wieloetapowy nadzór to dokładnie to, do czego zaprojektowano GPT-5.2.
Wiadomość użytkownika: Zawiera cel, żądane liczby, obliczone fakty i dozwolone klucze metryk. Ponieważ model nie widzi niczego innego, ma znacznie mniej sposobów na wymyślanie kolumn, zakresów czy niepopartych twierdzeń.
Dekodowanie zgodne ze schematem: Wywołujemy
chat_json_schema()zresponse_formatustawionym najson_schemaistrict: true, co wymusza zgodność ze schematem podczas dekodowania.Walidacja w dwóch warstwach: Najpierw
ArtifactPlan.model_validate(plan_json)sprawdza zgodność odpowiedzi z naszymi modelami Pydantic. Następnievalidate_plan()egzekwuje ograniczenia domenowe.Krok 9: Uruchomienie pipeline’u
Na końcu łączymy w całość wczytywanie CSV, generowanie Excela, planowanie przez LLM, renderowanie wykresów i finalny eksport do PPTX, a następnie zwracamy ścieżki wynikowe.
Połączmy wszystkie komponenty naszego pipeline’u krok po kroku:
@dataclasstworzy lekki kontener dlaxlsx_path,pptx_path,plan_pathilogs, aby interfejs Streamlit mógł traktować wynik pipeline’u jako jedną ustrukturyzowaną wartość zamiast żonglować krotkami czy globalnymi zmiennymi.load_dotenv()wczytuje nasz plik.envdo zmiennych środowiskowych, więcOPENAI_API_KEYiOPENROUTER_API_KEYmożna odczytać przezos.getenv(). Następnie wybieramy klucz dostawcy prostą regułą: jeśli istniejeOPENROUTER_API_KEY, używamy OpenRouter; w przeciwnym razie, jeśli dostępny, korzystamy z OpenAI.build_workbook_base(). Funkcjagenerate_plan_with_llm()zwracaArtifactPlan, który zapisujemy do JSON przy użyciujson.dump(), aby ułatwić inspekcję i debugowanie.build_pptx()buduje deck slajdów z planu i tabeli.Po tym kroku nasza aplikacja Streamlit może wywołać
run_pipeline(), wyświetlićlogsi udostępnić trzy deterministyczne wyniki do pobrania: skoroszyt Excel, deck PPTX oraz plan JSON.Krok 10: Interfejs Streamlit
Skoro rdzeń pipeline’u jest gotowy, ostatnim krokiem jest opakowanie go w aplikację Streamlit, aby każdy mógł przesłać plik, ustawić cel, wybrać model i pobrać wygenerowane artefakty.
W warstwie Streamlit ustawiamy ramę aplikacji przy użyciu
st.set_page_config( layout="wide"), a następnie dzielimy ekran przezst.columns(), tak aby lewa strona zbierała dane wejściowe, a prawa pokazywała sterowanie uruchomieniem i wyniki. Pipeline uruchamia się dopiero po kliknięciust.button(), gdzie najpierw zabezpieczamy się przed brakującymi plikami, owijamyst.spinner(), aby UI nie wyglądał na zawieszony, a następnie dostarczamy użytkownikowi artefakty przy pomocyst.download_button()dla Excela, PowerPointa i pliku planu.Po zakończeniu tego kroku zapisujemy całość jako
app.pyi uruchamiamy pełne doświadczenie poleceniem: