Structured output
Agents can combine tools to generate a structured output. Structured outputs are especially helpful when downstream systems expect typed data instead of free-form prose. Common production use cases include:
- Ticket routing: Map user intents into
{ category, severity, owner }
so incidents flow to the right queue automatically. - Data extraction: Pull
{ company_name, amount, invoice_date }
from support chats and push to billing systems. - Workflow kicks-offs: Produce
{ action, arguments }
payloads that can be directly executed by automation platforms (for example, Zapier or internal job runners).
The structured output will come as a TextPart
and require parsing into an object.
Example
Section titled “Example”In the example below a “travel agent” orchestrates two tools—search_flights
and search_hotels
—and requires the language model to emit the final answer as JSON conforming to a travel_plan
schema. The agent gathers flight and hotel options, merges them into the structured payload, and prints the parsed object so downstream systems can consume it without extra parsing heuristics.
import { Agent, getResponseText, tool } from "@hoangvvo/llm-agent";import type { ResponseFormatOption } from "@hoangvvo/llm-sdk";import { getModel } from "./get-model.ts";
// Define the model to use for the Agentconst model = getModel("openai", "gpt-4o");
const searchFlightsTool = tool({ name: "search_flights", description: "Search for flights between two cities", parameters: { type: "object", properties: { from: { type: "string", description: "Origin city/airport" }, to: { type: "string", description: "Destination city/airport" }, date: { type: "string", description: "Departure date in YYYY-MM-DD" }, }, required: ["from", "to", "date"], additionalProperties: false, }, execute(args: { from: string; to: string; date: string }) { const { from, to, date } = args; console.log(`Searching flights from ${from} to ${to} on ${date}`); return { content: [ { type: "text", text: JSON.stringify([ { airline: "Vietnam Airlines", departure: `${date}T10:00:00`, arrival: `${date}T12:00:00`, price: 150, }, { airline: "Southwest Airlines", departure: `${date}T11:00:00`, arrival: `${date}T13:00:00`, price: 120, }, ]), }, ], is_error: false, }; },});
const searchHotelsTool = tool({ name: "search_hotels", description: "Search for hotels in a city", parameters: { type: "object", properties: { city: { type: "string" }, checkin: { type: "string", description: "Check-in date in YYYY-MM-DD", }, nights: { type: "number", description: "Number of nights" }, }, required: ["city", "checkin", "nights"], additionalProperties: false, }, execute(args: { city: string; checkin: string; nights: number }) { const { city, checkin, nights } = args; console.log( `Searching hotels in ${city} from ${checkin} for ${String(nights)} nights`, ); return { content: [ { type: "text", text: JSON.stringify([ { name: "The Plaza", location: city, pricePerNight: 150, rating: 4.8, }, { name: "Hotel Ritz", location: city, pricePerNight: 200, rating: 4.6, }, ]), }, ], is_error: false, }; },});
// Define the response formatconst responseFormat: ResponseFormatOption = { type: "json", name: "travel_plan", description: "A structured travel plan including flights, hotels, and weather forecast.", schema: { type: "object", properties: { destination: { type: "string" }, flights: { type: "array", items: { type: "object", properties: { airline: { type: "string" }, departure: { type: "string" }, arrival: { type: "string" }, price: { type: "number" }, }, required: ["airline", "departure", "arrival", "price"], additionalProperties: false, }, }, hotels: { type: "array", items: { type: "object", properties: { name: { type: "string" }, location: { type: "string" }, pricePerNight: { type: "number" }, rating: { type: "number" }, }, required: ["name", "location", "pricePerNight", "rating"], additionalProperties: false, }, }, }, required: ["destination", "flights", "hotels"], additionalProperties: false, },};
const travelAgent = new Agent({ name: "Bob", instructions: [ "You are Bob, a travel agent that helps users plan their trips.", () => `The current time is ${new Date().toISOString()}`, ], model, response_format: responseFormat, tools: [searchFlightsTool, searchHotelsTool],});
const prompt = "Plan a trip from Paris to Tokyo next week";
const response = await travelAgent.run({ input: [ { type: "message", role: "user", content: [{ type: "text", text: prompt }], }, ], context: {},});
console.dir(JSON.parse(getResponseText(response)));
use async_trait::async_trait;use dotenvy::dotenv;use llm_agent::{Agent, AgentItem, AgentRequest, AgentTool, AgentToolResult, RunState};use llm_sdk::{ openai::{OpenAIModel, OpenAIModelOptions}, JSONSchema, Message, Part, ResponseFormatJson, ResponseFormatOption,};use schemars::JsonSchema;use serde::Deserialize;use serde_json::{json, Value};use std::{env, error::Error, sync::Arc};
#[derive(Deserialize, JsonSchema)]#[serde(deny_unknown_fields)]struct SearchFlightsParams { #[schemars(description = "Origin city/airport")] from: String, #[schemars(description = "Destination city/airport")] to: String, #[schemars(description = "Departure date in YYYY-MM-DD")] date: String,}
struct SearchFlightsTool;
#[async_trait]impl AgentTool<()> for SearchFlightsTool { fn name(&self) -> String { "search_flights".to_string() } fn description(&self) -> String { "Search for flights between two cities".to_string() } fn parameters(&self) -> JSONSchema { schemars::schema_for!(SearchFlightsParams).into() } async fn execute( &self, args: Value, _context: &(), _state: &RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>> { let params: SearchFlightsParams = serde_json::from_value(args)?; println!( "Searching flights from {} to {} on {}", params.from, params.to, params.date ); Ok(AgentToolResult { content: vec![Part::text( json!([ { "airline": "Vietnam Airlines", "departure": format!("{}T10:00:00", params.date), "arrival": format!("{}T12:00:00", params.date), "price": 150 }, { "airline": "Southwest Airlines", "departure": format!("{}T11:00:00", params.date), "arrival": format!("{}T13:00:00", params.date), "price": 120 } ]) .to_string(), )], is_error: false, }) }}
#[derive(Deserialize, JsonSchema)]#[serde(deny_unknown_fields)]struct SearchHotelsParams { #[schemars(description = "City to search hotels in")] city: String, #[schemars(description = "Check-in date in YYYY-MM-DD")] check_in: String, #[schemars(description = "Number of nights to stay")] nights: u32,}
struct SearchHotelsTool;
#[async_trait]impl AgentTool<()> for SearchHotelsTool { fn name(&self) -> String { "search_hotels".to_string() } fn description(&self) -> String { "Search for hotels in a specific location".to_string() } fn parameters(&self) -> JSONSchema { schemars::schema_for!(SearchHotelsParams).into() } async fn execute( &self, args: Value, _context: &(), _state: &RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>> { let params: SearchHotelsParams = serde_json::from_value(args)?; println!( "Searching hotels in {} from {} for {} nights", params.city, params.check_in, params.nights ); Ok(AgentToolResult { content: vec![Part::text( json!([ { "name": "The Plaza", "location": params.city.to_string(), "pricePerNight": 150, "rating": 4.8 }, { "name": "Hotel Ritz", "location": params.city.to_string(), "pricePerNight": 200, "rating": 4.7 } ]) .to_string(), )], is_error: false, }) }}
#[allow(clippy::too_many_lines)]#[tokio::main]async fn main() -> Result<(), Box<dyn Error>> { dotenv().ok();
let model = Arc::new(OpenAIModel::new( "gpt-4o", OpenAIModelOptions { api_key: env::var("OPENAI_API_KEY") .expect("OPENAI_API_KEY environment variable must be set"), ..Default::default() }, ));
// Define the response format let response_format = ResponseFormatOption::Json(ResponseFormatJson { name: "travel_plan".to_string(), description: Some( "A structured travel plan including flights, hotels, and weather forecast.".to_string(), ), schema: Some(json!({ "type": "object", "properties": { "destination": { "type": "string" }, "flights": { "type": "array", "items": { "type": "object", "properties": { "airline": { "type": "string" }, "departure": { "type": "string" }, "arrival": { "type": "string" }, "price": { "type": "number" } }, "required": [ "airline", "departure", "arrival", "price" ], "additionalProperties": false } }, "hotels": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "location": { "type": "string" }, "pricePerNight": { "type": "number" }, "rating": { "type": "number" } }, "required": [ "name", "location", "pricePerNight", "rating" ], "additionalProperties": false } } }, "required": [ "destination", "flights", "hotels", ], "additionalProperties": false })), });
let travel_agent = Agent::<()>::builder("Bob", model) .add_instruction("You are Bob, a travel agent that helps users plan their trips.") .add_instruction(|_ctx: &()| Ok(format!("The current time is {}", chrono::Local::now()))) .response_format(response_format) .add_tool(SearchFlightsTool) .add_tool(SearchHotelsTool) .build();
let prompt = "Plan a trip from Paris to Tokyo next week";
let response = travel_agent .run(AgentRequest { input: vec![AgentItem::Message(Message::user(vec![Part::text(prompt)]))], context: (), }) .await?;
let val: Value = serde_json::from_str(&response.text()).expect("Invalid JSON response");
println!( "{}", serde_json::to_string_pretty(&val).expect("Failed to format JSON") );
Ok(())}
package main
import ( "context" "encoding/json" "fmt" "log" "os" "time"
llmagent "github.com/hoangvvo/llm-sdk/agent-go" llmsdk "github.com/hoangvvo/llm-sdk/sdk-go" "github.com/hoangvvo/llm-sdk/sdk-go/openai" "github.com/joho/godotenv")
type SearchFlightsParams struct { From string `json:"from"` To string `json:"to"` Date string `json:"date"`}
type SearchFlightsTool struct{}
func (t *SearchFlightsTool) Name() string { return "search_flights"}
func (t *SearchFlightsTool) Description() string { return "Search for flights between two cities"}
func (t *SearchFlightsTool) Parameters() llmsdk.JSONSchema { return llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "from": map[string]any{ "type": "string", "description": "Origin city/airport", }, "to": map[string]any{ "type": "string", "description": "Destination city/airport", }, "date": map[string]any{ "type": "string", "description": "Departure date in YYYY-MM-DD", }, }, "required": []string{"from", "to", "date"}, "additionalProperties": false, }}
func (t *SearchFlightsTool) Execute(ctx context.Context, paramsJSON json.RawMessage, contextVal struct{}, runState *llmagent.RunState) (llmagent.AgentToolResult, error) { var params SearchFlightsParams if err := json.Unmarshal(paramsJSON, ¶ms); err != nil { return llmagent.AgentToolResult{}, err }
fmt.Printf("Searching flights from %s to %s on %s\n", params.From, params.To, params.Date)
result := []map[string]any{ { "airline": "Vietnam Airlines", "departure": fmt.Sprintf("%sT10:00:00", params.Date), "arrival": fmt.Sprintf("%sT12:00:00", params.Date), "price": 150, }, { "airline": "Southwest Airlines", "departure": fmt.Sprintf("%sT11:00:00", params.Date), "arrival": fmt.Sprintf("%sT13:00:00", params.Date), "price": 120, }, }
resultJSON, err := json.Marshal(result) if err != nil { return llmagent.AgentToolResult{}, err }
return llmagent.AgentToolResult{ Content: []llmsdk.Part{ llmsdk.NewTextPart(string(resultJSON)), }, IsError: false, }, nil}
type SearchHotelsParams struct { City string `json:"city"` CheckIn string `json:"check_in"` Nights int `json:"nights"`}
type SearchHotelsTool struct{}
func (t *SearchHotelsTool) Name() string { return "search_hotels"}
func (t *SearchHotelsTool) Description() string { return "Search for hotels in a specific location"}
func (t *SearchHotelsTool) Parameters() llmsdk.JSONSchema { return llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "city": map[string]any{ "type": "string", "description": "City to search hotels in", }, "check_in": map[string]any{ "type": "string", "description": "Check-in date in YYYY-MM-DD", }, "nights": map[string]any{ "type": "integer", "description": "Number of nights to stay", }, }, "required": []string{"city", "check_in", "nights"}, "additionalProperties": false, }}
func (t *SearchHotelsTool) Execute(ctx context.Context, paramsJSON json.RawMessage, contextVal struct{}, runState *llmagent.RunState) (llmagent.AgentToolResult, error) { var params SearchHotelsParams if err := json.Unmarshal(paramsJSON, ¶ms); err != nil { return llmagent.AgentToolResult{}, err }
fmt.Printf("Searching hotels in %s from %s for %d nights\n", params.City, params.CheckIn, params.Nights)
result := []map[string]any{ { "name": "The Plaza", "location": params.City, "pricePerNight": 150, "rating": 4.8, }, { "name": "Hotel Ritz", "location": params.City, "pricePerNight": 200, "rating": 4.7, }, }
resultJSON, err := json.Marshal(result) if err != nil { return llmagent.AgentToolResult{}, err }
return llmagent.AgentToolResult{ Content: []llmsdk.Part{ llmsdk.NewTextPart(string(resultJSON)), }, IsError: false, }, nil}
func main() { godotenv.Load("../.env")
apiKey := os.Getenv("OPENAI_API_KEY") if apiKey == "" { log.Fatal("OPENAI_API_KEY environment variable must be set") }
model := openai.NewOpenAIModel("gpt-4o", openai.OpenAIModelOptions{ APIKey: apiKey, })
// Define the response format responseSchema := llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "destination": map[string]any{ "type": "string", }, "flights": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "airline": map[string]any{ "type": "string", }, "departure": map[string]any{ "type": "string", }, "arrival": map[string]any{ "type": "string", }, "price": map[string]any{ "type": "number", }, }, "required": []string{"airline", "departure", "arrival", "price"}, "additionalProperties": false, }, }, "hotels": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "name": map[string]any{ "type": "string", }, "location": map[string]any{ "type": "string", }, "pricePerNight": map[string]any{ "type": "number", }, "rating": map[string]any{ "type": "number", }, }, "required": []string{"name", "location", "pricePerNight", "rating"}, "additionalProperties": false, }, }, }, "required": []string{"destination", "flights", "hotels"}, "additionalProperties": false, }
description := "A structured travel plan including flights, hotels, and weather forecast." responseFormat := llmsdk.NewResponseFormatJSON("travel_plan", &description, &responseSchema)
staticInstruction := "You are Bob, a travel agent that helps users plan their trips." dynamicInstruction := func(ctx context.Context, ctxVal struct{}) (string, error) { return fmt.Sprintf("The current time is %s", time.Now().Format(time.RFC3339)), nil }
travelAgent := llmagent.NewAgent("Bob", model, llmagent.WithInstructions( llmagent.InstructionParam[struct{}]{String: &staticInstruction}, llmagent.InstructionParam[struct{}]{Func: dynamicInstruction}, ), llmagent.WithResponseFormat[struct{}](*responseFormat), llmagent.WithTools( &SearchFlightsTool{}, &SearchHotelsTool{}, ), )
prompt := "Plan a trip from Paris to Tokyo next week"
response, err := travelAgent.Run(context.Background(), llmagent.AgentRequest[struct{}]{ Input: []llmagent.AgentItem{ llmagent.NewAgentItemMessage( llmsdk.NewUserMessage( llmsdk.NewTextPart(prompt), ), ), }, Context: struct{}{}, })
if err != nil { log.Fatal(err) }
// Parse and pretty print the JSON response var val map[string]any if err := json.Unmarshal([]byte(response.Text()), &val); err != nil { log.Fatalf("Invalid JSON response: %v", err) }
prettyJSON, err := json.MarshalIndent(val, "", " ") if err != nil { log.Fatalf("Failed to format JSON: %v", err) }
fmt.Println(string(prettyJSON))}