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
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,51 @@ 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
|
+
|
|
33
|
+
## 0.2.0
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `Session#use_tools` copies tool definitions from a `ToolRegistry` (the global registry by default) onto the session by name, raising a `ToolNotFoundError` for unknown names.
|
|
38
|
+
- `Session#json_output` configures JSON Schema structured output without writing the raw `text.format` wire hash by hand.
|
|
39
|
+
- `Session#think` configures reasoning portably across serializers via `effort:` or `budget_tokens:`; `think(false)` clears the configuration.
|
|
40
|
+
- `Response#parsed_json` parses the response text as JSON (stripping fenced ```json wrappers), returning `nil` when it cannot be parsed; `Response#parsed_json!` raises a `ParseError` including the raw text instead.
|
|
41
|
+
- `Response.from_text` synthesizes a completed assistant text response for canned answers, cached responses, and tests.
|
|
42
|
+
- `PromptBuilder::ParseError` error class.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `Session.new` raises an `ArgumentError` when passed an unsupported keyword option instead of silently ignoring it.
|
|
47
|
+
- The Messages serializer raises an `UnsupportedFormatError` when `session.max_output_tokens` is not set, since the Messages API requires the `max_tokens` parameter.
|
|
48
|
+
- Serializers now raise an `UnsupportedFormatError` for missing fields their target API requires instead of emitting an invalid payload: Chat Completions, Converse, and Open Responses require `session.model`; Chat Completions requires at least one message; Gemini requires a non-empty `contents` array.
|
|
49
|
+
- The Messages serializer raises an `UnsupportedFormatError` when thinking is enabled with a `budget_tokens` that is not less than `max_output_tokens`, matching the Anthropic API requirement that `max_tokens` be greater than the thinking budget.
|
|
50
|
+
- The Converse serializer warns once per process when `session.reasoning` is set instead of silently dropping it (the Converse endpoint has no reasoning parameter).
|
|
51
|
+
|
|
7
52
|
## 0.1.2
|
|
8
53
|
|
|
9
54
|
### Fixed
|
data/README.md
CHANGED
|
@@ -48,15 +48,29 @@ 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
|
|
|
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)).
|
|
73
|
+
|
|
60
74
|
### Conversation History
|
|
61
75
|
|
|
62
76
|
Build up a multi-turn conversation by adding messages:
|
|
@@ -69,7 +83,13 @@ session.assistant("Hi there! How can I help you today?")
|
|
|
69
83
|
session.user("What's the weather like?")
|
|
70
84
|
```
|
|
71
85
|
|
|
72
|
-
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
|
+
```
|
|
73
93
|
|
|
74
94
|
### Serializing Requests
|
|
75
95
|
|
|
@@ -160,6 +180,59 @@ response.has_tool_calls? # => false
|
|
|
160
180
|
response.usage # => #<PromptBuilder::Usage input_tokens=25 output_tokens=12 ...>
|
|
161
181
|
```
|
|
162
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
|
+
|
|
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:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
response = PromptBuilder::Response.from_text("Authentication failed.",
|
|
194
|
+
model: llm_response.model, usage: llm_response.usage)
|
|
195
|
+
response.text # => "Authentication failed."
|
|
196
|
+
response.completed? # => true
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Structured Output
|
|
200
|
+
|
|
201
|
+
Use `json_output` to request JSON Schema structured output and `parsed_json` to read it back:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
schema = {
|
|
205
|
+
"type" => "object",
|
|
206
|
+
"properties" => {"answer" => {"type" => "string"}},
|
|
207
|
+
"required" => ["answer"]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
session.json_output(schema, name: "response", strict: true)
|
|
211
|
+
payload = session.request_payload(:messages) # works with all five serializers
|
|
212
|
+
|
|
213
|
+
# After parsing the API response:
|
|
214
|
+
data = response.parsed_json # => {"answer" => "..."} or nil if the text isn't valid JSON
|
|
215
|
+
data = response.parsed_json! # raises PromptBuilder::ParseError (including the raw text) instead
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`parsed_json` strips fenced ```` ```json ```` wrappers, a common provider quirk. `json_output` is sugar for setting `session.text` to the canonical `format` wire hash, so direct `session.text = {...}` assignment keeps working.
|
|
219
|
+
|
|
220
|
+
### Reasoning / Extended Thinking
|
|
221
|
+
|
|
222
|
+
Use `think` to configure reasoning portably; each serializer maps it to its native parameter:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
session.think(effort: :medium) # portable across serializers
|
|
226
|
+
session.think(budget_tokens: 8_000) # explicit token budget where supported
|
|
227
|
+
session.think(false) # clear the reasoning configuration
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `effort:` (one of `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) maps to `output_config.effort` (Messages), `reasoning_effort` (Chat Completions), `thinkingConfig.thinkingLevel` (Gemini), and `reasoning.effort` (Open Responses). Levels a target API doesn't accept are omitted.
|
|
231
|
+
- `budget_tokens:` maps to `thinking.budget_tokens` (Messages) and `thinkingConfig.thinkingBudget` (Gemini); effort-based APIs ignore it. The Messages serializer raises an `UnsupportedFormatError` at `request_payload` time when `max_output_tokens` is not greater than the budget, since the Anthropic API requires `max_tokens > budget_tokens`.
|
|
232
|
+
- The Converse API has no reasoning parameter; the serializer warns once per process and omits the configuration.
|
|
233
|
+
|
|
234
|
+
Direct `session.reasoning = {...}` assignment keeps working for provider-specific keys (e.g. Anthropic's `display`, Gemini's `summary`).
|
|
235
|
+
|
|
163
236
|
### Agentic Tool Loops
|
|
164
237
|
|
|
165
238
|
You can register tool definitions on a session, add API responses to the conversation, and manually append tool outputs to build an agentic loop:
|
|
@@ -240,6 +313,23 @@ end
|
|
|
240
313
|
session.register_tools(PromptBuilder.tool_registry)
|
|
241
314
|
```
|
|
242
315
|
|
|
316
|
+
Use `use_tools` to attach a subset of registry tools by name without copying schemas by hand. Unknown names raise a `ToolNotFoundError`, so a typo fails fast instead of producing a silently absent tool:
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
session.use_tools("weather", "traffic_conditions") # from PromptBuilder.tool_registry
|
|
320
|
+
session.use_tools # all tools in the registry
|
|
321
|
+
session.use_tools(:weather, registry: my_registry) # explicit registry
|
|
322
|
+
```
|
|
323
|
+
|
|
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.
|
|
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
|
+
|
|
243
333
|
### Content Types
|
|
244
334
|
|
|
245
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.
|
|
@@ -360,6 +450,8 @@ session = PromptBuilder::Session.new(
|
|
|
360
450
|
|
|
361
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.
|
|
362
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
|
+
|
|
363
455
|
#### Session Extra
|
|
364
456
|
|
|
365
457
|
Pass provider-specific top-level request parameters via `session.extra`:
|
|
@@ -413,6 +505,26 @@ session = PromptBuilder::Session.new(
|
|
|
413
505
|
)
|
|
414
506
|
```
|
|
415
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
|
+
|
|
416
528
|
#### Content Extra
|
|
417
529
|
|
|
418
530
|
Content blocks support provider-specific attributes via keyword arguments that are captured in the `extra` hash:
|
|
@@ -442,6 +554,21 @@ session.user([
|
|
|
442
554
|
cache_point: true
|
|
443
555
|
)
|
|
444
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
|
+
)
|
|
445
572
|
```
|
|
446
573
|
|
|
447
574
|
#### Tool Definition Extra
|
|
@@ -460,12 +587,14 @@ session.register_tool(
|
|
|
460
587
|
|
|
461
588
|
#### Recognized Extra Keys by Serializer
|
|
462
589
|
|
|
590
|
+
Session extra keys can be supplied to the constructor or assigned later with `session.extra=`.
|
|
591
|
+
|
|
463
592
|
| Serializer | Session Extra Keys | Content Extra Keys | Tool Extra Keys |
|
|
464
593
|
|:---|:---|:---|:---|
|
|
465
594
|
| **Chat Completions** | `stop`, `seed`, `logit_bias`, `n`, `prediction`, `web_search_options`, `modalities`, `audio` | `file_id`, `media_type` | — |
|
|
466
595
|
| **Messages** | `top_k`, `stop_sequences`, `cache_control` | `cache_control`, `citations`, `file_id`, `media_type` | `cache_control` |
|
|
467
596
|
| **Gemini** | `safety_settings`, `cached_content`, `stop_sequences`, `top_k`, `seed`, `candidate_count`, `response_modalities`, `media_resolution` | `file_id`, `media_type`, `thought_signature` | — |
|
|
468
|
-
| **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` |
|
|
469
598
|
|
|
470
599
|
### Serialization and Persistence
|
|
471
600
|
|
|
@@ -481,6 +610,9 @@ restored_session = PromptBuilder::Session.from_h(hash)
|
|
|
481
610
|
|
|
482
611
|
This makes it straightforward to persist conversation state in a database or cache between requests.
|
|
483
612
|
|
|
613
|
+
> [!WARNING]
|
|
614
|
+
> `to_h` is the persistence format, not a request payload. It retains provider-specific `extra` data and non-replayable items that APIs reject. Send the output of `request_payload(serializer)` to APIs, and use `to_h`/`from_h` only for storage.
|
|
615
|
+
|
|
484
616
|
## Serializer Compatibility
|
|
485
617
|
|
|
486
618
|
The Open Responses format is the canonical data model for this gem. When serializing to other formats, some features may not be available because either the target API does not support them or because the Open Responses format does not expose parameters unique to the target API. If you attempt to use a feature that is not supported by a particular serializer, it will be silently omitted from the serialized output.
|
|
@@ -601,6 +733,8 @@ For Messages specifically:
|
|
|
601
733
|
|
|
602
734
|
### Chat Completions-specific notes
|
|
603
735
|
|
|
736
|
+
Serializing a session without `model` set, or one that produces no messages at all, raises a `PromptBuilder::UnsupportedFormatError`.
|
|
737
|
+
|
|
604
738
|
Request-side mappings worth calling out:
|
|
605
739
|
|
|
606
740
|
| Canonical field / value | Chat Completions mapping |
|
|
@@ -631,6 +765,8 @@ Response-side behavior and limitations:
|
|
|
631
765
|
|
|
632
766
|
### Anthropic Messages-specific mappings
|
|
633
767
|
|
|
768
|
+
The Messages API requires the `max_tokens` parameter, which is populated from `session.max_output_tokens`. Serializing a session without `max_output_tokens` set raises a `PromptBuilder::UnsupportedFormatError`.
|
|
769
|
+
|
|
634
770
|
A few features map between the canonical Open Responses format and the Messages API in non-obvious ways:
|
|
635
771
|
|
|
636
772
|
| Canonical field / value | Messages mapping |
|
|
@@ -654,6 +790,8 @@ Built-in tool response content blocks (`server_tool_use`, `web_search_tool_resul
|
|
|
654
790
|
|
|
655
791
|
### Gemini-specific notes
|
|
656
792
|
|
|
793
|
+
Serializing a session without `model` set (the model belongs in the request URL, but it is validated at serialization time), or one that produces an empty `contents` array, raises a `PromptBuilder::UnsupportedFormatError`.
|
|
794
|
+
|
|
657
795
|
Request-side mappings worth calling out:
|
|
658
796
|
|
|
659
797
|
| Canonical field / value | Gemini mapping |
|
|
@@ -697,6 +835,8 @@ Response-side limitations:
|
|
|
697
835
|
|
|
698
836
|
### Converse-specific notes
|
|
699
837
|
|
|
838
|
+
Serializing a session without `model` set (populates the required `modelId` parameter) raises a `PromptBuilder::UnsupportedFormatError`.
|
|
839
|
+
|
|
700
840
|
Request-side restrictions worth calling out:
|
|
701
841
|
|
|
702
842
|
| Canonical field / value | Converse mapping |
|
|
@@ -725,7 +865,7 @@ Content and message restrictions:
|
|
|
725
865
|
- `FunctionCall.arguments` must parse to a JSON object; non-object JSON values raise (Bedrock's `toolUse.input` requires an object).
|
|
726
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.
|
|
727
867
|
- `tool_choice: "none"` and `tool_choice` without registered tools are both omitted.
|
|
728
|
-
- 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.
|
|
729
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.
|
|
730
870
|
|
|
731
871
|
Response-side limitations:
|
|
@@ -733,7 +873,7 @@ Response-side limitations:
|
|
|
733
873
|
- Unknown content block keys (e.g. `citationsContent`, `guardContent`) are silently skipped.
|
|
734
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`.
|
|
735
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`).
|
|
736
|
-
- `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.
|
|
737
877
|
|
|
738
878
|
## Installation
|
|
739
879
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
|
@@ -18,4 +18,12 @@ module PromptBuilder
|
|
|
18
18
|
|
|
19
19
|
# Raised when a response payload does not match the expected shape.
|
|
20
20
|
class UnexpectedPayloadError < Error; end
|
|
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
|
+
|
|
27
|
+
# Raised by Response#parsed_json! when the response text is not valid JSON.
|
|
28
|
+
class ParseError < Error; end
|
|
21
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}"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module PromptBuilder
|
|
4
6
|
# Represents a parsed API response from the Open Responses API.
|
|
5
7
|
# All fields are optional and will be nil if not present in the response.
|
|
@@ -173,6 +175,23 @@ module PromptBuilder
|
|
|
173
175
|
attrs = FIELDS.each_with_object({}) { |f, acc| acc[f] = hash[f.to_s] }
|
|
174
176
|
new(**attrs, text_config: hash["text"], output: output, usage: usage, extra: hash["extra"])
|
|
175
177
|
end
|
|
178
|
+
|
|
179
|
+
# Synthesize a Response containing a single assistant text message.
|
|
180
|
+
# Useful for canned answers (halt messages, cached responses) and tests
|
|
181
|
+
# without assembling Items::Message and Content::OutputText by hand.
|
|
182
|
+
#
|
|
183
|
+
# @param text [String] the assistant message text
|
|
184
|
+
# @param status [String] the response status
|
|
185
|
+
# @param attributes [Hash] any other Response attributes (e.g. +model+,
|
|
186
|
+
# +usage+, +id+)
|
|
187
|
+
# @return [Response]
|
|
188
|
+
# @example
|
|
189
|
+
# PromptBuilder::Response.from_text("Authentication failed.",
|
|
190
|
+
# model: llm_response.model, usage: llm_response.usage)
|
|
191
|
+
def from_text(text, status: "completed", **attributes)
|
|
192
|
+
message = Items::Message.new(role: "assistant", content: [Content::OutputText.new(text: text)])
|
|
193
|
+
new(status: status, output: [message], **attributes)
|
|
194
|
+
end
|
|
176
195
|
end
|
|
177
196
|
|
|
178
197
|
# Check if the response completed successfully.
|
|
@@ -224,6 +243,40 @@ module PromptBuilder
|
|
|
224
243
|
nil
|
|
225
244
|
end
|
|
226
245
|
|
|
246
|
+
# Parse the response text as JSON, for use with structured output
|
|
247
|
+
# (see +Session#json_output+). Strips a fenced ```json wrapper when
|
|
248
|
+
# present — a common provider quirk. Returns nil when the response has
|
|
249
|
+
# no text or the text is not valid JSON; use +parsed_json!+ to raise
|
|
250
|
+
# instead.
|
|
251
|
+
#
|
|
252
|
+
# @return [Hash, Array, Object, nil] the parsed JSON value, or nil
|
|
253
|
+
def parsed_json
|
|
254
|
+
raw = text
|
|
255
|
+
return nil unless raw
|
|
256
|
+
|
|
257
|
+
begin
|
|
258
|
+
JSON.parse(strip_json_fences(raw))
|
|
259
|
+
rescue JSON::ParserError
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Parse the response text as JSON, raising when it cannot be parsed.
|
|
265
|
+
#
|
|
266
|
+
# @return [Hash, Array, Object] the parsed JSON value
|
|
267
|
+
# @raise [ParseError] if the response has no text or the text is not
|
|
268
|
+
# valid JSON; the error message includes the raw text
|
|
269
|
+
def parsed_json!
|
|
270
|
+
raw = text
|
|
271
|
+
raise ParseError, "Response has no text output to parse as JSON" unless raw
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
JSON.parse(strip_json_fences(raw))
|
|
275
|
+
rescue JSON::ParserError => e
|
|
276
|
+
raise ParseError, "Response text is not valid JSON (#{e.message}); raw text: #{raw}"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
227
280
|
# Serialize to a Hash with string keys. Nil values are omitted.
|
|
228
281
|
#
|
|
229
282
|
# @return [Hash]
|
|
@@ -246,6 +299,12 @@ module PromptBuilder
|
|
|
246
299
|
|
|
247
300
|
private
|
|
248
301
|
|
|
302
|
+
def strip_json_fences(raw)
|
|
303
|
+
stripped = raw.strip
|
|
304
|
+
match = stripped.match(/\A```(?:json)?\s*\n(.*?)\n?```\z/m)
|
|
305
|
+
match ? match[1] : stripped
|
|
306
|
+
end
|
|
307
|
+
|
|
249
308
|
def coerce_field(field, value)
|
|
250
309
|
return value if value.nil?
|
|
251
310
|
|
|
@@ -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
|
|
@@ -5,6 +5,11 @@ module PromptBuilder
|
|
|
5
5
|
class ChatCompletion < Base
|
|
6
6
|
# Request serializer for the OpenAI Chat Completions API format.
|
|
7
7
|
#
|
|
8
|
+
# Required session fields (an UnsupportedFormatError is raised when missing):
|
|
9
|
+
# - +model+
|
|
10
|
+
# - at least one message (+instructions+ or a conversation item that
|
|
11
|
+
# serializes to a message)
|
|
12
|
+
#
|
|
8
13
|
# === Unsupported Open Responses features
|
|
9
14
|
#
|
|
10
15
|
# These session fields are not supported and are silently omitted from the
|
|
@@ -22,6 +27,10 @@ module PromptBuilder
|
|
|
22
27
|
# supported, and only when +stream+ is set (otherwise it is omitted)
|
|
23
28
|
#
|
|
24
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)
|
|
25
34
|
# - +InputVideo+ content is not supported in any message (omitted)
|
|
26
35
|
# - +Reasoning+ items are not supported (skipped)
|
|
27
36
|
# - +RefusalContent+ is dropped silently (a parsed Chat Completions
|
|
@@ -62,8 +71,14 @@ module PromptBuilder
|
|
|
62
71
|
|
|
63
72
|
def serialize_request(session)
|
|
64
73
|
h = {}
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
raise UnsupportedFormatError, "Chat Completions format requires session.model" unless session.model
|
|
75
|
+
|
|
76
|
+
h["model"] = session.model
|
|
77
|
+
messages = build_messages(session)
|
|
78
|
+
if messages.empty?
|
|
79
|
+
raise UnsupportedFormatError, "Chat Completions format requires at least one message"
|
|
80
|
+
end
|
|
81
|
+
h["messages"] = messages
|
|
67
82
|
h["temperature"] = session.temperature if session.temperature
|
|
68
83
|
h["top_p"] = session.top_p if session.top_p
|
|
69
84
|
h["presence_penalty"] = session.presence_penalty if session.presence_penalty
|
|
@@ -108,7 +123,8 @@ module PromptBuilder
|
|
|
108
123
|
end
|
|
109
124
|
|
|
110
125
|
# Session extra: recognized keys for Chat Completions API
|
|
111
|
-
|
|
126
|
+
extra = session.extra
|
|
127
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
112
128
|
|
|
113
129
|
h
|
|
114
130
|
end
|
|
@@ -127,10 +143,6 @@ module PromptBuilder
|
|
|
127
143
|
def build_messages(session)
|
|
128
144
|
messages = []
|
|
129
145
|
|
|
130
|
-
if session.instructions
|
|
131
|
-
messages << {"role" => "system", "content" => session.instructions}
|
|
132
|
-
end
|
|
133
|
-
|
|
134
146
|
pending_tool_calls = []
|
|
135
147
|
last_assistant_msg = nil
|
|
136
148
|
|
|
@@ -160,9 +172,18 @@ module PromptBuilder
|
|
|
160
172
|
end
|
|
161
173
|
|
|
162
174
|
flush_tool_calls!(messages, pending_tool_calls, last_assistant_msg)
|
|
175
|
+
insert_instructions!(messages, session.instructions) if session.instructions
|
|
163
176
|
messages
|
|
164
177
|
end
|
|
165
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
|
+
|
|
166
187
|
def serialize_message(item)
|
|
167
188
|
# Messages with an unsupported role are silently skipped.
|
|
168
189
|
return nil unless SUPPORTED_MESSAGE_ROLES.include?(item.role)
|
|
@@ -187,7 +208,7 @@ module PromptBuilder
|
|
|
187
208
|
|
|
188
209
|
def serialize_content(role, content)
|
|
189
210
|
case content
|
|
190
|
-
when Content::InputText, Content::OutputText
|
|
211
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
191
212
|
serialize_text_content(content)
|
|
192
213
|
when Content::InputImage
|
|
193
214
|
# Image content is only supported in user messages; omit otherwise.
|
|
@@ -230,7 +251,7 @@ module PromptBuilder
|
|
|
230
251
|
# are silently omitted.
|
|
231
252
|
content = output.filter_map do |content|
|
|
232
253
|
case content
|
|
233
|
-
when Content::InputText, Content::OutputText
|
|
254
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
234
255
|
serialize_text_content(content)
|
|
235
256
|
end
|
|
236
257
|
end
|