prompt_builder 0.2.0 → 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 +26 -0
- data/README.md +77 -6
- data/VERSION +1 -1
- data/lib/prompt_builder/errors.rb +5 -0
- data/lib/prompt_builder/items/message.rb +40 -16
- data/lib/prompt_builder/serializers/base.rb +28 -0
- data/lib/prompt_builder/serializers/chat_completion/request.rb +17 -7
- data/lib/prompt_builder/serializers/chat_completion/response.rb +12 -0
- data/lib/prompt_builder/serializers/converse/request.rb +33 -14
- data/lib/prompt_builder/serializers/converse/response.rb +15 -0
- data/lib/prompt_builder/serializers/gemini/request.rb +20 -8
- data/lib/prompt_builder/serializers/gemini/response.rb +9 -0
- data/lib/prompt_builder/serializers/messages/request.rb +18 -8
- data/lib/prompt_builder/serializers/messages/response.rb +9 -0
- data/lib/prompt_builder/serializers/open_responses/request.rb +1 -0
- data/lib/prompt_builder/serializers/open_responses/response.rb +10 -0
- data/lib/prompt_builder/session.rb +109 -17
- data/prompt_builder.gemspec +0 -2
- metadata +2 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6a857e2a0958425d04263986bca11967fb575af1c1e40ada5bee63556447fe0
|
|
4
|
+
data.tar.gz: 9840a2750ac27e265aea1e25d9290ee3aecb56cd24889d5903b481891a2925db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70fe6418fdd6865e12ec8babb5d5a7542ff31e93b44e0dfb389c1e0853955d5657bbf3482462e1ec091fca8031ac988bfc70222195a8ad2fd50833eb2542c4a1
|
|
7
|
+
data.tar.gz: bd0d9abb2cbb42d10d70c8f1d15322cc58a4c88c6401f937a45844fbde2dcf8013b183cf19d206b1e4c0e48cf9c0a03fb3ad5b35c17ed083b12d7c73f00224c6
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.3.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Response parsers now recognize API error payloads (OpenAI/Gemini `error` envelopes, Anthropic `type: "error"` responses, Bedrock exception bodies) and raise a `PromptBuilder::ErrorResponseError` (a subclass of `UnexpectedPayloadError`) containing the error message reported by the API.
|
|
12
|
+
- `Session.new` accepts a `system:` shorthand that adds a system message, mirroring the existing `input:` shorthand.
|
|
13
|
+
- `Session#clear` resets the conversation — items, `instructions`, and `previous_response_id` — while preserving model configuration and registered tools.
|
|
14
|
+
- `Session#remove_tool` removes a registered tool by name (string or symbol); `Session#clear_tools` removes them all. Both return the removed definitions.
|
|
15
|
+
- `Session#extra=` sets or clears provider-specific extra data after the session has been constructed. Keys are normalized to strings; assigning `nil` clears the data.
|
|
16
|
+
- Message content hashes with a `text` key no longer require a `type` key; the type defaults to `input_text` (or `output_text` on assistant messages).
|
|
17
|
+
- `Items::Message#system?`, `#user?`, and `#assistant?` role predicates.
|
|
18
|
+
- The Converse serializer emits `cachePoint` markers from the `cache_point` extra on tool definitions and `FunctionCallOutput` items, in addition to content blocks.
|
|
19
|
+
- `Session#to_h`/`Session.from_h` now round-trip the response boundary (`response_boundary_index`), and a `response_boundary_index=` setter is available for reconstructing session state.
|
|
20
|
+
- `Session::INITIALIZE_OPTIONS` is now public API: the frozen Array of all keyword options accepted by the constructor, for code that needs to validate or partition an options hash before creating a session.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `Session#extra` always returns a Hash (previously `nil` when unset). The returned Hash is a copy; assign a modified copy back through `Session#extra=` to change it.
|
|
25
|
+
- `Session#to_h` omits `extra` when it is empty.
|
|
26
|
+
- `instructions` is now serialized after system/developer messages instead of before them, matching the Open Responses semantics of `instructions` applying to the current request.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Serializers silently dropped `Content::Text` content (hashes with `type: "text"`) from messages, system prompts, and tool results; it is now serialized as text everywhere `InputText`/`OutputText` are, including cache markers from content extras. The Messages and Gemini serializers also dropped `OutputText` content from system messages.
|
|
31
|
+
- Tool names are normalized to strings on registration, so registering the same tool by symbol and by string no longer creates duplicate definitions.
|
|
32
|
+
|
|
7
33
|
## 0.2.0
|
|
8
34
|
|
|
9
35
|
### Added
|
data/README.md
CHANGED
|
@@ -48,16 +48,28 @@ session = PromptBuilder::Session.new(
|
|
|
48
48
|
session.user("What is the capital of France?")
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
You can also pass
|
|
51
|
+
You can also pass `system` and `input` shorthands to create a system message and a user message in one step (equivalent to calling `session.system(...)` and `session.user(...)`):
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
54
|
session = PromptBuilder::Session.new(
|
|
55
55
|
model: "gpt-5.4",
|
|
56
|
+
system: "You are a helpful assistant.",
|
|
56
57
|
input: "What is the capital of France?"
|
|
57
58
|
)
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
Passing an option the constructor doesn't recognize
|
|
61
|
+
Passing an option the constructor doesn't recognize raises an `ArgumentError`. The full list of supported constructor options is available as `PromptBuilder::Session::INITIALIZE_OPTIONS`.
|
|
62
|
+
|
|
63
|
+
#### `instructions` vs. system and developer messages
|
|
64
|
+
|
|
65
|
+
Although both can be used to steer the model's behavior, `instructions` and system/developer messages are not interchangeable in the Open Responses API:
|
|
66
|
+
|
|
67
|
+
- `instructions` is a top-level request parameter, not part of the conversation. It is sent with every request, and in the Open Responses API it applies only to the request it accompanies — it is *not* carried over when chaining responses with `previous_response_id`. That makes it safe to change `session.instructions` at any point in a conversation; the new value simply applies from the next request onward.
|
|
68
|
+
- `session.system(...)` and `session.developer(...)` create messages inside the conversation history (the `input` array). They occupy a position in the transcript like any other message, and once the session is chained with `previous_response_id` they become part of the server-stored history and are not re-sent on subsequent requests.
|
|
69
|
+
|
|
70
|
+
As a rule of thumb, use `instructions` for standing behavior you may want to adjust from request to request, and use system or developer messages when the directive should be a durable part of the conversation record.
|
|
71
|
+
|
|
72
|
+
When serializing to the other formats this distinction collapses: `instructions` and any system/developer messages are merged together into the target format's single system field (see [Serializer Compatibility](#serializer-compatibility)).
|
|
61
73
|
|
|
62
74
|
### Conversation History
|
|
63
75
|
|
|
@@ -71,7 +83,13 @@ session.assistant("Hi there! How can I help you today?")
|
|
|
71
83
|
session.user("What's the weather like?")
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
Messages support the roles `user`, `assistant`, `system`, and `developer`.
|
|
86
|
+
Messages support the roles `user`, `assistant`, `system`, and `developer`. A `Message` exposes `system?`, `user?`, and `assistant?` predicates for checking its role.
|
|
87
|
+
|
|
88
|
+
Call `session.clear` to reset the conversation: it removes all items and the `instructions`, drops any `previous_response_id`, and returns the session to a fresh local-state start. Model configuration and registered tools are preserved, so the session can be reused for a new conversation.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
session.clear # items and instructions removed; model/config/tools kept
|
|
92
|
+
```
|
|
75
93
|
|
|
76
94
|
### Serializing Requests
|
|
77
95
|
|
|
@@ -162,6 +180,13 @@ response.has_tool_calls? # => false
|
|
|
162
180
|
response.usage # => #<PromptBuilder::Usage input_tokens=25 output_tokens=12 ...>
|
|
163
181
|
```
|
|
164
182
|
|
|
183
|
+
If the payload is an error returned by the API rather than a completion (e.g. an authentication failure, a validation error, or an AWS Coral service envelope), `Response.parse` raises a `PromptBuilder::ErrorResponseError` whose message contains the error reported by the API. Payloads that are neither a completion nor a recognizable error envelope raise a `PromptBuilder::UnexpectedPayloadError` (which `ErrorResponseError` subclasses) including the offending body.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
PromptBuilder::Response.parse({"error" => {"type" => "invalid_request_error", "message" => "Incorrect API key provided"}}, :chat_completion)
|
|
187
|
+
# => raises PromptBuilder::ErrorResponseError: the API returned an error: invalid_request_error: Incorrect API key provided
|
|
188
|
+
```
|
|
189
|
+
|
|
165
190
|
You can also synthesize a plain-text response without calling an API — useful for canned answers (e.g. halting an agent loop), cached responses, and tests:
|
|
166
191
|
|
|
167
192
|
```ruby
|
|
@@ -298,6 +323,13 @@ session.use_tools(:weather, registry: my_registry) # explicit registry
|
|
|
298
323
|
|
|
299
324
|
Tool definitions are *copied* onto the session in all cases, so later registry changes don't affect the session and the tools survive `to_h`/`from_h` round-trips.
|
|
300
325
|
|
|
326
|
+
Remove tools from a session with `remove_tool` (by name; accepts a string or symbol) or `clear_tools` (all at once). `remove_tool` returns the removed `Tools::Definition`, or `nil` if no tool by that name was registered; `clear_tools` returns the removed definitions.
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
session.remove_tool("weather") # => removed Tools::Definition, or nil
|
|
330
|
+
session.clear_tools # => [<removed definitions>]
|
|
331
|
+
```
|
|
332
|
+
|
|
301
333
|
### Content Types
|
|
302
334
|
|
|
303
335
|
Message content can be a plain string or an array of structured content objects for multi-modal input. Content can be provided as raw Hashes or as `PromptBuilder::Content` objects.
|
|
@@ -418,6 +450,8 @@ session = PromptBuilder::Session.new(
|
|
|
418
450
|
|
|
419
451
|
The `extra` attribute on sessions, content blocks, and tool definitions lets you pass provider-specific parameters that are not part of the Open Responses canonical format. Each serializer recognizes a defined set of `extra` keys and maps them to the appropriate location in the target format. Unrecognized keys are silently ignored.
|
|
420
452
|
|
|
453
|
+
On a session, `extra` is a read/write attribute that can be set at any point in the session's life. On content blocks and tool definitions it is set with keyword arguments at construction time.
|
|
454
|
+
|
|
421
455
|
#### Session Extra
|
|
422
456
|
|
|
423
457
|
Pass provider-specific top-level request parameters via `session.extra`:
|
|
@@ -471,6 +505,26 @@ session = PromptBuilder::Session.new(
|
|
|
471
505
|
)
|
|
472
506
|
```
|
|
473
507
|
|
|
508
|
+
The same data can be set after the session has been created by assigning to `session.extra`. Keys are normalized to strings, so symbol keys work too.
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
session = PromptBuilder::Session.new(model: "anthropic.claude-v2")
|
|
512
|
+
|
|
513
|
+
session.extra # => {}
|
|
514
|
+
|
|
515
|
+
# Assign the whole hash.
|
|
516
|
+
session.extra = {guardrail_config: {"guardrailIdentifier" => "my-guardrail", "guardrailVersion" => "1"}}
|
|
517
|
+
|
|
518
|
+
# `extra` returns a copy, so read, modify, and assign it back to change one key.
|
|
519
|
+
session.extra = session.extra.merge("stop_sequences" => ["\n\nHuman:"])
|
|
520
|
+
session.extra = session.extra.except("stop_sequences")
|
|
521
|
+
|
|
522
|
+
session.extra = nil # clears it
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
> [!IMPORTANT]
|
|
526
|
+
> `session.extra` never returns `nil` and its keys are always strings. Because the reader returns a copy, mutating it in place has no effect on the session — `session.extra["seed"] = 42` is silently discarded. Assign the modified hash back as shown above.
|
|
527
|
+
|
|
474
528
|
#### Content Extra
|
|
475
529
|
|
|
476
530
|
Content blocks support provider-specific attributes via keyword arguments that are captured in the `extra` hash:
|
|
@@ -500,6 +554,21 @@ session.user([
|
|
|
500
554
|
cache_point: true
|
|
501
555
|
)
|
|
502
556
|
])
|
|
557
|
+
|
|
558
|
+
# Bedrock Converse: cache_point on tool definitions and tool results
|
|
559
|
+
session.register_tool(
|
|
560
|
+
"search",
|
|
561
|
+
description: "Search the knowledge base.",
|
|
562
|
+
parameters: {"type" => "object", "properties" => {"query" => {"type" => "string"}}},
|
|
563
|
+
cache_point: true
|
|
564
|
+
)
|
|
565
|
+
session.add_item(
|
|
566
|
+
PromptBuilder::Items::FunctionCallOutput.new(
|
|
567
|
+
call_id: "call_1",
|
|
568
|
+
output: "Search results...",
|
|
569
|
+
cache_point: true
|
|
570
|
+
)
|
|
571
|
+
)
|
|
503
572
|
```
|
|
504
573
|
|
|
505
574
|
#### Tool Definition Extra
|
|
@@ -518,12 +587,14 @@ session.register_tool(
|
|
|
518
587
|
|
|
519
588
|
#### Recognized Extra Keys by Serializer
|
|
520
589
|
|
|
590
|
+
Session extra keys can be supplied to the constructor or assigned later with `session.extra=`.
|
|
591
|
+
|
|
521
592
|
| Serializer | Session Extra Keys | Content Extra Keys | Tool Extra Keys |
|
|
522
593
|
|:---|:---|:---|:---|
|
|
523
594
|
| **Chat Completions** | `stop`, `seed`, `logit_bias`, `n`, `prediction`, `web_search_options`, `modalities`, `audio` | `file_id`, `media_type` | — |
|
|
524
595
|
| **Messages** | `top_k`, `stop_sequences`, `cache_control` | `cache_control`, `citations`, `file_id`, `media_type` | `cache_control` |
|
|
525
596
|
| **Gemini** | `safety_settings`, `cached_content`, `stop_sequences`, `top_k`, `seed`, `candidate_count`, `response_modalities`, `media_resolution` | `file_id`, `media_type`, `thought_signature` | — |
|
|
526
|
-
| **Converse** | `stop_sequences`, `guardrail_config`, `additional_model_request_fields`, `additional_model_response_field_paths`, `performance_config`, `prompt_variables` | `media_type`, `cache_point` |
|
|
597
|
+
| **Converse** | `stop_sequences`, `guardrail_config`, `additional_model_request_fields`, `additional_model_response_field_paths`, `performance_config`, `prompt_variables` | `media_type`, `cache_point` | `cache_point` |
|
|
527
598
|
|
|
528
599
|
### Serialization and Persistence
|
|
529
600
|
|
|
@@ -794,7 +865,7 @@ Content and message restrictions:
|
|
|
794
865
|
- `FunctionCall.arguments` must parse to a JSON object; non-object JSON values raise (Bedrock's `toolUse.input` requires an object).
|
|
795
866
|
- `FunctionCallOutput.output` content supports `InputText`/`OutputText`, `InputImage`, `InputFile`, and `InputVideo`, mapping to Converse `text`, `image`, `document`, and `video` tool result blocks. Converse also supports `json` and `searchResult` tool result blocks, but this gem has no canonical content types for them, so unsupported content is omitted. `FunctionCallOutput.status` is mapped: `completed` → `success`, `failed`/`incomplete` → `error`, anything else passes through or is dropped.
|
|
796
867
|
- `tool_choice: "none"` and `tool_choice` without registered tools are both omitted.
|
|
797
|
-
- Converse API request features with no canonical Open Responses field are available through `session.extra` (`stop_sequences`, `guardrail_config`, `additional_model_request_fields`, `additional_model_response_field_paths`, `performance_config`, `prompt_variables`) and content `extra` (`cache_point`). Features not exposed at all include `guardContent`, audio blocks, and `searchResult` blocks.
|
|
868
|
+
- Converse API request features with no canonical Open Responses field are available through `session.extra` (`stop_sequences`, `guardrail_config`, `additional_model_request_fields`, `additional_model_response_field_paths`, `performance_config`, `prompt_variables`) and content `extra` (`cache_point`). `cache_point` is also recognized on tool definitions (the marker is appended to the `toolConfig.tools` array) and on `FunctionCallOutput` items (the marker is appended after the `toolResult` content block). Features not exposed at all include `guardContent`, audio blocks, and `searchResult` blocks.
|
|
798
869
|
- The serialized payload includes `modelId` as a convenience; the Converse REST endpoint takes the model id in the URL path (`/model/{modelId}/converse`), and AWS REST-JSON services ignore unrecognized body members. Remove it from the body if you prefer a strictly minimal payload.
|
|
799
870
|
|
|
800
871
|
Response-side limitations:
|
|
@@ -802,7 +873,7 @@ Response-side limitations:
|
|
|
802
873
|
- Unknown content block keys (e.g. `citationsContent`, `guardContent`) are silently skipped.
|
|
803
874
|
- `metrics.latencyMs`, `trace` (guardrail and prompt-router trace events), `additionalModelResponseFields`, `performanceConfig`, and the raw `serviceTier` object are exposed on `response.provider_data`. `response.service_tier` is also populated from `serviceTier.type`.
|
|
804
875
|
- `stopReason` mappings: `end_turn` / `tool_use` / `stop_sequence` → `completed`, `max_tokens` / `model_context_window_exceeded` → `incomplete`, `guardrail_intervened` / `content_filtered` / `malformed_model_output` / `malformed_tool_use` → `failed`. Unlike the Messages serializer, the matched stop sequence text is not surfaced separately because Converse does not echo it back unless you request provider-specific fields through `additionalModelResponseFieldPaths` (available via the `additional_model_response_field_paths` session extra; the requested fields appear on `response.provider_data`).
|
|
805
|
-
- `usage.cacheReadInputTokens` and `usage.cacheWriteInputTokens` populate `response.usage.input_tokens_details["cached_tokens"]` and `["cache_creation_input_tokens"]`. Cache writes require `cachePoint` markers in the request, which are emitted via the `cache_point` content
|
|
876
|
+
- `usage.cacheReadInputTokens` and `usage.cacheWriteInputTokens` populate `response.usage.input_tokens_details["cached_tokens"]` and `["cache_creation_input_tokens"]`. Cache writes require `cachePoint` markers in the request, which are emitted via the `cache_point` extra on content, tool definitions, and `FunctionCallOutput` items.
|
|
806
877
|
|
|
807
878
|
## Installation
|
|
808
879
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
|
@@ -19,6 +19,11 @@ module PromptBuilder
|
|
|
19
19
|
# Raised when a response payload does not match the expected shape.
|
|
20
20
|
class UnexpectedPayloadError < Error; end
|
|
21
21
|
|
|
22
|
+
# Raised when a response payload is an error returned by the API rather
|
|
23
|
+
# than a completion (e.g. an authentication, validation, or throttling
|
|
24
|
+
# error envelope). The message contains the error reported by the API.
|
|
25
|
+
class ErrorResponseError < UnexpectedPayloadError; end
|
|
26
|
+
|
|
22
27
|
# Raised by Response#parsed_json! when the response text is not valid JSON.
|
|
23
28
|
class ParseError < Error; end
|
|
24
29
|
end
|
|
@@ -23,6 +23,24 @@ module PromptBuilder
|
|
|
23
23
|
# @return [Hash, nil] provider-specific extra data
|
|
24
24
|
attr_reader :extra
|
|
25
25
|
|
|
26
|
+
class << self
|
|
27
|
+
# Deserialize a Message from a Hash.
|
|
28
|
+
#
|
|
29
|
+
# @param hash [Hash] a Hash with string keys
|
|
30
|
+
# @return [Message]
|
|
31
|
+
def from_h(hash)
|
|
32
|
+
content = (hash["content"] || []).map { |c| Content::Base.from_h(c) }
|
|
33
|
+
new(
|
|
34
|
+
id: hash["id"],
|
|
35
|
+
role: hash["role"],
|
|
36
|
+
status: hash["status"],
|
|
37
|
+
phase: hash["phase"],
|
|
38
|
+
content: content,
|
|
39
|
+
**hash.except("type", "id", "role", "status", "phase", "content").transform_keys(&:to_sym)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
26
44
|
# Create a new Message item.
|
|
27
45
|
#
|
|
28
46
|
# @param id [String, nil] the message identifier
|
|
@@ -40,22 +58,25 @@ module PromptBuilder
|
|
|
40
58
|
@extra = extra.transform_keys(&:to_s)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
# Check if the message is a system message.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def system?
|
|
65
|
+
role == "system"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if the message is a user message.
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def user?
|
|
72
|
+
role == "user"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if the message is an assistant message.
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def assistant?
|
|
79
|
+
role == "assistant"
|
|
59
80
|
end
|
|
60
81
|
|
|
61
82
|
# Serialize to a Hash with string keys.
|
|
@@ -102,6 +123,9 @@ module PromptBuilder
|
|
|
102
123
|
|
|
103
124
|
def normalize_hash_content(hash)
|
|
104
125
|
hash = hash.transform_keys(&:to_s)
|
|
126
|
+
if hash["type"].nil? && hash.key?("text")
|
|
127
|
+
hash = hash.merge("type" => (assistant? ? "output_text" : "input_text"))
|
|
128
|
+
end
|
|
105
129
|
unless hash["type"]
|
|
106
130
|
raise InvalidItemError,
|
|
107
131
|
"Content hash is missing required \"type\" key: #{hash.inspect}"
|
|
@@ -18,7 +18,9 @@ module PromptBuilder
|
|
|
18
18
|
#
|
|
19
19
|
# @param hash [Hash] the response hash in the target format
|
|
20
20
|
# @return [Response] the parsed response
|
|
21
|
+
# @raise [ErrorResponseError] if the payload is an API error envelope
|
|
21
22
|
def parse_response(hash)
|
|
23
|
+
check_error_response!(hash)
|
|
22
24
|
deserialize_response(hash)
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -32,6 +34,32 @@ module PromptBuilder
|
|
|
32
34
|
raise NotImplementedError
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# Detect API error envelopes before parsing so errors surface with the
|
|
38
|
+
# message reported by the API instead of the generic missing-key error
|
|
39
|
+
# from require_response_key!. Serializer response classes override
|
|
40
|
+
# error_response_message to recognize their format's error envelope.
|
|
41
|
+
#
|
|
42
|
+
# @param hash [Object] the raw response payload
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @raise [ErrorResponseError] if the payload is an API error envelope
|
|
45
|
+
def check_error_response!(hash)
|
|
46
|
+
return unless hash.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
message = error_response_message(hash)
|
|
49
|
+
return if message.nil? || message.empty?
|
|
50
|
+
|
|
51
|
+
raise ErrorResponseError, "the API returned an error: #{message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract a human-readable message from an API error envelope.
|
|
55
|
+
#
|
|
56
|
+
# @param _hash [Hash] the raw response payload
|
|
57
|
+
# @return [String, nil] nil when the payload is not recognized as an
|
|
58
|
+
# error envelope
|
|
59
|
+
def error_response_message(_hash)
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
35
63
|
# Ensure a response payload is a Hash containing the key that identifies
|
|
36
64
|
# it as a well-formed response for this format. Raised before parsing so
|
|
37
65
|
# malformed bodies (e.g. provider error envelopes) fail loudly with the
|
|
@@ -27,6 +27,10 @@ module PromptBuilder
|
|
|
27
27
|
# supported, and only when +stream+ is set (otherwise it is omitted)
|
|
28
28
|
#
|
|
29
29
|
# Input content restrictions:
|
|
30
|
+
# - +instructions+ is serialized as a system message inserted after the
|
|
31
|
+
# last system/developer message, or first when there are none
|
|
32
|
+
# (instructions apply to the current request, matching Open Responses
|
|
33
|
+
# semantics)
|
|
30
34
|
# - +InputVideo+ content is not supported in any message (omitted)
|
|
31
35
|
# - +Reasoning+ items are not supported (skipped)
|
|
32
36
|
# - +RefusalContent+ is dropped silently (a parsed Chat Completions
|
|
@@ -119,7 +123,8 @@ module PromptBuilder
|
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
# Session extra: recognized keys for Chat Completions API
|
|
122
|
-
|
|
126
|
+
extra = session.extra
|
|
127
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
123
128
|
|
|
124
129
|
h
|
|
125
130
|
end
|
|
@@ -138,10 +143,6 @@ module PromptBuilder
|
|
|
138
143
|
def build_messages(session)
|
|
139
144
|
messages = []
|
|
140
145
|
|
|
141
|
-
if session.instructions
|
|
142
|
-
messages << {"role" => "system", "content" => session.instructions}
|
|
143
|
-
end
|
|
144
|
-
|
|
145
146
|
pending_tool_calls = []
|
|
146
147
|
last_assistant_msg = nil
|
|
147
148
|
|
|
@@ -171,9 +172,18 @@ module PromptBuilder
|
|
|
171
172
|
end
|
|
172
173
|
|
|
173
174
|
flush_tool_calls!(messages, pending_tool_calls, last_assistant_msg)
|
|
175
|
+
insert_instructions!(messages, session.instructions) if session.instructions
|
|
174
176
|
messages
|
|
175
177
|
end
|
|
176
178
|
|
|
179
|
+
# Instructions apply to the current request (matching Open Responses
|
|
180
|
+
# semantics), so they are inserted after the last system/developer
|
|
181
|
+
# message rather than before any of them.
|
|
182
|
+
def insert_instructions!(messages, instructions)
|
|
183
|
+
index = messages.rindex { |msg| msg["role"] == "system" || msg["role"] == "developer" }
|
|
184
|
+
messages.insert(index ? index + 1 : 0, {"role" => "system", "content" => instructions})
|
|
185
|
+
end
|
|
186
|
+
|
|
177
187
|
def serialize_message(item)
|
|
178
188
|
# Messages with an unsupported role are silently skipped.
|
|
179
189
|
return nil unless SUPPORTED_MESSAGE_ROLES.include?(item.role)
|
|
@@ -198,7 +208,7 @@ module PromptBuilder
|
|
|
198
208
|
|
|
199
209
|
def serialize_content(role, content)
|
|
200
210
|
case content
|
|
201
|
-
when Content::InputText, Content::OutputText
|
|
211
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
202
212
|
serialize_text_content(content)
|
|
203
213
|
when Content::InputImage
|
|
204
214
|
# Image content is only supported in user messages; omit otherwise.
|
|
@@ -241,7 +251,7 @@ module PromptBuilder
|
|
|
241
251
|
# are silently omitted.
|
|
242
252
|
content = output.filter_map do |content|
|
|
243
253
|
case content
|
|
244
|
-
when Content::InputText, Content::OutputText
|
|
254
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
245
255
|
serialize_text_content(content)
|
|
246
256
|
end
|
|
247
257
|
end
|
|
@@ -16,6 +16,18 @@ module PromptBuilder
|
|
|
16
16
|
class << self
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
+
# OpenAI errors arrive as {"error" => {"message" => ..., "type" =>
|
|
20
|
+
# ..., "code" => ...}}; some compatible servers return a bare string
|
|
21
|
+
# under "error".
|
|
22
|
+
def error_response_message(hash)
|
|
23
|
+
error = hash["error"]
|
|
24
|
+
return nil if error.nil? || hash.key?("choices")
|
|
25
|
+
|
|
26
|
+
return error.to_s unless error.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
[error["code"] || error["type"], error["message"]].compact.join(": ")
|
|
29
|
+
end
|
|
30
|
+
|
|
19
31
|
def deserialize_response(hash)
|
|
20
32
|
validate_response!(hash)
|
|
21
33
|
|
|
@@ -39,6 +39,10 @@ module PromptBuilder
|
|
|
39
39
|
# - +tool_choice: "none"+ has no Converse representation and is omitted
|
|
40
40
|
#
|
|
41
41
|
# Input content restrictions:
|
|
42
|
+
# - System and developer messages are hoisted out of conversational order
|
|
43
|
+
# into the top-level +system+ parameter, with +instructions+ merged last
|
|
44
|
+
# (instructions apply to the current request, matching Open Responses
|
|
45
|
+
# semantics)
|
|
42
46
|
# - +Reasoning+ items are silently skipped
|
|
43
47
|
# - +RefusalContent+ is dropped silently (a parsed response refusal can
|
|
44
48
|
# stay in session history without breaking subsequent request_payload calls)
|
|
@@ -68,8 +72,11 @@ module PromptBuilder
|
|
|
68
72
|
# - Prompt management variables (+prompt_variables+)
|
|
69
73
|
#
|
|
70
74
|
# Prompt caching markers (+cachePoint+) are emitted via the +cache_point+
|
|
71
|
-
#
|
|
72
|
-
#
|
|
75
|
+
# extra on system/message content, tool definitions (appended to the
|
|
76
|
+
# +toolConfig.tools+ array), and +FunctionCallOutput+ items (appended to
|
|
77
|
+
# the message content after the +toolResult+ block). Cross-region routing
|
|
78
|
+
# via inference profiles is selected through the model id and needs no
|
|
79
|
+
# request field.
|
|
73
80
|
class Request < Base
|
|
74
81
|
IMAGE_MEDIA_TYPE_FORMATS = {
|
|
75
82
|
"image/jpeg" => "jpeg",
|
|
@@ -164,7 +171,8 @@ module PromptBuilder
|
|
|
164
171
|
h["toolConfig"] = tool_config if tool_config
|
|
165
172
|
|
|
166
173
|
# Session extra: recognized keys for Converse API
|
|
167
|
-
|
|
174
|
+
extra = session.extra
|
|
175
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
168
176
|
|
|
169
177
|
h
|
|
170
178
|
end
|
|
@@ -194,8 +202,6 @@ module PromptBuilder
|
|
|
194
202
|
def build_system(session)
|
|
195
203
|
parts = []
|
|
196
204
|
|
|
197
|
-
parts << {"text" => session.instructions} if session.instructions
|
|
198
|
-
|
|
199
205
|
session.items.each do |item|
|
|
200
206
|
next unless item.is_a?(Items::Message)
|
|
201
207
|
next unless item.role == "system" || item.role == "developer"
|
|
@@ -211,6 +217,11 @@ module PromptBuilder
|
|
|
211
217
|
end
|
|
212
218
|
end
|
|
213
219
|
|
|
220
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
221
|
+
# the current request, so they must follow any system messages
|
|
222
|
+
# accumulated in the conversation history.
|
|
223
|
+
parts << {"text" => session.instructions} if session.instructions
|
|
224
|
+
|
|
214
225
|
parts
|
|
215
226
|
end
|
|
216
227
|
|
|
@@ -249,10 +260,14 @@ module PromptBuilder
|
|
|
249
260
|
}]
|
|
250
261
|
}
|
|
251
262
|
when Items::FunctionCallOutput
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
# The Converse ToolResultContentBlock union has no cachePoint
|
|
264
|
+
# member; the marker must be a sibling content block after the
|
|
265
|
+
# toolResult block.
|
|
266
|
+
content = [serialize_tool_result(item, ctx)]
|
|
267
|
+
if item.extra && item.extra["cache_point"]
|
|
268
|
+
content << {"cachePoint" => {"type" => "default"}}
|
|
269
|
+
end
|
|
270
|
+
raw_messages << {"role" => "user", "content" => content}
|
|
256
271
|
when Items::Reasoning, Items::Compaction, Items::ItemReference
|
|
257
272
|
# Reasoning, Compaction, and ItemReference items are not supported
|
|
258
273
|
# in the request, so ignore them rather than raising an error.
|
|
@@ -265,7 +280,7 @@ module PromptBuilder
|
|
|
265
280
|
|
|
266
281
|
def serialize_system_content(content)
|
|
267
282
|
case content
|
|
268
|
-
when Content::InputText
|
|
283
|
+
when Content::InputText, Content::Text
|
|
269
284
|
{"text" => content.text}
|
|
270
285
|
when Content::OutputText
|
|
271
286
|
validate_output_text!(content)
|
|
@@ -293,7 +308,7 @@ module PromptBuilder
|
|
|
293
308
|
|
|
294
309
|
def serialize_content(content, role:, ctx: nil)
|
|
295
310
|
case content
|
|
296
|
-
when Content::InputText, Content::OutputText
|
|
311
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
297
312
|
validate_output_text!(content) if content.is_a?(Content::OutputText)
|
|
298
313
|
{"text" => content.text}
|
|
299
314
|
when Content::InputImage
|
|
@@ -512,7 +527,7 @@ module PromptBuilder
|
|
|
512
527
|
|
|
513
528
|
def serialize_tool_result_content(content, ctx)
|
|
514
529
|
case content
|
|
515
|
-
when Content::InputText, Content::OutputText
|
|
530
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
516
531
|
validate_output_text!(content) if content.is_a?(Content::OutputText)
|
|
517
532
|
{"text" => content.text}
|
|
518
533
|
when Content::InputImage
|
|
@@ -610,13 +625,17 @@ module PromptBuilder
|
|
|
610
625
|
end
|
|
611
626
|
|
|
612
627
|
def build_tools(session)
|
|
613
|
-
session.tool_definitions.
|
|
628
|
+
session.tool_definitions.flat_map do |definition|
|
|
614
629
|
tool_spec = {"name" => definition.name}
|
|
615
630
|
tool_spec["description"] = definition.description if definition.description
|
|
616
631
|
tool_spec["inputSchema"] = {
|
|
617
632
|
"json" => definition.parameters || {"type" => "object", "properties" => {}}
|
|
618
633
|
}
|
|
619
|
-
{"toolSpec" => tool_spec}
|
|
634
|
+
tools = [{"toolSpec" => tool_spec}]
|
|
635
|
+
# The Converse Tool union type has a cachePoint member, so the
|
|
636
|
+
# marker is its own entry in the tools array.
|
|
637
|
+
tools << {"cachePoint" => {"type" => "default"}} if definition.extra["cache_point"]
|
|
638
|
+
tools
|
|
620
639
|
end
|
|
621
640
|
end
|
|
622
641
|
|
|
@@ -10,6 +10,21 @@ module PromptBuilder
|
|
|
10
10
|
class << self
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
+
# Bedrock exception bodies carry a top-level "message" (the exception
|
|
14
|
+
# class travels in the x-amzn-ErrorType header) or a top-level
|
|
15
|
+
# "__type", while misrouted requests get a Coral service envelope
|
|
16
|
+
# like {"Output" => {"__type" => "..."}, "Version" => "1.0"}.
|
|
17
|
+
def error_response_message(hash)
|
|
18
|
+
return nil if hash.key?("output")
|
|
19
|
+
|
|
20
|
+
coral_output = hash["Output"].is_a?(Hash) ? hash["Output"] : {}
|
|
21
|
+
error_type = hash["__type"] || coral_output["__type"]
|
|
22
|
+
message = hash["message"] || hash["Message"] || coral_output["message"] || coral_output["Message"]
|
|
23
|
+
return nil unless error_type || message
|
|
24
|
+
|
|
25
|
+
[error_type, message].compact.join(": ")
|
|
26
|
+
end
|
|
27
|
+
|
|
13
28
|
def deserialize_response(hash)
|
|
14
29
|
require_response_key!(hash, "output")
|
|
15
30
|
require_response_key!(hash, "stopReason")
|
|
@@ -27,6 +27,12 @@ module PromptBuilder
|
|
|
27
27
|
# - +truncation+ — server-side context truncation is not supported
|
|
28
28
|
#
|
|
29
29
|
# Input content restrictions:
|
|
30
|
+
# - System and developer messages are hoisted out of conversational order
|
|
31
|
+
# into the top-level +systemInstruction+ field, with +instructions+ merged
|
|
32
|
+
# last (instructions apply to the current request, matching Open Responses
|
|
33
|
+
# semantics)
|
|
34
|
+
# - Only text content (+InputText+/+OutputText+/+Text+) survives in system
|
|
35
|
+
# and developer messages; other content is silently omitted
|
|
30
36
|
# - +InputImage+ content is only supported in user messages (assistant images are omitted)
|
|
31
37
|
# - +InputImage+ with +image_url+ requires either base64 +data+ or a URL;
|
|
32
38
|
# content without +image_url+ or +file_id+ raises
|
|
@@ -136,7 +142,8 @@ module PromptBuilder
|
|
|
136
142
|
h["toolConfig"] = tool_config if tool_config
|
|
137
143
|
|
|
138
144
|
# Session extra: recognized keys for Gemini API
|
|
139
|
-
|
|
145
|
+
extra = session.extra
|
|
146
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
140
147
|
|
|
141
148
|
# Gemini selects streaming via endpoint (:streamGenerateContent)
|
|
142
149
|
# rather than a request body field, so session.stream is a no-op
|
|
@@ -162,19 +169,24 @@ module PromptBuilder
|
|
|
162
169
|
def build_system_instruction(session)
|
|
163
170
|
parts = []
|
|
164
171
|
|
|
165
|
-
if session.instructions
|
|
166
|
-
parts << {"text" => session.instructions}
|
|
167
|
-
end
|
|
168
|
-
|
|
169
172
|
session.items.each do |item|
|
|
170
173
|
next unless item.is_a?(Items::Message)
|
|
171
174
|
next unless item.role == "system" || item.role == "developer"
|
|
172
175
|
|
|
173
176
|
item.content.each do |content|
|
|
174
|
-
|
|
177
|
+
if content.is_a?(Content::InputText) || content.is_a?(Content::OutputText) || content.is_a?(Content::Text)
|
|
178
|
+
parts << {"text" => content.text}
|
|
179
|
+
end
|
|
175
180
|
end
|
|
176
181
|
end
|
|
177
182
|
|
|
183
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
184
|
+
# the current request, so they must follow any system messages
|
|
185
|
+
# accumulated in the conversation history.
|
|
186
|
+
if session.instructions
|
|
187
|
+
parts << {"text" => session.instructions}
|
|
188
|
+
end
|
|
189
|
+
|
|
178
190
|
return nil if parts.empty?
|
|
179
191
|
|
|
180
192
|
{"parts" => parts}
|
|
@@ -274,7 +286,7 @@ module PromptBuilder
|
|
|
274
286
|
# types are silently omitted.
|
|
275
287
|
text = output.filter_map do |content|
|
|
276
288
|
case content
|
|
277
|
-
when Content::InputText, Content::OutputText
|
|
289
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
278
290
|
content.text
|
|
279
291
|
end
|
|
280
292
|
end.join("\n")
|
|
@@ -307,7 +319,7 @@ module PromptBuilder
|
|
|
307
319
|
|
|
308
320
|
def serialize_content(content, role:)
|
|
309
321
|
case content
|
|
310
|
-
when Content::InputText, Content::OutputText
|
|
322
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
311
323
|
part = {"text" => content.text}
|
|
312
324
|
if content.extra && content.extra["thought_signature"]
|
|
313
325
|
part["thoughtSignature"] = content.extra["thought_signature"]
|
|
@@ -63,6 +63,15 @@ module PromptBuilder
|
|
|
63
63
|
class << self
|
|
64
64
|
private
|
|
65
65
|
|
|
66
|
+
# Gemini errors arrive as
|
|
67
|
+
# {"error" => {"code" => 400, "message" => ..., "status" => ...}}.
|
|
68
|
+
def error_response_message(hash)
|
|
69
|
+
error = hash["error"]
|
|
70
|
+
return nil unless error.is_a?(Hash) && !hash.key?("candidates")
|
|
71
|
+
|
|
72
|
+
[error["status"] || error["code"], error["message"]].compact.join(": ")
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
def deserialize_response(hash)
|
|
67
76
|
require_response_key!(hash, "candidates")
|
|
68
77
|
require_response_key!(hash, "modelVersion")
|
|
@@ -36,6 +36,12 @@ module PromptBuilder
|
|
|
36
36
|
# is enabled
|
|
37
37
|
#
|
|
38
38
|
# Input content restrictions:
|
|
39
|
+
# - System and developer messages are hoisted out of conversational order
|
|
40
|
+
# into the top-level +system+ parameter, with +instructions+ merged last
|
|
41
|
+
# (instructions apply to the current request, matching Open Responses
|
|
42
|
+
# semantics)
|
|
43
|
+
# - Only text content (+InputText+/+OutputText+/+Text+) survives in system
|
|
44
|
+
# and developer messages; other content is silently omitted
|
|
39
45
|
# - +InputVideo+ content is not supported and is omitted
|
|
40
46
|
# - +RefusalContent+ is dropped silently (a parsed refusal can stay in
|
|
41
47
|
# session history without breaking subsequent request_payload calls)
|
|
@@ -115,7 +121,8 @@ module PromptBuilder
|
|
|
115
121
|
h["stream"] = session.stream unless session.stream.nil?
|
|
116
122
|
|
|
117
123
|
# Session extra: recognized keys for Messages API
|
|
118
|
-
|
|
124
|
+
extra = session.extra
|
|
125
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
119
126
|
|
|
120
127
|
output_config = build_output_config(session)
|
|
121
128
|
h["output_config"] = output_config unless output_config.empty?
|
|
@@ -279,16 +286,12 @@ module PromptBuilder
|
|
|
279
286
|
def build_system(session)
|
|
280
287
|
parts = []
|
|
281
288
|
|
|
282
|
-
if session.instructions
|
|
283
|
-
parts << {"type" => "text", "text" => session.instructions}
|
|
284
|
-
end
|
|
285
|
-
|
|
286
289
|
session.items.each do |item|
|
|
287
290
|
next unless item.is_a?(Items::Message)
|
|
288
291
|
next unless item.role == "system" || item.role == "developer"
|
|
289
292
|
|
|
290
293
|
item.content.each do |content|
|
|
291
|
-
if content.is_a?(Content::InputText)
|
|
294
|
+
if content.is_a?(Content::InputText) || content.is_a?(Content::OutputText) || content.is_a?(Content::Text)
|
|
292
295
|
part = {"type" => "text", "text" => content.text}
|
|
293
296
|
if content.extra && content.extra["cache_control"]
|
|
294
297
|
part["cache_control"] = content.extra["cache_control"]
|
|
@@ -298,6 +301,13 @@ module PromptBuilder
|
|
|
298
301
|
end
|
|
299
302
|
end
|
|
300
303
|
|
|
304
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
305
|
+
# the current request, so they must follow any system messages
|
|
306
|
+
# accumulated in the conversation history.
|
|
307
|
+
if session.instructions
|
|
308
|
+
parts << {"type" => "text", "text" => session.instructions}
|
|
309
|
+
end
|
|
310
|
+
|
|
301
311
|
parts
|
|
302
312
|
end
|
|
303
313
|
|
|
@@ -363,7 +373,7 @@ module PromptBuilder
|
|
|
363
373
|
|
|
364
374
|
def serialize_content_block(content, role:)
|
|
365
375
|
case content
|
|
366
|
-
when Content::InputText
|
|
376
|
+
when Content::InputText, Content::Text
|
|
367
377
|
{"type" => "text", "text" => content.text}
|
|
368
378
|
when Content::OutputText
|
|
369
379
|
text = {"type" => "text", "text" => content.text}
|
|
@@ -492,7 +502,7 @@ module PromptBuilder
|
|
|
492
502
|
# Document blocks are rejected by the API.
|
|
493
503
|
def serialize_tool_result_content(content)
|
|
494
504
|
case content
|
|
495
|
-
when Content::InputText, Content::OutputText
|
|
505
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
496
506
|
{"type" => "text", "text" => content.text}
|
|
497
507
|
when Content::InputImage
|
|
498
508
|
serialize_content(content, role: "user")
|
|
@@ -10,6 +10,15 @@ module PromptBuilder
|
|
|
10
10
|
class << self
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
+
# Anthropic errors arrive as
|
|
14
|
+
# {"type" => "error", "error" => {"type" => ..., "message" => ...}}.
|
|
15
|
+
def error_response_message(hash)
|
|
16
|
+
error = hash["error"]
|
|
17
|
+
return nil unless error.is_a?(Hash) && !hash.key?("content")
|
|
18
|
+
|
|
19
|
+
[error["type"], error["message"]].compact.join(": ")
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
def deserialize_response(hash)
|
|
14
23
|
require_response_key!(hash, "content")
|
|
15
24
|
require_response_key!(hash, "stop_reason")
|
|
@@ -28,6 +28,7 @@ module PromptBuilder
|
|
|
28
28
|
payload = session.to_h
|
|
29
29
|
items = apply_server_state!(payload, session)
|
|
30
30
|
payload.delete("extra")
|
|
31
|
+
payload.delete("response_boundary_index")
|
|
31
32
|
strip_extra(payload, items, session.tool_definitions)
|
|
32
33
|
normalize_content_urls!(payload)
|
|
33
34
|
strip_non_replayable_reasoning!(payload)
|
|
@@ -8,6 +8,16 @@ module PromptBuilder
|
|
|
8
8
|
class << self
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
# Open Responses request errors arrive as a bare {"error" => {...}}
|
|
12
|
+
# envelope. A failed response object also carries an "error" field,
|
|
13
|
+
# but it has a "status" and parses into a failed Response instead.
|
|
14
|
+
def error_response_message(hash)
|
|
15
|
+
error = hash["error"]
|
|
16
|
+
return nil unless error.is_a?(Hash) && !hash.key?("status")
|
|
17
|
+
|
|
18
|
+
[error["code"] || error["type"], error["message"]].compact.join(": ")
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
def deserialize_response(hash)
|
|
12
22
|
require_response_key!(hash, "status")
|
|
13
23
|
require_response_key!(hash, "object")
|
|
@@ -34,11 +34,17 @@ module PromptBuilder
|
|
|
34
34
|
private_constant :THINK_EFFORT_LEVELS
|
|
35
35
|
|
|
36
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
|
|
37
45
|
INITIALIZE_OPTIONS = (
|
|
38
|
-
STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS +
|
|
39
|
-
%i[input extra]
|
|
46
|
+
STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS + %i[input extra system]
|
|
40
47
|
).freeze
|
|
41
|
-
private_constant :INITIALIZE_OPTIONS
|
|
42
48
|
|
|
43
49
|
# @!attribute [rw] model
|
|
44
50
|
# @return [String, nil] the model identifier
|
|
@@ -122,10 +128,44 @@ module PromptBuilder
|
|
|
122
128
|
# @return [Integer] the index in +items+ marking the boundary after the last response
|
|
123
129
|
attr_reader :response_boundary_index
|
|
124
130
|
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
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
|
|
129
169
|
|
|
130
170
|
class << self
|
|
131
171
|
# Deserialize a Session from a Hash produced by +to_h+ or parsed JSON.
|
|
@@ -138,7 +178,7 @@ module PromptBuilder
|
|
|
138
178
|
def from_h(hash)
|
|
139
179
|
attrs = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS)
|
|
140
180
|
.each_with_object({}) { |f, acc| acc[f] = hash[f.to_s] }
|
|
141
|
-
attrs[:extra] = hash["extra"]
|
|
181
|
+
attrs[:extra] = hash["extra"]
|
|
142
182
|
session = new(**attrs)
|
|
143
183
|
|
|
144
184
|
Array(hash["input"]).each do |item_hash|
|
|
@@ -151,6 +191,8 @@ module PromptBuilder
|
|
|
151
191
|
session.register_tool(defn.name, description: defn.description, parameters: defn.parameters, strict: defn.strict, **extra)
|
|
152
192
|
end
|
|
153
193
|
|
|
194
|
+
session.response_boundary_index = hash["response_boundary_index"] if hash["response_boundary_index"]
|
|
195
|
+
|
|
154
196
|
session
|
|
155
197
|
end
|
|
156
198
|
end
|
|
@@ -158,16 +200,18 @@ module PromptBuilder
|
|
|
158
200
|
# Create a new Session with the given options.
|
|
159
201
|
# Accepts keyword arguments for all typed field groups (STRING_FIELDS,
|
|
160
202
|
# FLOAT_FIELDS, INTEGER_FIELDS, BOOLEAN_FIELDS, JSONIFY_FIELDS); all default
|
|
161
|
-
# to +nil+. The +
|
|
162
|
-
# if provided. Unsupported keyword options raise an ArgumentError.
|
|
203
|
+
# to +nil+. The +system+ and +input+ shorthands auto-create a system and
|
|
204
|
+
# user message if provided. Unsupported keyword options raise an ArgumentError.
|
|
163
205
|
#
|
|
164
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
|
|
165
209
|
# @option attributes [String, nil] :input optional string shorthand; a user
|
|
166
210
|
# message is automatically added with this text
|
|
167
211
|
# @option attributes [Hash, nil] :extra provider-specific extra data for
|
|
168
|
-
# serializers; recognized keys vary by target format
|
|
169
|
-
#
|
|
170
|
-
#
|
|
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
|
|
171
215
|
def initialize(**attributes)
|
|
172
216
|
unsupported = attributes.keys - INITIALIZE_OPTIONS
|
|
173
217
|
unless unsupported.empty?
|
|
@@ -177,8 +221,12 @@ module PromptBuilder
|
|
|
177
221
|
(STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS).each do |f|
|
|
178
222
|
send(:"#{f}=", attributes[f])
|
|
179
223
|
end
|
|
180
|
-
|
|
224
|
+
|
|
225
|
+
self.extra = attributes[:extra]
|
|
226
|
+
|
|
181
227
|
@items = []
|
|
228
|
+
system(attributes[:system]) if attributes[:system]
|
|
229
|
+
|
|
182
230
|
@tool_definitions = {}
|
|
183
231
|
@response_boundary_index = 0
|
|
184
232
|
user(attributes[:input]) if attributes[:input]
|
|
@@ -192,6 +240,7 @@ module PromptBuilder
|
|
|
192
240
|
# session.user("Hello, how are you?")
|
|
193
241
|
# session.user(Content::InputText.new(text: "Hello, how are you?"))
|
|
194
242
|
# session.user(type: "input_text", text: "Hello, how are you?")
|
|
243
|
+
# session.user(text: "Hello, how are you?") # type defaults to "input_text"
|
|
195
244
|
# session.user([
|
|
196
245
|
# Content::InputText.new(text: "What is in this image?"),
|
|
197
246
|
# Content::InputImage.new(url: "http://example.com/image.png")
|
|
@@ -216,6 +265,10 @@ module PromptBuilder
|
|
|
216
265
|
#
|
|
217
266
|
# @param content [String, Content::Base, Hash, Array<Content::Base>, Array<Hash>] the message content
|
|
218
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"})
|
|
219
272
|
def system(content)
|
|
220
273
|
add_item(Items::Message.new(role: "system", content: content))
|
|
221
274
|
end
|
|
@@ -268,6 +321,19 @@ module PromptBuilder
|
|
|
268
321
|
@response_boundary_index = @items.length
|
|
269
322
|
end
|
|
270
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
|
+
|
|
271
337
|
# Register a tool on this session.
|
|
272
338
|
#
|
|
273
339
|
# @param name [String] the tool name
|
|
@@ -284,7 +350,7 @@ module PromptBuilder
|
|
|
284
350
|
strict: strict,
|
|
285
351
|
**extra
|
|
286
352
|
)
|
|
287
|
-
@tool_definitions[name] = definition
|
|
353
|
+
@tool_definitions[name.to_s] = definition
|
|
288
354
|
definition
|
|
289
355
|
end
|
|
290
356
|
|
|
@@ -346,6 +412,31 @@ module PromptBuilder
|
|
|
346
412
|
end
|
|
347
413
|
end
|
|
348
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
|
+
|
|
349
440
|
# Configure JSON Schema structured output. Writes the canonical
|
|
350
441
|
# +text.format+ wire hash consumed by all serializers, preserving any
|
|
351
442
|
# other +text+ keys (e.g. +verbosity+) already set.
|
|
@@ -465,6 +556,7 @@ module PromptBuilder
|
|
|
465
556
|
|
|
466
557
|
h["input"] = @items.map(&:to_h) unless @items.empty?
|
|
467
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?
|
|
468
560
|
|
|
469
561
|
h["tools"] = tool_definitions.map(&:to_h) unless @tool_definitions.empty?
|
|
470
562
|
|
|
@@ -488,7 +580,7 @@ module PromptBuilder
|
|
|
488
580
|
val = send(f)
|
|
489
581
|
h[f.to_s] = val if val
|
|
490
582
|
}
|
|
491
|
-
h["extra"] =
|
|
583
|
+
h["extra"] = extra unless @extra.empty?
|
|
492
584
|
|
|
493
585
|
h
|
|
494
586
|
end
|
|
@@ -509,7 +601,7 @@ module PromptBuilder
|
|
|
509
601
|
def config_hash
|
|
510
602
|
h = (STRING_FIELDS + FLOAT_FIELDS + INTEGER_FIELDS + BOOLEAN_FIELDS + JSONIFY_FIELDS - %i[previous_response_id])
|
|
511
603
|
.each_with_object({}) { |f, acc| acc[f] = send(f) }
|
|
512
|
-
h[:extra] = @extra
|
|
604
|
+
h[:extra] = @extra
|
|
513
605
|
h
|
|
514
606
|
end
|
|
515
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: []
|