prompt_builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +763 -0
  5. data/VERSION +1 -0
  6. data/lib/prompt_builder/content/base.rb +44 -0
  7. data/lib/prompt_builder/content/input_file.rb +63 -0
  8. data/lib/prompt_builder/content/input_image.rb +64 -0
  9. data/lib/prompt_builder/content/input_text.rb +42 -0
  10. data/lib/prompt_builder/content/input_video.rb +43 -0
  11. data/lib/prompt_builder/content/output_text.rb +59 -0
  12. data/lib/prompt_builder/content/reasoning_text.rb +42 -0
  13. data/lib/prompt_builder/content/refusal_content.rb +42 -0
  14. data/lib/prompt_builder/content/summary_text.rb +42 -0
  15. data/lib/prompt_builder/content/text.rb +42 -0
  16. data/lib/prompt_builder/content.rb +28 -0
  17. data/lib/prompt_builder/errors.rb +18 -0
  18. data/lib/prompt_builder/items/base.rb +41 -0
  19. data/lib/prompt_builder/items/compaction.rb +60 -0
  20. data/lib/prompt_builder/items/function_call.rb +97 -0
  21. data/lib/prompt_builder/items/function_call_output.rb +110 -0
  22. data/lib/prompt_builder/items/item_reference.rb +42 -0
  23. data/lib/prompt_builder/items/message.rb +113 -0
  24. data/lib/prompt_builder/items/reasoning.rb +75 -0
  25. data/lib/prompt_builder/items.rb +13 -0
  26. data/lib/prompt_builder/response.rb +257 -0
  27. data/lib/prompt_builder/serializers/base.rb +37 -0
  28. data/lib/prompt_builder/serializers/chat_completion/request.rb +389 -0
  29. data/lib/prompt_builder/serializers/chat_completion/response.rb +139 -0
  30. data/lib/prompt_builder/serializers/chat_completion.rb +30 -0
  31. data/lib/prompt_builder/serializers/converse/request.rb +623 -0
  32. data/lib/prompt_builder/serializers/converse/response.rb +140 -0
  33. data/lib/prompt_builder/serializers/converse.rb +30 -0
  34. data/lib/prompt_builder/serializers/gemini/request.rb +562 -0
  35. data/lib/prompt_builder/serializers/gemini/response.rb +233 -0
  36. data/lib/prompt_builder/serializers/gemini.rb +30 -0
  37. data/lib/prompt_builder/serializers/messages/request.rb +634 -0
  38. data/lib/prompt_builder/serializers/messages/response.rb +157 -0
  39. data/lib/prompt_builder/serializers/messages.rb +30 -0
  40. data/lib/prompt_builder/serializers/open_responses/request.rb +229 -0
  41. data/lib/prompt_builder/serializers/open_responses/response.rb +18 -0
  42. data/lib/prompt_builder/serializers/open_responses.rb +30 -0
  43. data/lib/prompt_builder/serializers.rb +35 -0
  44. data/lib/prompt_builder/session.rb +383 -0
  45. data/lib/prompt_builder/tool_registry.rb +75 -0
  46. data/lib/prompt_builder/tools/definition.rb +66 -0
  47. data/lib/prompt_builder/tools.rb +7 -0
  48. data/lib/prompt_builder/usage.rb +100 -0
  49. data/lib/prompt_builder.rb +86 -0
  50. data/prompt_builder.gemspec +41 -0
  51. metadata +107 -0
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ # The main DSL entry point for building Open Responses API request payloads.
5
+ # Manages conversation items, tool registration, and serialization to
6
+ # multiple API formats.
7
+ class Session
8
+ # Boolean fields that may legitimately be false; serialized with a nil? check.
9
+ BOOLEAN_FIELDS = %i[parallel_tool_calls stream background store].freeze
10
+ private_constant :BOOLEAN_FIELDS
11
+
12
+ # String-typed fields coerced with .to_s on assignment.
13
+ STRING_FIELDS = %i[
14
+ model instructions previous_response_id truncation safety_identifier prompt_cache_key prompt_cache_retention service_tier
15
+ ].freeze
16
+ private_constant :STRING_FIELDS
17
+
18
+ # Float-typed fields coerced with .to_f on assignment.
19
+ FLOAT_FIELDS = %i[temperature top_p presence_penalty frequency_penalty].freeze
20
+ private_constant :FLOAT_FIELDS
21
+
22
+ # Integer-typed fields coerced with .to_i on assignment.
23
+ INTEGER_FIELDS = %i[max_output_tokens max_tool_calls top_logprobs].freeze
24
+ private_constant :INTEGER_FIELDS
25
+
26
+ # Complex fields serialized to JSON-compatible values via PromptBuilder.jsonify.
27
+ JSONIFY_FIELDS = %i[include tool_choice metadata text stream_options reasoning].freeze
28
+ private_constant :JSONIFY_FIELDS
29
+
30
+ # @!attribute [rw] model
31
+ # @return [String, nil] the model identifier
32
+ # @!attribute [rw] instructions
33
+ # @return [String, nil] the system instructions
34
+ # @!attribute [rw] previous_response_id
35
+ # @return [String, nil] the previous response identifier for server-side state
36
+ # @!attribute [rw] truncation
37
+ # @return [String, nil] the truncation strategy
38
+ # @!attribute [rw] safety_identifier
39
+ # @return [String, nil] the safety identifier
40
+ # @!attribute [rw] prompt_cache_key
41
+ # @return [String, nil] the prompt cache key
42
+ # @!attribute [rw] prompt_cache_retention
43
+ # @return [String, nil] the prompt cache retention policy
44
+ # @!attribute [rw] service_tier
45
+ # @return [String, nil] the service tier
46
+ STRING_FIELDS.each do |f|
47
+ attr_reader f
48
+ define_method(:"#{f}=") { |v| instance_variable_set(:"@#{f}", v.nil? ? nil : v.to_s) }
49
+ end
50
+
51
+ # @!attribute [rw] temperature
52
+ # @return [Float, nil] the temperature
53
+ # @!attribute [rw] top_p
54
+ # @return [Float, nil] the top_p sampling parameter
55
+ # @!attribute [rw] presence_penalty
56
+ # @return [Float, nil] the presence penalty
57
+ # @!attribute [rw] frequency_penalty
58
+ # @return [Float, nil] the frequency penalty
59
+ FLOAT_FIELDS.each do |f|
60
+ attr_reader f
61
+ define_method(:"#{f}=") { |v| instance_variable_set(:"@#{f}", v.nil? ? nil : v.to_f) }
62
+ end
63
+
64
+ # @!attribute [rw] max_output_tokens
65
+ # @return [Integer, nil] the maximum output tokens
66
+ # @!attribute [rw] max_tool_calls
67
+ # @return [Integer, nil] the maximum number of tool calls
68
+ # @!attribute [rw] top_logprobs
69
+ # @return [Integer, nil] the number of top log probabilities to return
70
+ INTEGER_FIELDS.each do |f|
71
+ attr_reader f
72
+ define_method(:"#{f}=") { |v| instance_variable_set(:"@#{f}", v.nil? ? nil : v.to_i) }
73
+ end
74
+
75
+ # @!attribute [rw] parallel_tool_calls
76
+ # @return [Boolean, nil] whether parallel tool calls are enabled
77
+ # @!attribute [rw] stream
78
+ # @return [Boolean, nil] whether to stream the response
79
+ # @!attribute [rw] background
80
+ # @return [Boolean, nil] whether this is a background request
81
+ # @!attribute [rw] store
82
+ # @return [Boolean, nil] whether to store the response
83
+ BOOLEAN_FIELDS.each do |f|
84
+ attr_reader f
85
+ define_method(:"#{f}=") { |v| instance_variable_set(:"@#{f}", v) }
86
+ alias_method :"#{f}?", f
87
+ end
88
+
89
+ # @!attribute [rw] include
90
+ # @return [Array, nil] fields to include in the response
91
+ # @!attribute [rw] tool_choice
92
+ # @return [String, Hash, nil] the tool choice configuration
93
+ # @!attribute [rw] metadata
94
+ # @return [Hash, nil] arbitrary metadata
95
+ # @!attribute [rw] text
96
+ # @return [Hash, nil] text output configuration
97
+ # @!attribute [rw] stream_options
98
+ # @return [Hash, nil] stream configuration options
99
+ # @!attribute [rw] reasoning
100
+ # @return [Hash, nil] the reasoning configuration
101
+ JSONIFY_FIELDS.each do |f|
102
+ attr_reader f
103
+ define_method(:"#{f}=") { |v| instance_variable_set(:"@#{f}", v.nil? ? nil : PromptBuilder.jsonify(v)) }
104
+ end
105
+
106
+ # @return [Array<Items::Base>] all conversation items
107
+ attr_reader :items
108
+
109
+ # @return [Integer] the index in +items+ marking the boundary after the last response
110
+ attr_reader :response_boundary_index
111
+
112
+ # @return [Hash, nil] provider-specific extra data for serializers.
113
+ # Recognized keys vary by target format. Unrecognized keys are silently
114
+ # ignored by each serializer.
115
+ attr_reader :extra
116
+
117
+ class << self
118
+ # Deserialize a Session from a Hash produced by +to_h+ or parsed JSON.
119
+ # Reconstructs all config fields and conversation items. Tool definitions
120
+ # are restored without handlers; re-register handlers separately if you
121
+ # need to invoke the tools.
122
+ #
123
+ # @param hash [Hash] a Hash with string keys
124
+ # @return [Session]
125
+ def from_h(hash)
126
+ attrs = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS)
127
+ .each_with_object({}) { |f, acc| acc[f] = hash[f.to_s] }
128
+ attrs[:extra] = hash["extra"] if hash["extra"]
129
+ session = new(**attrs)
130
+
131
+ Array(hash["input"]).each do |item_hash|
132
+ session.add_item(Items::Base.from_h(item_hash))
133
+ end
134
+
135
+ Array(hash["tools"]).each do |tool_hash|
136
+ defn = Tools::Definition.from_h(tool_hash)
137
+ extra = defn.extra.transform_keys(&:to_sym)
138
+ session.register_tool(defn.name, description: defn.description, parameters: defn.parameters, strict: defn.strict, **extra)
139
+ end
140
+
141
+ session
142
+ end
143
+ end
144
+
145
+ # Create a new Session with the given options.
146
+ # Accepts keyword arguments for all typed field groups (STRING_FIELDS,
147
+ # FLOAT_FIELDS, INTEGER_FIELDS, BOOLEAN_FIELDS, JSONIFY_FIELDS); all default
148
+ # to +nil+. The +input+ shorthand auto-creates a user message if provided.
149
+ #
150
+ # @param attributes [Hash] keyword options; see attribute declarations above
151
+ # @option attributes [String, nil] :input optional string shorthand; a user
152
+ # message is automatically added with this text
153
+ # @option attributes [Hash, nil] :extra provider-specific extra data for
154
+ # serializers; recognized keys vary by target format
155
+ def initialize(**attributes)
156
+ (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS).each do |f|
157
+ send(:"#{f}=", attributes[f])
158
+ end
159
+ @extra = PromptBuilder.jsonify(attributes[:extra]) if attributes[:extra]
160
+ @items = []
161
+ @tool_definitions = {}
162
+ @response_boundary_index = 0
163
+ user(attributes[:input]) if attributes[:input]
164
+ end
165
+
166
+ # Add a user message to the conversation.
167
+ #
168
+ # @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
169
+ # @return [Items::Message] the added message
170
+ # @example
171
+ # session.user("Hello, how are you?")
172
+ # session.user(Content::InputText.new(text: "Hello, how are you?"))
173
+ # session.user(type: "input_text", text: "Hello, how are you?")
174
+ # session.user([
175
+ # Content::InputText.new(text: "What is in this image?"),
176
+ # Content::InputImage.new(url: "http://example.com/image.png")
177
+ # ])
178
+ # session.user([
179
+ # {type: "input_text", text: "What is in this image?"},
180
+ # {type: "input_image", url: "http://example.com/image.png"}
181
+ # ])
182
+ def user(content)
183
+ add_item(Items::Message.new(role: "user", content: content))
184
+ end
185
+
186
+ # Add an assistant message to the conversation.
187
+ #
188
+ # @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
189
+ # @return [Items::Message] the added message
190
+ def assistant(content)
191
+ add_item(Items::Message.new(role: "assistant", content: content))
192
+ end
193
+
194
+ # Add a system message to the conversation.
195
+ #
196
+ # @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
197
+ # @return [Items::Message] the added message
198
+ def system(content)
199
+ add_item(Items::Message.new(role: "system", content: content))
200
+ end
201
+
202
+ # Add a developer message to the conversation.
203
+ #
204
+ # @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
205
+ # @return [Items::Message] the added message
206
+ def developer(content)
207
+ add_item(Items::Message.new(role: "developer", content: content))
208
+ end
209
+
210
+ # Add a tool call output to the conversation.
211
+ #
212
+ # @param call_id [String] the tool call identifier
213
+ # @param result [String, Hash, Array, Content::Base, nil] the tool call result
214
+ # @return [Items::FunctionOutput] the added function output item
215
+ def add_function_call_output(call_id:, result:)
216
+ add_item(Items::FunctionOutput.new(call_id: call_id, result: result))
217
+ end
218
+
219
+ # Add a raw item to the conversation.
220
+ #
221
+ # @param item [Items::Base] the item to add
222
+ # @return [Items::Base] the added item
223
+ def add_item(item)
224
+ raise ArgumentError, "item must be an instance of Items::Base" unless item.is_a?(Items::Base)
225
+
226
+ @items << item
227
+ item
228
+ end
229
+
230
+ # Add a response to the conversation. Output items are always appended
231
+ # to +items+ so that the full history is available locally. When the
232
+ # session is in server state mode (previous_response_id already set),
233
+ # the id is also updated so +to_h+ can use it as a serialization
234
+ # optimization.
235
+ #
236
+ # @param response [Response] the API response
237
+ # @return [void]
238
+ def add_response(response)
239
+ raise ArgumentError, "response must be an instance of Response" unless response.is_a?(Response)
240
+
241
+ @items.concat(response.output)
242
+ # Only refresh previous_response_id when the session is already in
243
+ # server-state mode AND the response actually carries an id; otherwise
244
+ # leave the existing pointer alone (responses from formats that don't
245
+ # populate `id` would otherwise silently drop us back into local state).
246
+ self.previous_response_id = response.id if !local_state? && response.id
247
+ @response_boundary_index = @items.length
248
+ end
249
+
250
+ # Register a tool on this session.
251
+ #
252
+ # @param name [String] the tool name
253
+ # @param description [String, nil] the tool description
254
+ # @param parameters [Hash, nil] the JSON Schema for parameters
255
+ # @param strict [Boolean] whether strict mode is enabled
256
+ # @param extra [Hash] provider-specific extra keyword arguments (e.g. cache_control)
257
+ # @return [Tools::Definition] the registered definition
258
+ def register_tool(name, description: nil, parameters: nil, strict: false, **extra)
259
+ definition = Tools::Definition.new(
260
+ name: name,
261
+ description: description,
262
+ parameters: parameters,
263
+ strict: strict,
264
+ **extra
265
+ )
266
+ @tool_definitions[name] = definition
267
+ definition
268
+ end
269
+
270
+ # Register all tools from a ToolRegistry.
271
+ #
272
+ # @param registry [ToolRegistry] the registry to copy tools from
273
+ # @return [void]
274
+ def register_tools(registry)
275
+ raise ArgumentError, "registry must be an instance of ToolRegistry" unless registry.is_a?(ToolRegistry)
276
+
277
+ registry.definitions.each do |defn|
278
+ extra = defn.extra.transform_keys(&:to_sym)
279
+ register_tool(
280
+ defn.name,
281
+ description: defn.description,
282
+ parameters: defn.parameters,
283
+ strict: defn.strict,
284
+ **extra
285
+ )
286
+ end
287
+ end
288
+
289
+ # Return all tool definitions registered on this session.
290
+ #
291
+ # @return [Array<Tools::Definition>] all tool definitions
292
+ def tool_definitions
293
+ @tool_definitions.values
294
+ end
295
+
296
+ # Create a new Session with the same configuration and tools but no items.
297
+ #
298
+ # @return [Session] a new session with cloned configuration
299
+ def clone_config
300
+ session = Session.new(**config_hash)
301
+ @tool_definitions.each do |name, defn|
302
+ extra = defn.extra.transform_keys(&:to_sym)
303
+ session.register_tool(
304
+ name,
305
+ description: defn.description,
306
+ parameters: defn.parameters,
307
+ strict: defn.strict,
308
+ **extra
309
+ )
310
+ end
311
+ session
312
+ end
313
+
314
+ # Check if this session is in local state mode (no previous_response_id). This
315
+ # indicates that the full conversation history is stored in the session and will be
316
+ # sent with each request. Once a response with an id is added, the session switches
317
+ # to server state mode, where only new items after the last response are sent
318
+ # and the previous_response_id is used to reference the last response.
319
+ #
320
+ # @return [Boolean]
321
+ def local_state?
322
+ @previous_response_id.nil?
323
+ end
324
+
325
+ # Serialize to an Open Responses API request Hash with string keys.
326
+ #
327
+ # @return [Hash]
328
+ def to_h
329
+ h = {}
330
+ h["model"] = @model if @model
331
+ h["instructions"] = @instructions if @instructions
332
+
333
+ h["input"] = @items.map(&:to_h) unless @items.empty?
334
+ h["previous_response_id"] = @previous_response_id if @previous_response_id
335
+
336
+ h["tools"] = tool_definitions.map(&:to_h) unless @tool_definitions.empty?
337
+
338
+ (STRING_FIELDS - %i[model instructions previous_response_id]).each do |f|
339
+ val = send(f)
340
+ h[f.to_s] = val if val
341
+ end
342
+ FLOAT_FIELDS.each { |f|
343
+ val = send(f)
344
+ h[f.to_s] = val if val
345
+ }
346
+ INTEGER_FIELDS.each { |f|
347
+ val = send(f)
348
+ h[f.to_s] = val if val
349
+ }
350
+ BOOLEAN_FIELDS.each { |f|
351
+ val = send(f)
352
+ h[f.to_s] = val unless val.nil?
353
+ }
354
+ JSONIFY_FIELDS.each { |f|
355
+ val = send(f)
356
+ h[f.to_s] = val if val
357
+ }
358
+ h["extra"] = @extra if @extra
359
+
360
+ h
361
+ end
362
+
363
+ # Export this session to an alternate API format using the given serializer.
364
+ #
365
+ # @param serializer_class [Class, Symbol] a serializer class (e.g. Serializers::ChatCompletion)
366
+ # or a symbol shorthand (+:open_responses+, +:chat_completion+, +:messages+,
367
+ # +:gemini+, +:converse+)
368
+ # @return [Hash] the serialized request payload
369
+ # @raise [ArgumentError] if a symbol is given that does not map to a known serializer
370
+ def request_payload(serializer_class)
371
+ Serializers.resolve(serializer_class).request_payload(self)
372
+ end
373
+
374
+ private
375
+
376
+ def config_hash
377
+ h = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS - %i[previous_response_id])
378
+ .each_with_object({}) { |f, acc| acc[f] = send(f) }
379
+ h[:extra] = @extra if @extra
380
+ h
381
+ end
382
+ end
383
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ # Registry for tool definitions and their handler callables.
5
+ class ToolRegistry
6
+ # Create a new ToolRegistry.
7
+ def initialize
8
+ @definitions = {}
9
+ @handlers = {}
10
+ end
11
+
12
+ # Register a tool with its definition and handler.
13
+ #
14
+ # @param name [String, Symbol] the tool name
15
+ # @param description [String, nil] the tool description
16
+ # @param parameters [Hash, nil] the JSON Schema for parameters
17
+ # @param strict [Boolean] whether strict mode is enabled
18
+ # @param callable [#call, nil] a callable handler (alternative to block)
19
+ # @param extra [Hash] provider-specific extra keyword arguments (e.g. cache_control)
20
+ # @yield [Hash] the parsed arguments when the tool is invoked
21
+ # @yieldreturn [Object] the tool output (String, Hash, Array, or any object)
22
+ # @return [Tools::Definition] the registered definition
23
+ def register(name, description: nil, parameters: nil, strict: false, callable: nil, **extra, &handler)
24
+ name = name.to_s
25
+ raise ArgumentError.new("Tool name is required") if name.empty?
26
+
27
+ definition = Tools::Definition.new(
28
+ name: name,
29
+ description: description,
30
+ parameters: parameters,
31
+ strict: strict,
32
+ **extra
33
+ )
34
+ @definitions[name] = definition
35
+ @handlers[name] = callable || handler
36
+ definition
37
+ end
38
+
39
+ # Look up a handler by name.
40
+ #
41
+ # @param name [String] the tool name
42
+ # @return [#call, nil] the handler, or nil if not found
43
+ def handler_for(name)
44
+ @handlers[name]
45
+ end
46
+
47
+ # Look up a definition by name.
48
+ #
49
+ # @param name [String] the tool name
50
+ # @return [Tools::Definition, nil] the definition, or nil if not found
51
+ def definition_for(name)
52
+ @definitions[name]
53
+ end
54
+
55
+ # Return all tool definitions.
56
+ #
57
+ # @return [Array<Tools::Definition>] all tool definitions
58
+ def definitions
59
+ @definitions.values
60
+ end
61
+
62
+ # Invoke a tool by name with the given arguments.
63
+ #
64
+ # @param name [String] the tool name
65
+ # @param arguments [Hash] the parsed arguments
66
+ # @return [Object] the raw tool handler return value
67
+ # @raise [ToolNotFoundError] if no handler is found for the given name
68
+ def invoke(name, arguments)
69
+ handler = handler_for(name)
70
+ raise ToolNotFoundError, "No handler registered for tool: #{name.inspect}" unless handler
71
+
72
+ handler.call(arguments)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ module Tools
5
+ # Represents a tool definition that describes a function available to the model.
6
+ class Definition
7
+ # @return [String] the tool name
8
+ attr_reader :name
9
+
10
+ # @return [String, nil] the tool description
11
+ attr_reader :description
12
+
13
+ # @return [Hash, nil] the JSON Schema for the tool parameters
14
+ attr_reader :parameters
15
+
16
+ # @return [Boolean] whether strict mode is enabled
17
+ attr_reader :strict
18
+
19
+ # @return [Hash, nil] provider-specific extra data (e.g. cache_control)
20
+ attr_reader :extra
21
+
22
+ # Create a new tool Definition.
23
+ #
24
+ # @param name [String] the tool name
25
+ # @param description [String, nil] the tool description
26
+ # @param parameters [Hash, nil] the JSON Schema for the parameters
27
+ # @param strict [Boolean] whether strict mode is enabled
28
+ # @param extra [Hash] provider-specific extra keyword arguments
29
+ def initialize(name:, description: nil, parameters: nil, strict: false, **extra)
30
+ @name = name&.to_s
31
+ @description = description&.to_s
32
+ @parameters = PromptBuilder.jsonify(parameters)
33
+ @strict = strict ? true : false
34
+ @extra = extra.transform_keys(&:to_s)
35
+ end
36
+
37
+ class << self
38
+ # Deserialize a Definition from a Hash.
39
+ #
40
+ # @param hash [Hash] a Hash with string keys
41
+ # @return [Definition]
42
+ def from_h(hash)
43
+ new(
44
+ name: hash["name"],
45
+ description: hash["description"],
46
+ parameters: hash["parameters"],
47
+ strict: hash.fetch("strict", false),
48
+ **hash.except("type", "name", "description", "parameters", "strict").transform_keys(&:to_sym)
49
+ )
50
+ end
51
+ end
52
+
53
+ # Serialize to a Hash with string keys. Nil values are omitted.
54
+ #
55
+ # @return [Hash]
56
+ def to_h
57
+ h = {"type" => "function", "name" => @name}
58
+ h["description"] = @description if @description
59
+ h["parameters"] = @parameters if @parameters
60
+ h["strict"] = @strict if @strict
61
+ h = PromptBuilder.jsonify(@extra).merge(h) unless @extra.empty?
62
+ h
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ module Tools
5
+ autoload :Definition, File.expand_path("tools/definition", __dir__)
6
+ end
7
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ # Value object representing token usage statistics from an API response.
5
+ class Usage
6
+ # @return [Integer, nil] number of input tokens
7
+ attr_reader :input_tokens
8
+
9
+ # @return [Integer, nil] number of output tokens
10
+ attr_reader :output_tokens
11
+
12
+ # @return [Integer, nil] total number of tokens
13
+ attr_reader :total_tokens
14
+
15
+ # @return [Hash, nil] input token details
16
+ attr_reader :input_tokens_details
17
+
18
+ # @return [Hash, nil] output token details
19
+ attr_reader :output_tokens_details
20
+
21
+ # Create a new Usage value object.
22
+ #
23
+ # @param input_tokens [Integer, nil] number of input tokens
24
+ # @param output_tokens [Integer, nil] number of output tokens
25
+ # @param total_tokens [Integer, nil] total number of tokens
26
+ # @param input_tokens_details [Hash, nil] input token details
27
+ # @param output_tokens_details [Hash, nil] output token details
28
+ # @param reasoning_tokens [Integer, nil] number of reasoning tokens
29
+ def initialize(input_tokens: nil, output_tokens: nil, total_tokens: nil,
30
+ input_tokens_details: nil, output_tokens_details: nil, reasoning_tokens: nil)
31
+ @input_tokens = input_tokens&.to_i
32
+ @output_tokens = output_tokens&.to_i
33
+ @total_tokens = total_tokens&.to_i
34
+ @input_tokens_details = PromptBuilder.jsonify(input_tokens_details)
35
+ @output_tokens_details = PromptBuilder.jsonify(output_tokens_details) || build_output_tokens_details(reasoning_tokens)
36
+ end
37
+
38
+ class << self
39
+ # Deserialize a Usage from a Hash.
40
+ #
41
+ # @param hash [Hash] a Hash with string keys
42
+ # @return [Usage]
43
+ def from_h(hash)
44
+ input_tokens_details = hash["input_tokens_details"]
45
+ output_tokens_details = hash["output_tokens_details"]
46
+
47
+ new(
48
+ input_tokens: hash["input_tokens"],
49
+ output_tokens: hash["output_tokens"],
50
+ total_tokens: hash["total_tokens"],
51
+ input_tokens_details: input_tokens_details,
52
+ output_tokens_details: output_tokens_details,
53
+ reasoning_tokens: hash["reasoning_tokens"] || output_tokens_details&.fetch("reasoning_tokens", nil)
54
+ )
55
+ end
56
+ end
57
+
58
+ # Number of cached input tokens.
59
+ #
60
+ # @return [Integer, nil]
61
+ def cached_tokens
62
+ @input_tokens_details&.fetch("cached_tokens", nil)
63
+ end
64
+
65
+ # Number of tokens used for cache creation. This is specific only to the Anthropic Messages API format.
66
+ #
67
+ # @return [Integer, nil]
68
+ def cache_creation_input_tokens
69
+ @input_tokens_details&.fetch("cache_creation_input_tokens", nil)
70
+ end
71
+
72
+ # Number of reasoning tokens.
73
+ #
74
+ # @return [Integer, nil]
75
+ def reasoning_tokens
76
+ @output_tokens_details&.fetch("reasoning_tokens", nil)
77
+ end
78
+
79
+ # Serialize to a Hash with string keys. Nil values are omitted.
80
+ #
81
+ # @return [Hash]
82
+ def to_h
83
+ h = {}
84
+ h["input_tokens"] = @input_tokens if @input_tokens
85
+ h["output_tokens"] = @output_tokens if @output_tokens
86
+ h["total_tokens"] = @total_tokens if @total_tokens
87
+ h["input_tokens_details"] = @input_tokens_details if @input_tokens_details
88
+ h["output_tokens_details"] = @output_tokens_details if @output_tokens_details
89
+ h
90
+ end
91
+
92
+ private
93
+
94
+ def build_output_tokens_details(reasoning_tokens)
95
+ return nil unless reasoning_tokens
96
+
97
+ {"reasoning_tokens" => reasoning_tokens}
98
+ end
99
+ end
100
+ end