Skip to content

Commit 5e524ea

Browse files
authored
feat: structured output for responses API (tools) (#691)
1 parent 282ec24 commit 5e524ea

File tree

7 files changed

+77
-3
lines changed

7 files changed

+77
-3
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "../lib/openai"
5+
6+
class GetWeather < OpenAI::BaseModel
7+
required :location, String, doc: "City and country e.g. Bogotá, Colombia"
8+
end
9+
10+
# gets API Key from environment variable `OPENAI_API_KEY`
11+
client = OpenAI::Client.new
12+
13+
response = client.responses.create(
14+
model: "gpt-4o-2024-08-06",
15+
input: [
16+
{
17+
role: :user,
18+
content: "What's the weather like in Paris today?"
19+
}
20+
],
21+
tools: [GetWeather]
22+
)
23+
24+
response
25+
.output
26+
.each do |output|
27+
pp(output.parsed)
28+
end

lib/openai/models/responses/function_tool.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ class FunctionTool < OpenAI::Internal::Type::BaseModel
1414
# A JSON schema object describing the parameters of the function.
1515
#
1616
# @return [Hash{Symbol=>Object}, nil]
17-
required :parameters, OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown], nil?: true
17+
required :parameters,
18+
union: OpenAI::UnionOf[
19+
OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown],
20+
OpenAI::StructuredOutput::JsonSchemaConverter
21+
],
22+
nil?: true
1823

1924
# @!attribute strict
2025
# Whether to enforce strict parameter validation. Default `true`.

lib/openai/models/responses/response_create_params.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class ResponseCreateParams < OpenAI::Internal::Type::BaseModel
191191
# the model to call your own code. Learn more about
192192
# [function calling](https://platform.openai.com/docs/guides/function-calling).
193193
#
194-
# @return [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>, nil]
194+
# @return [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>, nil]
195195
optional :tools, -> { OpenAI::Internal::Type::ArrayOf[union: OpenAI::Responses::Tool] }
196196

197197
# @!attribute top_p
@@ -258,7 +258,7 @@ class ResponseCreateParams < OpenAI::Internal::Type::BaseModel
258258
#
259259
# @param tool_choice [Symbol, OpenAI::Models::Responses::ToolChoiceOptions, OpenAI::Models::Responses::ToolChoiceTypes, OpenAI::Models::Responses::ToolChoiceFunction] How the model should select which tool (or tools) to use when generating
260260
#
261-
# @param tools [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>] An array of tools the model may call while generating a response. You
261+
# @param tools [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>] An array of tools the model may call while generating a response. You
262262
#
263263
# @param top_p [Float, nil] An alternative to sampling with temperature, called nucleus sampling,
264264
#

lib/openai/models/responses/response_function_tool_call.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ class ResponseFunctionToolCall < OpenAI::Internal::Type::BaseModel
1010
# @return [String]
1111
required :arguments, String
1212

13+
# @!attribute parsed
14+
# The parsed contents of the arguments.
15+
#
16+
# @return [Object, nil]
17+
required :parsed, OpenAI::Internal::Type::Unknown
18+
1319
# @!attribute call_id
1420
# The unique ID of the function tool call generated by the model.
1521
#

lib/openai/models/responses/tool.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module Tool
1212
# Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling).
1313
variant :function, -> { OpenAI::Responses::FunctionTool }
1414

15+
variant -> { OpenAI::StructuredOutput::JsonSchemaConverter }
16+
1517
# A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search).
1618
variant :file_search, -> { OpenAI::Responses::FileSearchTool }
1719

lib/openai/resources/responses.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def create(params)
7676
end
7777

7878
model = nil
79+
tool_models = {}
7980
case parsed
8081
in {text: OpenAI::StructuredOutput::JsonSchemaConverter => model}
8182
parsed.update(
@@ -99,6 +100,27 @@ def create(params)
99100
)
100101
in {text: {format: {type: :json_schema, schema: OpenAI::StructuredOutput::JsonSchemaConverter => model}}}
101102
parsed.dig(:text, :format).store(:schema, model.to_json_schema)
103+
in {tools: Array => tools}
104+
mapped = tools.map do |tool|
105+
case tool
106+
in OpenAI::StructuredOutput::JsonSchemaConverter
107+
name = tool.name.split("::").last
108+
tool_models.store(name, tool)
109+
{
110+
type: :function,
111+
strict: true,
112+
name: name,
113+
parameters: tool.to_json_schema
114+
}
115+
in {type: :function, parameters: OpenAI::StructuredOutput::JsonSchemaConverter => params}
116+
func = tool.fetch(:function)
117+
name = func[:name] ||= params.name.split("::").last
118+
tool_models.store(name, params)
119+
func.update(parameters: params.to_json_schema)
120+
else
121+
end
122+
end
123+
tools.replace(mapped)
102124
else
103125
end
104126

@@ -116,6 +138,13 @@ def create(params)
116138
content.store(:parsed, coerced)
117139
end
118140
end
141+
raw[:output]&.each do |output|
142+
next unless output[:type] == "function_call"
143+
next if (model = tool_models[output.fetch(:name)]).nil?
144+
parsed = JSON.parse(output.fetch(:arguments), symbolize_names: true)
145+
coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
146+
output.store(:parsed, coerced)
147+
end
119148

120149
raw
121150
end

rbi/openai/models/responses/response_function_tool_call.rbi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ module OpenAI
1616
sig { returns(String) }
1717
attr_accessor :arguments
1818

19+
# The parsed contents of the arguments.
20+
sig { returns(T.anything) }
21+
attr_accessor :parsed
22+
1923
# The unique ID of the function tool call generated by the model.
2024
sig { returns(String) }
2125
attr_accessor :call_id

0 commit comments

Comments
 (0)