prompt_builder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/MIT-LICENSE +20 -0
- data/README.md +763 -0
- data/VERSION +1 -0
- data/lib/prompt_builder/content/base.rb +44 -0
- data/lib/prompt_builder/content/input_file.rb +63 -0
- data/lib/prompt_builder/content/input_image.rb +64 -0
- data/lib/prompt_builder/content/input_text.rb +42 -0
- data/lib/prompt_builder/content/input_video.rb +43 -0
- data/lib/prompt_builder/content/output_text.rb +59 -0
- data/lib/prompt_builder/content/reasoning_text.rb +42 -0
- data/lib/prompt_builder/content/refusal_content.rb +42 -0
- data/lib/prompt_builder/content/summary_text.rb +42 -0
- data/lib/prompt_builder/content/text.rb +42 -0
- data/lib/prompt_builder/content.rb +28 -0
- data/lib/prompt_builder/errors.rb +18 -0
- data/lib/prompt_builder/items/base.rb +41 -0
- data/lib/prompt_builder/items/compaction.rb +60 -0
- data/lib/prompt_builder/items/function_call.rb +97 -0
- data/lib/prompt_builder/items/function_call_output.rb +110 -0
- data/lib/prompt_builder/items/item_reference.rb +42 -0
- data/lib/prompt_builder/items/message.rb +113 -0
- data/lib/prompt_builder/items/reasoning.rb +75 -0
- data/lib/prompt_builder/items.rb +13 -0
- data/lib/prompt_builder/response.rb +257 -0
- data/lib/prompt_builder/serializers/base.rb +37 -0
- data/lib/prompt_builder/serializers/chat_completion/request.rb +389 -0
- data/lib/prompt_builder/serializers/chat_completion/response.rb +139 -0
- data/lib/prompt_builder/serializers/chat_completion.rb +30 -0
- data/lib/prompt_builder/serializers/converse/request.rb +623 -0
- data/lib/prompt_builder/serializers/converse/response.rb +140 -0
- data/lib/prompt_builder/serializers/converse.rb +30 -0
- data/lib/prompt_builder/serializers/gemini/request.rb +562 -0
- data/lib/prompt_builder/serializers/gemini/response.rb +233 -0
- data/lib/prompt_builder/serializers/gemini.rb +30 -0
- data/lib/prompt_builder/serializers/messages/request.rb +634 -0
- data/lib/prompt_builder/serializers/messages/response.rb +157 -0
- data/lib/prompt_builder/serializers/messages.rb +30 -0
- data/lib/prompt_builder/serializers/open_responses/request.rb +229 -0
- data/lib/prompt_builder/serializers/open_responses/response.rb +18 -0
- data/lib/prompt_builder/serializers/open_responses.rb +30 -0
- data/lib/prompt_builder/serializers.rb +35 -0
- data/lib/prompt_builder/session.rb +383 -0
- data/lib/prompt_builder/tool_registry.rb +75 -0
- data/lib/prompt_builder/tools/definition.rb +66 -0
- data/lib/prompt_builder/tools.rb +7 -0
- data/lib/prompt_builder/usage.rb +100 -0
- data/lib/prompt_builder.rb +86 -0
- data/prompt_builder.gemspec +41 -0
- metadata +107 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module PromptBuilder
|
|
6
|
+
module Serializers
|
|
7
|
+
class Messages < Base
|
|
8
|
+
# Response parser for the Anthropic Messages API format.
|
|
9
|
+
class Response < Base
|
|
10
|
+
class << self
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def deserialize_response(hash)
|
|
14
|
+
usage_hash = hash["usage"]
|
|
15
|
+
usage = build_usage(usage_hash) if usage_hash
|
|
16
|
+
|
|
17
|
+
PromptBuilder::Response.new(
|
|
18
|
+
id: hash["id"],
|
|
19
|
+
object: hash["type"],
|
|
20
|
+
model: hash["model"],
|
|
21
|
+
output: build_output_items(hash["content"] || []),
|
|
22
|
+
status: map_stop_reason(hash["stop_reason"]),
|
|
23
|
+
incomplete_details: build_incomplete_details(hash),
|
|
24
|
+
usage: usage
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_usage(usage_hash)
|
|
29
|
+
input_tokens_details = usage_hash["input_tokens_details"] || {}
|
|
30
|
+
cache_creation = usage_hash["cache_creation_input_tokens"]
|
|
31
|
+
cache_read = usage_hash["cache_read_input_tokens"]
|
|
32
|
+
cache_creation_breakdown = usage_hash["cache_creation"]
|
|
33
|
+
service_tier = usage_hash["service_tier"]
|
|
34
|
+
inference_geo = usage_hash["inference_geo"]
|
|
35
|
+
server_tool_use = usage_hash["server_tool_use"]
|
|
36
|
+
|
|
37
|
+
input_tokens_details = input_tokens_details.merge("cache_creation_input_tokens" => cache_creation) if cache_creation
|
|
38
|
+
input_tokens_details = input_tokens_details.merge("cached_tokens" => cache_read) if cache_read
|
|
39
|
+
input_tokens_details = input_tokens_details.merge("cache_creation" => cache_creation_breakdown) if cache_creation_breakdown
|
|
40
|
+
input_tokens_details = input_tokens_details.merge("service_tier" => service_tier) if service_tier
|
|
41
|
+
input_tokens_details = input_tokens_details.merge("inference_geo" => inference_geo) if inference_geo
|
|
42
|
+
input_tokens_details = input_tokens_details.merge("server_tool_use" => server_tool_use) if server_tool_use
|
|
43
|
+
|
|
44
|
+
# Anthropic reports input_tokens excluding cached/cache-creation tokens,
|
|
45
|
+
# which are billed and counted separately. Include them in the total.
|
|
46
|
+
total = usage_hash["input_tokens"].to_i + usage_hash["output_tokens"].to_i +
|
|
47
|
+
cache_creation.to_i + cache_read.to_i
|
|
48
|
+
|
|
49
|
+
Usage.new(
|
|
50
|
+
input_tokens: usage_hash["input_tokens"],
|
|
51
|
+
output_tokens: usage_hash["output_tokens"],
|
|
52
|
+
total_tokens: total,
|
|
53
|
+
input_tokens_details: input_tokens_details.empty? ? nil : input_tokens_details,
|
|
54
|
+
output_tokens_details: usage_hash["output_tokens_details"]
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_incomplete_details(hash)
|
|
59
|
+
details = {}
|
|
60
|
+
details["stop_sequence"] = hash["stop_sequence"] if hash["stop_sequence"]
|
|
61
|
+
details["stop_details"] = hash["stop_details"] if hash["stop_details"]
|
|
62
|
+
details["container"] = hash["container"] if hash["container"]
|
|
63
|
+
details.empty? ? nil : details
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Anthropic stop_reason values:
|
|
67
|
+
# - end_turn, tool_use, stop_sequence, pause_turn → completed
|
|
68
|
+
# - max_tokens → incomplete (truncated mid-output)
|
|
69
|
+
# - refusal → failed (model declined to respond)
|
|
70
|
+
# - anything else → pass through unchanged so callers can inspect it
|
|
71
|
+
def map_stop_reason(reason)
|
|
72
|
+
case reason
|
|
73
|
+
when "end_turn", "tool_use", "stop_sequence", "pause_turn"
|
|
74
|
+
"completed"
|
|
75
|
+
when "max_tokens"
|
|
76
|
+
"incomplete"
|
|
77
|
+
when "refusal"
|
|
78
|
+
"failed"
|
|
79
|
+
else
|
|
80
|
+
reason
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_output_items(content_blocks)
|
|
85
|
+
output = []
|
|
86
|
+
text_contents = []
|
|
87
|
+
reasoning_contents = []
|
|
88
|
+
|
|
89
|
+
content_blocks.each do |block|
|
|
90
|
+
type = block["type"]
|
|
91
|
+
case type
|
|
92
|
+
when "text"
|
|
93
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
94
|
+
text_contents << Content::OutputText.new(
|
|
95
|
+
text: block["text"],
|
|
96
|
+
annotations: block["citations"] || []
|
|
97
|
+
)
|
|
98
|
+
when "tool_use"
|
|
99
|
+
flush_text_contents!(output, text_contents)
|
|
100
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
101
|
+
output << Items::FunctionCall.new(
|
|
102
|
+
name: block["name"],
|
|
103
|
+
call_id: block["id"],
|
|
104
|
+
arguments: JSON.generate(block["input"] || {})
|
|
105
|
+
)
|
|
106
|
+
when "thinking", "redacted_thinking"
|
|
107
|
+
flush_text_contents!(output, text_contents)
|
|
108
|
+
reasoning_contents << deserialize_reasoning_block(block)
|
|
109
|
+
else
|
|
110
|
+
# Unsupported content block types (e.g. built-in tool results)
|
|
111
|
+
# are silently skipped rather than raising.
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
flush_text_contents!(output, text_contents)
|
|
117
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
118
|
+
output
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def deserialize_reasoning_block(block)
|
|
122
|
+
case block["type"]
|
|
123
|
+
when "thinking"
|
|
124
|
+
{
|
|
125
|
+
"type" => "thinking",
|
|
126
|
+
"thinking" => block.fetch("thinking", ""),
|
|
127
|
+
"signature" => block["signature"]
|
|
128
|
+
}
|
|
129
|
+
when "redacted_thinking"
|
|
130
|
+
{
|
|
131
|
+
"type" => "redacted_thinking",
|
|
132
|
+
"data" => block["data"]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def flush_text_contents!(output, text_contents)
|
|
138
|
+
return if text_contents.empty?
|
|
139
|
+
|
|
140
|
+
output << Items::Message.new(
|
|
141
|
+
role: "assistant",
|
|
142
|
+
content: text_contents.dup
|
|
143
|
+
)
|
|
144
|
+
text_contents.clear
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def flush_reasoning_contents!(output, reasoning_contents)
|
|
148
|
+
return if reasoning_contents.empty?
|
|
149
|
+
|
|
150
|
+
output << Items::Reasoning.new(content: reasoning_contents.dup)
|
|
151
|
+
reasoning_contents.clear
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for the Anthropic Messages API format.
|
|
6
|
+
# Delegates request and response handling to dedicated nested classes.
|
|
7
|
+
class Messages < Base
|
|
8
|
+
autoload :Request, File.expand_path("messages/request", __dir__)
|
|
9
|
+
autoload :Response, File.expand_path("messages/response", __dir__)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Export a session to Messages request payload.
|
|
13
|
+
#
|
|
14
|
+
# @param session [Session] the session to export
|
|
15
|
+
# @return [Hash] the serialized request payload
|
|
16
|
+
def request_payload(session)
|
|
17
|
+
Request.request_payload(session)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse a Messages response into an PromptBuilder::Response.
|
|
21
|
+
#
|
|
22
|
+
# @param hash [Hash] the response hash in Messages format
|
|
23
|
+
# @return [PromptBuilder::Response] the parsed response
|
|
24
|
+
def parse_response(hash)
|
|
25
|
+
Response.parse_response(hash)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
class OpenResponses < Base
|
|
6
|
+
# Request serializer for the OpenAI Open Responses API format.
|
|
7
|
+
class Request < Base
|
|
8
|
+
class << self
|
|
9
|
+
# Export a session to Open Responses API request payload.
|
|
10
|
+
#
|
|
11
|
+
# @param session [Session] the session to export
|
|
12
|
+
# @return [Hash] the serialized request payload
|
|
13
|
+
def request_payload(session)
|
|
14
|
+
payload = session.to_h
|
|
15
|
+
apply_server_state!(payload, session)
|
|
16
|
+
payload.delete("extra")
|
|
17
|
+
strip_extra(payload)
|
|
18
|
+
normalize_content_urls!(payload)
|
|
19
|
+
strip_non_replayable_reasoning!(payload)
|
|
20
|
+
strip_output_only_fields!(payload)
|
|
21
|
+
normalize_and_validate_input_images!(payload)
|
|
22
|
+
normalize_text_format!(payload)
|
|
23
|
+
payload
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# When the session is in server-state mode, replace the full input
|
|
29
|
+
# array with only items added after the last response boundary and
|
|
30
|
+
# include previous_response_id for the API to resolve history.
|
|
31
|
+
def apply_server_state!(payload, session)
|
|
32
|
+
return if session.local_state?
|
|
33
|
+
|
|
34
|
+
payload.delete("input")
|
|
35
|
+
new_items = session.items[session.response_boundary_index..]
|
|
36
|
+
payload["input"] = new_items.map(&:to_h) if new_items && !new_items.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Walk the payload and remove any "extra" keys from items, content
|
|
40
|
+
# blocks, and tool definitions since they are not part of the Open
|
|
41
|
+
# Responses API schema.
|
|
42
|
+
def strip_extra(payload)
|
|
43
|
+
input = payload["input"]
|
|
44
|
+
if input.is_a?(Array)
|
|
45
|
+
input.each do |item|
|
|
46
|
+
next unless item.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
item.delete("extra")
|
|
49
|
+
|
|
50
|
+
if item["type"] == "function_call_output" && item["output"].is_a?(Array)
|
|
51
|
+
strip_extra_from_blocks!(item["output"])
|
|
52
|
+
else
|
|
53
|
+
content = item["content"]
|
|
54
|
+
strip_extra_from_blocks!(content) if content.is_a?(Array)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
tools = payload["tools"]
|
|
60
|
+
strip_extra_from_blocks!(tools) if tools.is_a?(Array)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convert canonical "url" keys back to Open Responses API keys:
|
|
64
|
+
# input_image "url" -> "image_url"
|
|
65
|
+
# input_file "url" -> "file_url"/"file_data"
|
|
66
|
+
# input_video "url" -> "video_url"
|
|
67
|
+
def normalize_content_urls!(payload)
|
|
68
|
+
input = payload["input"]
|
|
69
|
+
return unless input.is_a?(Array)
|
|
70
|
+
|
|
71
|
+
input.each do |item|
|
|
72
|
+
next unless item.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
if item["type"] == "function_call_output" && item["output"].is_a?(Array)
|
|
75
|
+
normalize_url_keys_in_blocks!(item["output"])
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
content = item["content"]
|
|
80
|
+
normalize_url_keys_in_blocks!(content) if content.is_a?(Array)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def normalize_url_keys_in_blocks!(blocks)
|
|
85
|
+
blocks.each do |block|
|
|
86
|
+
next unless block.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
case block["type"]
|
|
89
|
+
when "input_image"
|
|
90
|
+
if block.key?("url")
|
|
91
|
+
block["image_url"] = block.delete("url")
|
|
92
|
+
end
|
|
93
|
+
when "input_file"
|
|
94
|
+
if block.key?("url")
|
|
95
|
+
url = block.delete("url")
|
|
96
|
+
if PromptBuilder.parse_data_url(url)
|
|
97
|
+
block["file_data"] = url
|
|
98
|
+
else
|
|
99
|
+
block["file_url"] = url
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
when "input_video"
|
|
103
|
+
if block.key?("url")
|
|
104
|
+
block["video_url"] = block.delete("url")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def normalize_and_validate_input_images!(payload)
|
|
111
|
+
input = payload["input"]
|
|
112
|
+
return unless input.is_a?(Array)
|
|
113
|
+
|
|
114
|
+
input.each_with_index do |item, input_index|
|
|
115
|
+
next unless item.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
if item["type"] == "function_call_output" && item["output"].is_a?(Array)
|
|
118
|
+
normalize_and_validate_image_blocks!(item["output"], "input.#{input_index}.output")
|
|
119
|
+
next
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
content = item["content"]
|
|
123
|
+
next unless content.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
normalize_and_validate_image_blocks!(content, "input.#{input_index}.content")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def normalize_and_validate_image_blocks!(blocks, path_prefix)
|
|
130
|
+
blocks.each_with_index do |block, block_index|
|
|
131
|
+
next unless block.is_a?(Hash)
|
|
132
|
+
next unless block["type"] == "input_image"
|
|
133
|
+
|
|
134
|
+
image_url = normalize_optional_string(block["image_url"])
|
|
135
|
+
file_id = normalize_optional_string(block["file_id"])
|
|
136
|
+
|
|
137
|
+
image_url ? block["image_url"] = image_url : block.delete("image_url")
|
|
138
|
+
file_id ? block["file_id"] = file_id : block.delete("file_id")
|
|
139
|
+
|
|
140
|
+
if image_url && file_id
|
|
141
|
+
raise InvalidItemError,
|
|
142
|
+
"#{path_prefix}.#{block_index} includes both image_url and file_id; provide exactly one"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
next if image_url || file_id
|
|
146
|
+
|
|
147
|
+
raise InvalidItemError,
|
|
148
|
+
"#{path_prefix}.#{block_index} requires exactly one of image_url or file_id"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Remove reasoning items that cannot be sent back in input.
|
|
153
|
+
# The Responses API only accepts reasoning items with
|
|
154
|
+
# encrypted_content; plain reasoning_text content blocks are
|
|
155
|
+
# output-only and cause an invalid_union error.
|
|
156
|
+
# For preserved reasoning items, strip the content key since
|
|
157
|
+
# the input schema only accepts null for that field.
|
|
158
|
+
def strip_non_replayable_reasoning!(payload)
|
|
159
|
+
input = payload["input"]
|
|
160
|
+
return unless input.is_a?(Array)
|
|
161
|
+
|
|
162
|
+
input.reject! do |item|
|
|
163
|
+
item.is_a?(Hash) &&
|
|
164
|
+
item["type"] == "reasoning" &&
|
|
165
|
+
!item["encrypted_content"]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
input.each do |item|
|
|
169
|
+
next unless item.is_a?(Hash) && item["type"] == "reasoning"
|
|
170
|
+
|
|
171
|
+
item.delete("content")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Remove output-only fields from content blocks that are not
|
|
176
|
+
# accepted by the Responses API input schema.
|
|
177
|
+
# OutputTextContentParam only accepts type, text, and annotations;
|
|
178
|
+
# logprobs is output-only metadata.
|
|
179
|
+
def strip_output_only_fields!(payload)
|
|
180
|
+
input = payload["input"]
|
|
181
|
+
return unless input.is_a?(Array)
|
|
182
|
+
|
|
183
|
+
input.each do |item|
|
|
184
|
+
next unless item.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
content = item["content"]
|
|
187
|
+
next unless content.is_a?(Array)
|
|
188
|
+
|
|
189
|
+
content.each do |block|
|
|
190
|
+
next unless block.is_a?(Hash) && block["type"] == "output_text"
|
|
191
|
+
|
|
192
|
+
block.delete("logprobs")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def normalize_optional_string(value)
|
|
198
|
+
return value unless value.is_a?(String)
|
|
199
|
+
|
|
200
|
+
normalized = value.strip
|
|
201
|
+
normalized.empty? ? nil : normalized
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def strip_extra_from_blocks!(blocks)
|
|
205
|
+
blocks.each do |block|
|
|
206
|
+
next unless block.is_a?(Hash)
|
|
207
|
+
|
|
208
|
+
block.delete("extra")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Normalize text.format for the Responses API. If the format uses
|
|
213
|
+
# the Chat Completions nested json_schema sub-object, flatten it
|
|
214
|
+
# so name/schema/strict/description sit directly under format.
|
|
215
|
+
def normalize_text_format!(payload)
|
|
216
|
+
format = payload.dig("text", "format")
|
|
217
|
+
return unless format.is_a?(Hash) && format["type"] == "json_schema"
|
|
218
|
+
|
|
219
|
+
json_schema = format["json_schema"]
|
|
220
|
+
return unless json_schema.is_a?(Hash)
|
|
221
|
+
|
|
222
|
+
format.delete("json_schema")
|
|
223
|
+
json_schema.each { |key, value| format[key] = value }
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
class OpenResponses < Base
|
|
6
|
+
# Response parser for the OpenAI Open Responses API format.
|
|
7
|
+
class Response < Base
|
|
8
|
+
class << self
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def deserialize_response(hash)
|
|
12
|
+
PromptBuilder::Response.from_h(hash)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for OpenAI Open Responses API format.
|
|
6
|
+
# Delegates request and response handling to dedicated nested classes.
|
|
7
|
+
class OpenResponses < Base
|
|
8
|
+
autoload :Request, File.expand_path("open_responses/request", __dir__)
|
|
9
|
+
autoload :Response, File.expand_path("open_responses/response", __dir__)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Export a session to Open Responses API request payload.
|
|
13
|
+
#
|
|
14
|
+
# @param session [Session] the session to export
|
|
15
|
+
# @return [Hash] the serialized request payload
|
|
16
|
+
def request_payload(session)
|
|
17
|
+
Request.request_payload(session)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse an Open Responses response into an PromptBuilder::Response.
|
|
21
|
+
#
|
|
22
|
+
# @param hash [Hash] the response hash in Open Responses format
|
|
23
|
+
# @return [PromptBuilder::Response] the parsed response
|
|
24
|
+
def parse_response(hash)
|
|
25
|
+
Response.parse_response(hash)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
autoload :Base, File.expand_path("serializers/base", __dir__)
|
|
6
|
+
autoload :ChatCompletion, File.expand_path("serializers/chat_completion", __dir__)
|
|
7
|
+
autoload :Converse, File.expand_path("serializers/converse", __dir__)
|
|
8
|
+
autoload :Gemini, File.expand_path("serializers/gemini", __dir__)
|
|
9
|
+
autoload :Messages, File.expand_path("serializers/messages", __dir__)
|
|
10
|
+
autoload :OpenResponses, File.expand_path("serializers/open_responses", __dir__)
|
|
11
|
+
|
|
12
|
+
# Mapping of shorthand symbols to serializer classes.
|
|
13
|
+
ALIASES = {
|
|
14
|
+
chat_completion: ChatCompletion,
|
|
15
|
+
converse: Converse,
|
|
16
|
+
gemini: Gemini,
|
|
17
|
+
messages: Messages,
|
|
18
|
+
open_responses: OpenResponses
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Resolve a serializer class from a symbol or class reference.
|
|
22
|
+
#
|
|
23
|
+
# @param serializer [Class, Symbol] a serializer class or a symbol shorthand
|
|
24
|
+
# (+:open_responses+, +:chat_completion+, +:messages+, +:gemini+, +:converse+)
|
|
25
|
+
# @return [Class] the resolved serializer class
|
|
26
|
+
# @raise [ArgumentError] if a symbol is given that does not map to a known serializer
|
|
27
|
+
def self.resolve(serializer)
|
|
28
|
+
return serializer unless serializer.is_a?(Symbol)
|
|
29
|
+
|
|
30
|
+
ALIASES.fetch(serializer) do
|
|
31
|
+
raise ArgumentError, "Unknown serializer: #{serializer.inspect}. Valid options: #{ALIASES.keys.map(&:inspect).join(", ")}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|