prompt_builder 0.1.2 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +145 -5
- data/VERSION +1 -1
- data/lib/prompt_builder/errors.rb +8 -0
- data/lib/prompt_builder/items/message.rb +40 -16
- data/lib/prompt_builder/response.rb +59 -0
- data/lib/prompt_builder/serializers/base.rb +28 -0
- data/lib/prompt_builder/serializers/chat_completion/request.rb +30 -9
- data/lib/prompt_builder/serializers/chat_completion/response.rb +12 -0
- data/lib/prompt_builder/serializers/converse/request.rb +60 -16
- data/lib/prompt_builder/serializers/converse/response.rb +15 -0
- data/lib/prompt_builder/serializers/gemini/request.rb +30 -9
- data/lib/prompt_builder/serializers/gemini/response.rb +9 -0
- data/lib/prompt_builder/serializers/messages/request.rb +36 -10
- data/lib/prompt_builder/serializers/messages/response.rb +9 -0
- data/lib/prompt_builder/serializers/open_responses/request.rb +6 -0
- data/lib/prompt_builder/serializers/open_responses/response.rb +10 -0
- data/lib/prompt_builder/session.rb +236 -11
- data/prompt_builder.gemspec +0 -2
- metadata +2 -16
|
@@ -27,6 +27,25 @@ module PromptBuilder
|
|
|
27
27
|
JSONIFY_FIELDS = %i[include tool_choice metadata text stream_options reasoning].freeze
|
|
28
28
|
private_constant :JSONIFY_FIELDS
|
|
29
29
|
|
|
30
|
+
# Effort levels accepted by +think+. This is the union of the levels
|
|
31
|
+
# recognized across serializers; each serializer omits levels its target
|
|
32
|
+
# API does not support.
|
|
33
|
+
THINK_EFFORT_LEVELS = %w[minimal low medium high xhigh max].freeze
|
|
34
|
+
private_constant :THINK_EFFORT_LEVELS
|
|
35
|
+
|
|
36
|
+
# All keyword options accepted by +initialize+.
|
|
37
|
+
#
|
|
38
|
+
# This constant is public API — unlike the private field-group constants
|
|
39
|
+
# above, it must not be marked with +private_constant+. Integrating gems
|
|
40
|
+
# use it to validate or partition option hashes before constructing a
|
|
41
|
+
# Session, e.g. +options.slice(*PromptBuilder::Session::INITIALIZE_OPTIONS)+.
|
|
42
|
+
#
|
|
43
|
+
# @api public
|
|
44
|
+
# @return [Array<Symbol>] the supported keyword option names
|
|
45
|
+
INITIALIZE_OPTIONS = (
|
|
46
|
+
STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS + %i[input extra system]
|
|
47
|
+
).freeze
|
|
48
|
+
|
|
30
49
|
# @!attribute [rw] model
|
|
31
50
|
# @return [String, nil] the model identifier
|
|
32
51
|
# @!attribute [rw] instructions
|
|
@@ -109,10 +128,44 @@ module PromptBuilder
|
|
|
109
128
|
# @return [Integer] the index in +items+ marking the boundary after the last response
|
|
110
129
|
attr_reader :response_boundary_index
|
|
111
130
|
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
131
|
+
# Restore the response boundary, e.g. when deserializing a session. The
|
|
132
|
+
# value is clamped to the current item count. Normally the boundary is
|
|
133
|
+
# maintained by +add_response+; use this only to reconstruct state.
|
|
134
|
+
#
|
|
135
|
+
# @param index [Integer] the boundary index
|
|
136
|
+
# @return [Integer]
|
|
137
|
+
def response_boundary_index=(index)
|
|
138
|
+
@response_boundary_index = index.to_i.clamp(0, @items.length)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Provider-specific extra data for serializers. Always returns a Hash, empty
|
|
142
|
+
# when nothing is set. Keys are always Strings.
|
|
143
|
+
#
|
|
144
|
+
# The returned Hash is a copy, so mutating it does not change the session.
|
|
145
|
+
# To add or remove a key, modify a copy and assign it back:
|
|
146
|
+
#
|
|
147
|
+
# session.extra = session.extra.merge("guardrail_config" => {"guardrailIdentifier" => "gr-1"})
|
|
148
|
+
#
|
|
149
|
+
# Recognized keys vary by target format. Unrecognized keys are silently
|
|
150
|
+
# ignored by each serializer.
|
|
151
|
+
#
|
|
152
|
+
# @return [Hash] a copy of the provider-specific extra data
|
|
153
|
+
def extra
|
|
154
|
+
PromptBuilder.jsonify(@extra)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Replace the provider-specific extra data. Keys are deep-stringified with
|
|
158
|
+
# +PromptBuilder.jsonify+ and the value is copied, so the assigned Hash is
|
|
159
|
+
# not shared with the session. Pass +nil+ to clear.
|
|
160
|
+
#
|
|
161
|
+
# @param value [Hash, nil] the extra data, or nil to clear it
|
|
162
|
+
# @return [Hash] the stored extra data
|
|
163
|
+
# @raise [ArgumentError] if the value is neither a Hash nor nil
|
|
164
|
+
def extra=(value)
|
|
165
|
+
raise ArgumentError, "extra must be a Hash" unless value.nil? || value.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
@extra = PromptBuilder.jsonify(value || {})
|
|
168
|
+
end
|
|
116
169
|
|
|
117
170
|
class << self
|
|
118
171
|
# Deserialize a Session from a Hash produced by +to_h+ or parsed JSON.
|
|
@@ -125,7 +178,7 @@ module PromptBuilder
|
|
|
125
178
|
def from_h(hash)
|
|
126
179
|
attrs = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS)
|
|
127
180
|
.each_with_object({}) { |f, acc| acc[f] = hash[f.to_s] }
|
|
128
|
-
attrs[:extra] = hash["extra"]
|
|
181
|
+
attrs[:extra] = hash["extra"]
|
|
129
182
|
session = new(**attrs)
|
|
130
183
|
|
|
131
184
|
Array(hash["input"]).each do |item_hash|
|
|
@@ -138,6 +191,8 @@ module PromptBuilder
|
|
|
138
191
|
session.register_tool(defn.name, description: defn.description, parameters: defn.parameters, strict: defn.strict, **extra)
|
|
139
192
|
end
|
|
140
193
|
|
|
194
|
+
session.response_boundary_index = hash["response_boundary_index"] if hash["response_boundary_index"]
|
|
195
|
+
|
|
141
196
|
session
|
|
142
197
|
end
|
|
143
198
|
end
|
|
@@ -145,19 +200,33 @@ module PromptBuilder
|
|
|
145
200
|
# Create a new Session with the given options.
|
|
146
201
|
# Accepts keyword arguments for all typed field groups (STRING_FIELDS,
|
|
147
202
|
# FLOAT_FIELDS, INTEGER_FIELDS, BOOLEAN_FIELDS, JSONIFY_FIELDS); all default
|
|
148
|
-
# to +nil+. The +input+
|
|
203
|
+
# to +nil+. The +system+ and +input+ shorthands auto-create a system and
|
|
204
|
+
# user message if provided. Unsupported keyword options raise an ArgumentError.
|
|
149
205
|
#
|
|
150
206
|
# @param attributes [Hash] keyword options; see attribute declarations above
|
|
207
|
+
# @option attributes [String, nil] :system optional string shorthand; a system
|
|
208
|
+
# message is automatically added with this text
|
|
151
209
|
# @option attributes [String, nil] :input optional string shorthand; a user
|
|
152
210
|
# message is automatically added with this text
|
|
153
211
|
# @option attributes [Hash, nil] :extra provider-specific extra data for
|
|
154
|
-
# serializers; recognized keys vary by target format
|
|
212
|
+
# serializers; recognized keys vary by target format. Keys are stringified.
|
|
213
|
+
# Defaults to an empty Hash and can be replaced later with +extra=+.
|
|
214
|
+
# @raise [ArgumentError] if an unsupported option is passed
|
|
155
215
|
def initialize(**attributes)
|
|
216
|
+
unsupported = attributes.keys - INITIALIZE_OPTIONS
|
|
217
|
+
unless unsupported.empty?
|
|
218
|
+
raise ArgumentError, "unsupported option#{"s" if unsupported.size > 1}: #{unsupported.join(", ")}"
|
|
219
|
+
end
|
|
220
|
+
|
|
156
221
|
(STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS).each do |f|
|
|
157
222
|
send(:"#{f}=", attributes[f])
|
|
158
223
|
end
|
|
159
|
-
|
|
224
|
+
|
|
225
|
+
self.extra = attributes[:extra]
|
|
226
|
+
|
|
160
227
|
@items = []
|
|
228
|
+
system(attributes[:system]) if attributes[:system]
|
|
229
|
+
|
|
161
230
|
@tool_definitions = {}
|
|
162
231
|
@response_boundary_index = 0
|
|
163
232
|
user(attributes[:input]) if attributes[:input]
|
|
@@ -171,6 +240,7 @@ module PromptBuilder
|
|
|
171
240
|
# session.user("Hello, how are you?")
|
|
172
241
|
# session.user(Content::InputText.new(text: "Hello, how are you?"))
|
|
173
242
|
# session.user(type: "input_text", text: "Hello, how are you?")
|
|
243
|
+
# session.user(text: "Hello, how are you?") # type defaults to "input_text"
|
|
174
244
|
# session.user([
|
|
175
245
|
# Content::InputText.new(text: "What is in this image?"),
|
|
176
246
|
# Content::InputImage.new(url: "http://example.com/image.png")
|
|
@@ -195,6 +265,10 @@ module PromptBuilder
|
|
|
195
265
|
#
|
|
196
266
|
# @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
|
|
197
267
|
# @return [Items::Message] the added message
|
|
268
|
+
# @example
|
|
269
|
+
# session.system("You are a helpful assistant.")
|
|
270
|
+
# session.system(text: "You are a helpful assistant.") # type defaults to "input_text"
|
|
271
|
+
# session.system(text: "You are a helpful assistant.", cache_point: true, cache_control: {type: "ephemeral"})
|
|
198
272
|
def system(content)
|
|
199
273
|
add_item(Items::Message.new(role: "system", content: content))
|
|
200
274
|
end
|
|
@@ -247,6 +321,19 @@ module PromptBuilder
|
|
|
247
321
|
@response_boundary_index = @items.length
|
|
248
322
|
end
|
|
249
323
|
|
|
324
|
+
# Clear all conversation items and the system instructions, returning the
|
|
325
|
+
# session to a fresh local-state start. Model configuration and registered
|
|
326
|
+
# tools are preserved.
|
|
327
|
+
#
|
|
328
|
+
# @return [self]
|
|
329
|
+
def clear
|
|
330
|
+
@items.clear
|
|
331
|
+
self.instructions = nil
|
|
332
|
+
self.previous_response_id = nil
|
|
333
|
+
@response_boundary_index = 0
|
|
334
|
+
self
|
|
335
|
+
end
|
|
336
|
+
|
|
250
337
|
# Register a tool on this session.
|
|
251
338
|
#
|
|
252
339
|
# @param name [String] the tool name
|
|
@@ -263,7 +350,7 @@ module PromptBuilder
|
|
|
263
350
|
strict: strict,
|
|
264
351
|
**extra
|
|
265
352
|
)
|
|
266
|
-
@tool_definitions[name] = definition
|
|
353
|
+
@tool_definitions[name.to_s] = definition
|
|
267
354
|
definition
|
|
268
355
|
end
|
|
269
356
|
|
|
@@ -286,6 +373,143 @@ module PromptBuilder
|
|
|
286
373
|
end
|
|
287
374
|
end
|
|
288
375
|
|
|
376
|
+
# Copy tool definitions from a ToolRegistry onto this session by name.
|
|
377
|
+
# With no names, all tools in the registry are copied (same as
|
|
378
|
+
# +register_tools+). Definitions are copied, so later registry changes do
|
|
379
|
+
# not affect the session and the tools survive +to_h+/+from_h+ round-trips.
|
|
380
|
+
#
|
|
381
|
+
# @param names [Array<String, Symbol>] the tool names to copy; empty for all
|
|
382
|
+
# @param registry [ToolRegistry, nil] the registry to copy from; defaults
|
|
383
|
+
# to the global +PromptBuilder.tool_registry+
|
|
384
|
+
# @return [Array<Tools::Definition>] the definitions registered on the session
|
|
385
|
+
# @raise [ToolNotFoundError] if a name is not registered in the registry
|
|
386
|
+
# @example
|
|
387
|
+
# session.use_tools("weather", "traffic_conditions")
|
|
388
|
+
# session.use_tools # all registry tools
|
|
389
|
+
# session.use_tools(:weather, registry: my_registry)
|
|
390
|
+
def use_tools(*names, registry: nil)
|
|
391
|
+
registry ||= PromptBuilder.tool_registry
|
|
392
|
+
raise ArgumentError, "registry must be an instance of ToolRegistry" unless registry.is_a?(ToolRegistry)
|
|
393
|
+
|
|
394
|
+
definitions = if names.empty?
|
|
395
|
+
registry.definitions
|
|
396
|
+
else
|
|
397
|
+
names.map do |name|
|
|
398
|
+
registry.definition_for(name.to_s) ||
|
|
399
|
+
raise(ToolNotFoundError, "No tool registered with name: #{name.to_s.inspect}")
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
definitions.map do |defn|
|
|
404
|
+
extra = defn.extra.transform_keys(&:to_sym)
|
|
405
|
+
register_tool(
|
|
406
|
+
defn.name,
|
|
407
|
+
description: defn.description,
|
|
408
|
+
parameters: defn.parameters,
|
|
409
|
+
strict: defn.strict,
|
|
410
|
+
**extra
|
|
411
|
+
)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Remove a single registered tool by name. Accepts a string or symbol and
|
|
416
|
+
# matches regardless of how the tool's key was stored.
|
|
417
|
+
#
|
|
418
|
+
# @param name [String, Symbol] the tool name
|
|
419
|
+
# @return [Tools::Definition, nil] the removed definition, or nil if not found
|
|
420
|
+
def remove_tool(name)
|
|
421
|
+
key = name.to_s
|
|
422
|
+
removed = nil
|
|
423
|
+
@tool_definitions.delete_if do |k, defn|
|
|
424
|
+
match = k.to_s == key
|
|
425
|
+
removed = defn if match
|
|
426
|
+
match
|
|
427
|
+
end
|
|
428
|
+
removed
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Remove all registered tools from the session.
|
|
432
|
+
#
|
|
433
|
+
# @return [Array<Tools::Definition>] the removed tool definitions
|
|
434
|
+
def clear_tools
|
|
435
|
+
removed = @tool_definitions.values
|
|
436
|
+
@tool_definitions.clear
|
|
437
|
+
removed
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Configure JSON Schema structured output. Writes the canonical
|
|
441
|
+
# +text.format+ wire hash consumed by all serializers, preserving any
|
|
442
|
+
# other +text+ keys (e.g. +verbosity+) already set.
|
|
443
|
+
#
|
|
444
|
+
# @param schema [Hash] the JSON Schema for the response
|
|
445
|
+
# @param name [String] the schema name
|
|
446
|
+
# @param strict [Boolean, nil] whether strict schema adherence is requested;
|
|
447
|
+
# omitted from the format when nil
|
|
448
|
+
# @param description [String, nil] an optional schema description
|
|
449
|
+
# @return [Hash] the resulting +text+ configuration
|
|
450
|
+
# @example
|
|
451
|
+
# session.json_output({"type" => "object", "properties" => {...}}, strict: true)
|
|
452
|
+
def json_output(schema, name: "response", strict: nil, description: nil)
|
|
453
|
+
format = {"type" => "json_schema", "name" => name.to_s, "schema" => schema}
|
|
454
|
+
format["strict"] = strict unless strict.nil?
|
|
455
|
+
format["description"] = description.to_s if description
|
|
456
|
+
self.text = (text || {}).merge("format" => format)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Configure reasoning/extended thinking portably across serializers.
|
|
460
|
+
# Stores a normalized +reasoning+ configuration; each serializer maps it
|
|
461
|
+
# to its native parameter:
|
|
462
|
+
#
|
|
463
|
+
# - +effort+ — Messages (+output_config.effort+), Chat Completions
|
|
464
|
+
# (+reasoning_effort+), Gemini (+thinkingConfig.thinkingLevel+), and
|
|
465
|
+
# Open Responses (+reasoning.effort+)
|
|
466
|
+
# - +budget_tokens+ — Messages (+thinking.budget_tokens+) and Gemini
|
|
467
|
+
# (+thinkingConfig.thinkingBudget+); ignored by effort-based APIs
|
|
468
|
+
# - Converse supports neither and warns once at serialization time
|
|
469
|
+
#
|
|
470
|
+
# Pass either +effort+ or +budget_tokens+, not both — providers reject
|
|
471
|
+
# requests that set both controls. Direct +session.reasoning = {...}+
|
|
472
|
+
# assignment keeps working for provider-specific keys.
|
|
473
|
+
#
|
|
474
|
+
# @param enabled [Boolean] pass +false+ to clear the reasoning configuration
|
|
475
|
+
# @param effort [String, Symbol, nil] a portable effort level; one of
|
|
476
|
+
# +minimal+, +low+, +medium+, +high+, +xhigh+, +max+ (unsupported levels
|
|
477
|
+
# are omitted by serializers whose target API does not accept them)
|
|
478
|
+
# @param budget_tokens [Integer, nil] an explicit thinking token budget
|
|
479
|
+
# @return [Hash, nil] the resulting +reasoning+ configuration
|
|
480
|
+
# @raise [ArgumentError] if neither or both of +effort+ and +budget_tokens+
|
|
481
|
+
# are given when enabling, or the values are invalid
|
|
482
|
+
# @example
|
|
483
|
+
# session.think(effort: :medium) # portable across serializers
|
|
484
|
+
# session.think(budget_tokens: 8_000) # explicit where supported
|
|
485
|
+
# session.think(false) # disable
|
|
486
|
+
def think(enabled = true, effort: nil, budget_tokens: nil)
|
|
487
|
+
unless enabled
|
|
488
|
+
raise ArgumentError, "cannot combine think(false) with effort or budget_tokens" if effort || budget_tokens
|
|
489
|
+
|
|
490
|
+
return self.reasoning = nil
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
if effort && budget_tokens
|
|
494
|
+
raise ArgumentError, "pass either effort or budget_tokens, not both"
|
|
495
|
+
elsif effort.nil? && budget_tokens.nil?
|
|
496
|
+
raise ArgumentError, "think requires effort: or budget_tokens:"
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
if effort
|
|
500
|
+
effort = effort.to_s
|
|
501
|
+
unless THINK_EFFORT_LEVELS.include?(effort)
|
|
502
|
+
raise ArgumentError, "effort must be one of: #{THINK_EFFORT_LEVELS.join(", ")}"
|
|
503
|
+
end
|
|
504
|
+
self.reasoning = {"effort" => effort}
|
|
505
|
+
else
|
|
506
|
+
budget_tokens = Integer(budget_tokens)
|
|
507
|
+
raise ArgumentError, "budget_tokens must be positive" unless budget_tokens.positive?
|
|
508
|
+
|
|
509
|
+
self.reasoning = {"budget_tokens" => budget_tokens}
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
289
513
|
# Return all tool definitions registered on this session.
|
|
290
514
|
#
|
|
291
515
|
# @return [Array<Tools::Definition>] all tool definitions
|
|
@@ -332,6 +556,7 @@ module PromptBuilder
|
|
|
332
556
|
|
|
333
557
|
h["input"] = @items.map(&:to_h) unless @items.empty?
|
|
334
558
|
h["previous_response_id"] = @previous_response_id if @previous_response_id
|
|
559
|
+
h["response_boundary_index"] = @response_boundary_index if @response_boundary_index.positive?
|
|
335
560
|
|
|
336
561
|
h["tools"] = tool_definitions.map(&:to_h) unless @tool_definitions.empty?
|
|
337
562
|
|
|
@@ -355,7 +580,7 @@ module PromptBuilder
|
|
|
355
580
|
val = send(f)
|
|
356
581
|
h[f.to_s] = val if val
|
|
357
582
|
}
|
|
358
|
-
h["extra"] =
|
|
583
|
+
h["extra"] = extra unless @extra.empty?
|
|
359
584
|
|
|
360
585
|
h
|
|
361
586
|
end
|
|
@@ -376,7 +601,7 @@ module PromptBuilder
|
|
|
376
601
|
def config_hash
|
|
377
602
|
h = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS - %i[previous_response_id])
|
|
378
603
|
.each_with_object({}) { |f, acc| acc[f] = send(f) }
|
|
379
|
-
h[:extra] = @extra
|
|
604
|
+
h[:extra] = @extra
|
|
380
605
|
h
|
|
381
606
|
end
|
|
382
607
|
end
|
data/prompt_builder.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prompt_builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: bundler
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :development
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
11
|
+
dependencies: []
|
|
26
12
|
email:
|
|
27
13
|
- bbdurand@gmail.com
|
|
28
14
|
executables: []
|