Štruktúrovaný výstup
Štruktúrovaný výstup je technika, ktorá umožňuje získavať odpovede od jazykového modelu v predvídateľnom a strojovo spracovateľnom formáte, ako je napr. JSON, namiesto voľného textu. Tento prístup výrazne zvyšuje spoľahlivosť integrácie modelov do aplikácií, pretože vývojári môžu vopred definovať presnú štruktúru, typy údajov a obmedzenia pre odpoveď. Model je následne vedený k tomu, aby svoje informácie sformuloval podľa tejto špecifikácie, čo eliminuje potrebu riskantnej a chybovej analýzy neštruktúrovaného textu.
Podpora štruktúrovaných výstupov je kľúčová pre vytváranie robustných aplikácií, ako sú systémy na extrakciu informácií, generovanie API volaní alebo riešenie problémov krok za krokom, kde je konzistentný formát nevyhnutný pre ďalšie spracovanie.
Voľba response_format s parametrom "type": "json_schema" v knižnici OpenAI núti model vrátiť odpoveď v striktne definovanom JSON formáte podľa poskytnutej schémy. Tento prístup zaisťuje, že model vráti štruktúrované a predvídateľné dáta, ktoré je možné priamo spracovať v kóde bez potreby dodatočného parsovania alebo čistenia neštandardného textového výstupu.
Pozor, nie všetky modely podporujú štruktúrovaný výstup. V našom príklade používame model nvidia/nemotron-3-nano-30b-a3b:free, ktorý podporuje štruktúrovaný výstup. Takisto je možné použiť model mistralai/devstral-2512:free. Aktuálne sú oba modely funkčné, no ich budúca dostupnosť nie je garantovaná. V prípade výpadku alebo ukončenia podpory bude potrebné prejsť na alternatívny model s podporou štruktúrovaného výstupu
from openai import OpenAI
import os
import json
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.environ.get("OPENROUTER_API_KEY"),
)
text = """
Extract information about people mentioned in the following text. For each
person, provide their name, age, and city of residence in a structured JSON
format. John Doe is a software engineer living in New York. He is 30 years old
and enjoys hiking and photography. Jane Smith is a graphic designer based in San
Francisco. She is 28 years old and loves painting and traveling."""
response = client.chat.completions.create(
extra_body={},
model="nvidia/nemotron-3-nano-30b-a3b:free", # Model supporting structured outputs
messages=[
{
"role": "user",
"content": text
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "people_info",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": False
}
},
"strict": True
}
}
)
# Parse the JSON response
info = json.loads(response.choices[0].message.content)
print("Extracted info:", info)
V našom prvom príklade si ukážeme, ako požiadať model, aby vrátil údaje vo formáte JSON a ako tieto výstupy analyzovať v Pythone. Modelu poskytneme textový prompt, v ktorom ho požiadame, aby extrahoval informácie o osobách spomenutých v texte (meno, vek, mesto).
Použitá json_schema v našom príklade definuje štruktúru výstupu, ktorú model musí dodržať. Schéma špecifikuje, že odpoveď bude array (pole) objektov, pričom každý objekt musí obsahovať tri povinné vlastnosti: name typu string (reťazec), age typu integer (celé číslo) a city typu string (reťazec).
Nastavenie "required": ["name", "age", "city"] zaisťuje, že všetky tri vlastnosti musia byť v každom objekte prítomné, a "additionalProperties": False zakazuje pridávanie akýchkoľvek ďalších vlastností, čím sa získa čistý a presne definovaný formát. Parameter "strict": True núti model prísne dodržiavať schému a v prípade nemožnosti jej splnenia vráti chybu namiesto čiastočného alebo upraveného výstupu.
Použitie Pydantic na parsovanie štruktúrovaného výstupu
Pydantic je populárna knižnica v Pythone na overovanie dát a správanie nastavení prostredníctvom anotácií typu. Jej hlavnou úlohou je zabezpečiť, že prichádzajúce dáta (napríklad odpoveď od LLM) sú v správnom formáte a typoch, ako boli definované v modeli. To automaticky eliminuje chyby spôsobené nekonzistentnými dátami a poskytuje jasnú štruktúru pre prácu s výsledkami v kóde.
Nasledujúci príklad demonštruje použitie Pydantic na definovanie štruktúrovaného výstupu pre riešenie matematických rovníc krok za krokom. Ukazuje, ako definovať vnorené modely ( Step a MathResponse) a ich použitie na parsovanie odpovede modelu, zabezpečujúc tak typovú bezpečnosť a extrakciu štruktúrovaných údajov bez použitia response_format s JSON schémou.
from openai import OpenAI
from pydantic import BaseModel
from typing import List
import os
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.environ["OPENROUTER_API_KEY"],
)
class Step(BaseModel):
explanation: str
output: str
class MathResponse(BaseModel):
steps: List[Step]
final_answer: str
prompt = """
Solve the equation: 8x + 31 = 2.
Return your answer as a JSON object matching this format:
{
"steps": [
{"explanation": "...", "output": "..."},
...
],
"final_answer": "..."
}
"""
response = client.chat.completions.create(
model="nvidia/nemotron-3-nano-30b-a3b:free",
messages=[{"role": "user", "content": prompt}],
)
raw_text = response.choices[0].message.content
parsed = MathResponse.model_validate_json(raw_text)
print(parsed)
print(parsed.final_answer)
Po získaní odpovede od modelu ju parsujeme pomocou MathResponse.model_validate_json(), ktorý automaticky overí, že odpoveď zodpovedá definovanej štruktúre. Ak model vráti neplatný formát, Pydantic vyhodí výnimku, čím sa zabezpečí, že s dátami budeme pracovať len vtedy, keď sú správne štruktúrované.
Volanie nástrojov
Volanie nástrojov umožňuje modelu volať funkcie, ktoré definujete v našom kóde. Model sám negeneruje kód, ale vráti JSON objekt s názvom funkcie a argumentmi, ktoré by sa mali použiť. To umožňuje prepojiť LLM s externými nástrojmi a API.
Pôvodne sa skôr používal termín „volanie funkcií“ (function calling), dnes sa uprednostňuje širší pojem „volanie nástrojov“ (tool calling), pretože okrem funkcií môžu byť nástrojmi aj iné systémy, ako napríklad webové API.
V našom príklade model rozhodne, že má zavolať funkciu get_random_language, my ju vykonáme a výsledok pošleme späť modelu, aby mohol dokončiť úlohu. Táto schopnosť premieňa LLM z pasívneho generátora textu na aktívneho agenta, ktorý môže interagovať s inými systémami a vykonávať akcie.
import random
import os
import sys
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
tools = [
{
"type": "function",
"function": {
"name": "get_random_language",
"description": "Returns a random language to translate into",
"parameters": {"type": "object", "properties": {}, "required": []}
}
}
]
messages = [
{
"role": "user",
"content": f"Pick a random language and translate this sentence into it: 'Hello, how are you?'"
}
]
def get_random_language():
languages = ["Spanish", "Czech", "Hungarian", "French",
"German", "Italian", "Slovak", "Polish", "Russian"]
return random.choice(languages)
function_registry = {"get_random_language": get_random_language}
# First model call to trigger tool
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
tool_choice="auto"
)
tool_calls = response.choices[0].message.tool_calls
if not tool_calls:
print("No function called.")
sys.exit(1)
# Extract tool call and execute it
tool_call = tool_calls[0]
function_name = tool_call.function.name
tool_result = function_registry[function_name]()
# Feed tool call and result back to model
messages.append(response.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": f'"{tool_result}"'
})
# Final model call to complete the task
final_response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools
)
print("Language selected:", tool_result)
print("Final translation response:")
print(final_response.choices[0].message.content)
V kóde najprv definujeme schému funkcie v parametri tools. Táto schéma informuje model o dostupných nástrojoch, ich popisoch a očakávaných parametroch. Následne pošleme požiadavku modelu. Ak sa model rozhodne použiť funkciu, vráti odpoveď s tool_calls. My tento tool_calls objekt spracujeme, zavoláme príslušnú Python funkciu (v tomto prípade get_random_language) a výsledok pošleme späť modelu v ďalšom volaní.
V tomto druhom volaní má model k dispozícii výsledok funkcie a môže ho použiť na sformulovanie finálnej odpovede. Tento dvojkrokový proces je nevyhnutný, pretože model sám o sebe nemôže spúšťať kód, iba generovať požiadavky na jeho spustenie.
Zisťovanie teploty mesta
Nasledujúci príklad predstavuje komplexnejšiu aplikáciu, ktorá ukazuje silu kombinácie modelov s externými API a funkciami. Táto aplikácia demonštruje niekoľko kľúčových konceptov programovania s AI:
- Spracovanie prirodzeného jazyka (NLP) – model rozumie otázkam formulovaným v bežnej reči
- Volanie nástrojov – model aktívne interaguje s externými službami
- Integrácia API – kombinuje údaje z rôznych zdrojov (geokódovanie + počasie)
"""
Temperature CLI App using OpenAI Tools API (DeepSeek)
This app determines the temperature for any chosen city using Open-Meteo API
"""
import json
import requests
import os
import sys
import openai
# Configure OpenAI client for DeepSeek
deepseek_client = openai.OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1"
)
# Function to get coordinates for a city using Open-Meteo Geocoding API
def geocode_city(city_name):
"""Get latitude and longitude for a given city name."""
try:
url = "https://geocoding-api.open-meteo.com/v1/search"
params = {
"name": city_name,
"count": 1,
"language": "en",
"format": "json"
}
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
if "results" in data and len(data["results"]) > 0:
result = data["results"][0]
return {
"latitude": result["latitude"],
"longitude": result["longitude"],
"name": result.get("name", city_name),
"country": result.get("country", "")
}
else:
raise ValueError(f"City '{city_name}' not found")
except Exception as e:
raise Exception("Error getting temperature") from e
# Function to get weather from Open-Meteo API
def fetch_current_weather(latitude, longitude):
"""Get current weather for given coordinates."""
try:
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": latitude,
"longitude": longitude,
"current_weather": "true",
"temperature_unit": "celsius"
}
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
return {
"temperature": data["current_weather"]["temperature"],
"windspeed": data["current_weather"]["windspeed"],
"winddirection": data["current_weather"]["winddirection"],
"weathercode": data["current_weather"]["weathercode"],
"time": data["current_weather"]["time"]
}
except Exception as e:
raise Exception("Error getting temperature") from e
# Define tool schema for Tools API
WEATHER_TOOLS = [
{
"type": "function",
"function": {
"name": "fetch_city_weather",
"description": "Get the current weather for a specific city. Extract the city name from natural language queries about weather, temperature, or climate.",
"parameters": {
"type": "object",
"properties": {
"city_name": {
"type": "string",
"description": "The name of the city to get weather for. Extract this from natural language queries like 'What's the weather in Paris?' or 'How hot is it in Tokyo?'"
}
},
"required": ["city_name"]
}
}
}
]
# Combined function that uses both geocoding and weather APIs
def fetch_city_weather(city_name):
"""Get weather for a city using city name."""
try:
# Get coordinates
city_info = geocode_city(city_name)
# Get weather
weather_data = fetch_current_weather(city_info["latitude"], city_info["longitude"])
# Combine results
return {
"city": city_info["name"],
"country": city_info["country"],
"coordinates": {
"latitude": city_info["latitude"],
"longitude": city_info["longitude"]
},
"temperature": weather_data["temperature"],
"windspeed": weather_data["windspeed"],
"winddirection": weather_data["winddirection"],
"weathercode": weather_data["weathercode"],
"time": weather_data["time"]
}
except Exception as e:
raise Exception("Error getting city weather") from e
# Process natural language queries using Tools API
def resolve_weather_query(query):
"""Process natural language query using Tools API."""
try:
messages = [
{
"role": "system",
"content": (
"You are a weather assistant. When the user asks about weather or temperature, "
"identify the most likely city name and call the function fetch_city_weather "
"with parameter {\"city_name\": \"...\"}. Do not answer directly."
),
},
{"role": "user", "content": query},
]
response = deepseek_client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=WEATHER_TOOLS,
tool_choice={"type": "function", "function": {"name": "fetch_city_weather"}}
)
msg = response.choices[0].message
# Expect a tool call
if getattr(msg, "tool_calls", None):
for tc in msg.tool_calls:
if getattr(tc, "type", "") == "function" and getattr(tc, "function", None):
fn = tc.function
if fn.name == "fetch_city_weather":
args = json.loads(fn.arguments or "{}")
city_name = args.get("city_name")
if city_name:
return fetch_city_weather(city_name)
raise ValueError("No tool call produced or city name not found")
except Exception as e:
raise Exception("Error processing query") from e
# Weather code descriptions
WEATHER_CODE_DESCRIPTIONS = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail"
}
def describe_weather_code(code):
"""Get weather description from weather code."""
return WEATHER_CODE_DESCRIPTIONS.get(code, "Unknown")
def format_weather_report(data):
"""Format the weather data for display."""
weather_desc = describe_weather_code(data["weathercode"])
output = f"""
╔══════════════════════════════════════════════════════════════╗
║ WEATHER REPORT ║
╠══════════════════════════════════════════════════════════════╣
║ City: {data['city']}, {data['country']}
║ Coordinates: {data['coordinates']['latitude']:.2f}°N, {data['coordinates']['longitude']:.2f}°E
║ Time: {data['time']}
║ Temperature: {data['temperature']}°C
║ Weather: {weather_desc}
║ Wind: {data['windspeed']} km/h at {data['winddirection']}°
╚══════════════════════════════════════════════════════════════╝
"""
return output
def main():
print("Temperature CLI App with OpenAI DeepSeek (Tools API)")
print("=" * 50)
# Check if OpenAI API key is set
if not os.getenv("DEEPSEEK_API_KEY"):
print("Error: DEEPSEEK_API_KEY environment variable not set")
print("Please set your OpenAI API key: export DEEPSEEK_API_KEY='your-key-here'")
sys.exit(1)
# Get user input
if len(sys.argv) > 1:
query = " ".join(sys.argv[1:])
else:
query = input("Enter city name or weather query: ").strip()
if not query:
print("Error: No input provided")
sys.exit(1)
try:
print(f"\nProcessing query: '{query}'...")
# Process the query
result = resolve_weather_query(query)
# Display results
print(format_weather_report(result))
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Funkcia geocode_city komunikuje s Open‑Meteo Geocoding API, ktoré bezplatne poskytuje geografické súradnice pre akékoľvek mesto na svete. Táto funkcia je nevyhnutná, keďže väčšina meteorologických API pracuje so súradnicami (zemepisná šírka a dĺžka), nie s názvami miest. Geokódovanie prekladá zrozumiteľné názvy ako „Bratislava“ alebo „New York“ na presné súradnice ako 48.15°N, 17.11°E.
Funkcia fetch_current_weather využíva Open‑Meteo Weather API, ktoré poskytuje aktuálne údaje o počasí vrátane teploty, rýchlosti a smeru vetra a kódu počasia. API je bezplatné, nevyžaduje registráciu a vracia dáta v štruktúrovanom formáte current_weather, čo uľahčuje ich spracovanie.
Funkcia fetch_city_weather kombinuje oba predchádzajúce kroky do jedného celku. Najprv získa súradnice mesta pomocou geokódovania a následne na ich základe získa aktuálne počasie. Vracia objekt s kľúčovými informáciami: názov mesta, krajinu, súradnice, teplotu, vietor a kód počasia.
Zoznam tools definuje dostupné nástroje pre model. Každý nástroj má názov, popis a schému parametrov. V našom prípade má funkcia fetch_city_weather jeden parameter city_name, ktorý vie model automaticky extrahovať z prirodzenej reči. Táto schéma je kľúčová pre správne fungovanie Tools API a generovanie tool_calls.
Kľúčová funkcia resolve_weather_query používa moderné Tools API. V požiadavke registrujeme nástroje a pomocou tool_choice vynútime použitie funkcie fetch_city_weather. Aplikácia následne prečíta tool_calls, získa parameter city_name a vykoná volanie Python funkcie.
Ak model nevygeneruje tool_call alebo volanie zlyhá, aplikácia vypíše jednoduchú chybovú správu a skončí.
Funkcia format_weather_report premení surové údaje z API na prehľadnú textovú správu s ASCII rámčekom. Funkcia describe_weather_code používa mapu WEATHER_CODE_DESCRIPTIONS na preklad numerických kódov počasia na zrozumiteľné popisy.
python test.py "Ake je pocasie v Bratislave?" Temperature CLI App with OpenAI DeepSeek (Tools API) ================================================== Processing query: 'Ake je pocasie v Bratislave?'... ╔══════════════════════════════════════════════════════════════╗ ║ WEATHER REPORT ║ ╠══════════════════════════════════════════════════════════════╣ ║ City: Bratislava, Slovakia ║ Coordinates: 48.15°N, 17.11°E ║ Time: 2026-01-11T18:45 ║ Temperature: -4.0°C ║ Weather: Partly cloudy ║ Wind: 28.4 km/h at 303° ╚══════════════════════════════════════════════════════════════╝
DeepSeek rozumie aj Slovenčine.
Všetky príklady z článku a mnohé ďalšie sú dostupné na GitHub repozitári github.com/janbodnar/Python-AI-Skolenie.
