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,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module PromptBuilder
|
|
7
|
+
module Serializers
|
|
8
|
+
class Gemini < Base
|
|
9
|
+
# Response parser for the Google Gemini API format.
|
|
10
|
+
#
|
|
11
|
+
# === Response metadata not modeled by Open Responses
|
|
12
|
+
#
|
|
13
|
+
# The Gemini response surface has fields with no canonical Open Responses
|
|
14
|
+
# equivalent. The parser preserves them on +Response#extra+ rather
|
|
15
|
+
# than dropping them silently:
|
|
16
|
+
# - +groundingMetadata+ — web search grounding chunks, supports, queries
|
|
17
|
+
# - +citationMetadata+ — per-candidate citation sources
|
|
18
|
+
# - +urlContextMetadata+ / +urlRetrievalMetadata+ — URL retrieval traces
|
|
19
|
+
# - +safetyRatings+ — per-candidate safety probabilities and severities
|
|
20
|
+
# - +promptFeedback+ — prompt-level safety feedback
|
|
21
|
+
# - +avgLogprobs+ / +logprobsResult+ — log-probability outputs
|
|
22
|
+
# - +finishMessage+ — human-readable explanation of +finishReason+
|
|
23
|
+
# - +createTime+ — RFC 3339 response creation timestamp
|
|
24
|
+
# - +index+ — candidate index when +candidateCount+ > 1
|
|
25
|
+
#
|
|
26
|
+
# Additional +usageMetadata+ breakdowns (per-modality token counts and
|
|
27
|
+
# tool-use prompt tokens) are surfaced through +Usage#input_tokens_details+
|
|
28
|
+
# and +Usage#output_tokens_details+.
|
|
29
|
+
#
|
|
30
|
+
# === Unsupported response shapes
|
|
31
|
+
#
|
|
32
|
+
# The response parser silently skips +Part+ shapes that have no canonical
|
|
33
|
+
# Open Responses equivalent:
|
|
34
|
+
# - +inlineData+ / +fileData+ parts (image/audio output blobs)
|
|
35
|
+
# - +executableCode+ / +codeExecutionResult+ parts
|
|
36
|
+
# - server-side +toolCall+ / +toolResponse+ parts
|
|
37
|
+
# - +Part+s with no recognized content key
|
|
38
|
+
#
|
|
39
|
+
# When a response contains multiple candidates, only the first is parsed.
|
|
40
|
+
class Response < Base
|
|
41
|
+
FAILED_FINISH_REASONS = %w[
|
|
42
|
+
SAFETY
|
|
43
|
+
RECITATION
|
|
44
|
+
OTHER
|
|
45
|
+
BLOCKLIST
|
|
46
|
+
PROHIBITED_CONTENT
|
|
47
|
+
SPII
|
|
48
|
+
MALFORMED_FUNCTION_CALL
|
|
49
|
+
IMAGE_SAFETY
|
|
50
|
+
LANGUAGE
|
|
51
|
+
UNEXPECTED_TOOL_CALL
|
|
52
|
+
TOO_MANY_TOOL_CALLS
|
|
53
|
+
MODEL_ARMOR
|
|
54
|
+
IMAGE_PROHIBITED_CONTENT
|
|
55
|
+
IMAGE_OTHER
|
|
56
|
+
NO_IMAGE
|
|
57
|
+
IMAGE_RECITATION
|
|
58
|
+
MISSING_THOUGHT_SIGNATURE
|
|
59
|
+
MALFORMED_RESPONSE
|
|
60
|
+
].freeze
|
|
61
|
+
private_constant :FAILED_FINISH_REASONS
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def deserialize_response(hash)
|
|
67
|
+
usage = build_usage(hash["usageMetadata"])
|
|
68
|
+
|
|
69
|
+
# Only the first candidate is parsed; additional candidates have no
|
|
70
|
+
# canonical multi-candidate representation and are silently dropped.
|
|
71
|
+
candidates = hash["candidates"] || []
|
|
72
|
+
first_candidate = candidates[0]
|
|
73
|
+
|
|
74
|
+
response_id = hash["responseId"]
|
|
75
|
+
call_id_seed = response_id || SecureRandom.hex(4)
|
|
76
|
+
|
|
77
|
+
output_items = if first_candidate && first_candidate["content"]
|
|
78
|
+
build_output_items(first_candidate["content"]["parts"] || [], call_id_seed)
|
|
79
|
+
else
|
|
80
|
+
[]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
finish_reason = first_candidate ? first_candidate["finishReason"] : nil
|
|
84
|
+
status = map_finish_reason(finish_reason)
|
|
85
|
+
|
|
86
|
+
# When candidates is empty due to a prompt-level safety block, Gemini
|
|
87
|
+
# returns an empty `candidates` and a `promptFeedback.blockReason`.
|
|
88
|
+
# Surface that as a failed status rather than silently nil.
|
|
89
|
+
if status.nil? && candidates.empty? && hash.dig("promptFeedback", "blockReason")
|
|
90
|
+
status = "failed"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
provider_data = build_provider_data(hash, first_candidate)
|
|
94
|
+
|
|
95
|
+
PromptBuilder::Response.new(
|
|
96
|
+
id: response_id,
|
|
97
|
+
object: nil,
|
|
98
|
+
model: hash["modelVersion"],
|
|
99
|
+
output: output_items,
|
|
100
|
+
status: status,
|
|
101
|
+
usage: usage,
|
|
102
|
+
extra: provider_data
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_usage(usage_hash)
|
|
107
|
+
return nil unless usage_hash
|
|
108
|
+
|
|
109
|
+
input_tokens_details = {}
|
|
110
|
+
input_tokens_details["cached_tokens"] = usage_hash["cachedContentTokenCount"] if usage_hash["cachedContentTokenCount"]
|
|
111
|
+
input_tokens_details["tool_use_prompt_tokens"] = usage_hash["toolUsePromptTokenCount"] if usage_hash["toolUsePromptTokenCount"]
|
|
112
|
+
input_tokens_details["prompt_tokens_details"] = usage_hash["promptTokensDetails"] if usage_hash["promptTokensDetails"]
|
|
113
|
+
input_tokens_details["cache_tokens_details"] = usage_hash["cacheTokensDetails"] if usage_hash["cacheTokensDetails"]
|
|
114
|
+
input_tokens_details["tool_use_prompt_tokens_details"] = usage_hash["toolUsePromptTokensDetails"] if usage_hash["toolUsePromptTokensDetails"]
|
|
115
|
+
|
|
116
|
+
output_tokens_details = {}
|
|
117
|
+
output_tokens_details["reasoning_tokens"] = usage_hash["thoughtsTokenCount"] if usage_hash["thoughtsTokenCount"]
|
|
118
|
+
output_tokens_details["candidates_tokens_details"] = usage_hash["candidatesTokensDetails"] if usage_hash["candidatesTokensDetails"]
|
|
119
|
+
|
|
120
|
+
Usage.new(
|
|
121
|
+
input_tokens: usage_hash["promptTokenCount"],
|
|
122
|
+
output_tokens: usage_hash["candidatesTokenCount"],
|
|
123
|
+
total_tokens: usage_hash["totalTokenCount"],
|
|
124
|
+
input_tokens_details: input_tokens_details.empty? ? nil : input_tokens_details,
|
|
125
|
+
output_tokens_details: output_tokens_details.empty? ? nil : output_tokens_details
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_provider_data(hash, candidate)
|
|
130
|
+
data = {}
|
|
131
|
+
data["create_time"] = hash["createTime"] if hash["createTime"]
|
|
132
|
+
data["model_status"] = hash["modelStatus"] if hash["modelStatus"]
|
|
133
|
+
data["prompt_feedback"] = hash["promptFeedback"] if hash["promptFeedback"]
|
|
134
|
+
|
|
135
|
+
if candidate
|
|
136
|
+
data["index"] = candidate["index"] if candidate.key?("index")
|
|
137
|
+
data["safety_ratings"] = candidate["safetyRatings"] if candidate["safetyRatings"]
|
|
138
|
+
data["citation_metadata"] = candidate["citationMetadata"] if candidate["citationMetadata"]
|
|
139
|
+
data["grounding_metadata"] = candidate["groundingMetadata"] if candidate["groundingMetadata"]
|
|
140
|
+
data["url_context_metadata"] = candidate["urlContextMetadata"] if candidate["urlContextMetadata"]
|
|
141
|
+
data["url_retrieval_metadata"] = candidate["urlRetrievalMetadata"] if candidate["urlRetrievalMetadata"]
|
|
142
|
+
data["avg_logprobs"] = candidate["avgLogprobs"] if candidate.key?("avgLogprobs")
|
|
143
|
+
data["logprobs_result"] = candidate["logprobsResult"] if candidate["logprobsResult"]
|
|
144
|
+
data["grounding_attributions"] = candidate["groundingAttributions"] if candidate["groundingAttributions"]
|
|
145
|
+
data["finish_message"] = candidate["finishMessage"] if candidate["finishMessage"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
data.empty? ? nil : data
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def map_finish_reason(reason)
|
|
152
|
+
case reason
|
|
153
|
+
when nil, "FINISH_REASON_UNSPECIFIED"
|
|
154
|
+
nil
|
|
155
|
+
when "STOP"
|
|
156
|
+
"completed"
|
|
157
|
+
when "MAX_TOKENS"
|
|
158
|
+
"incomplete"
|
|
159
|
+
when *FAILED_FINISH_REASONS
|
|
160
|
+
"failed"
|
|
161
|
+
else
|
|
162
|
+
reason
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_output_items(parts, call_id_seed)
|
|
167
|
+
output = []
|
|
168
|
+
text_contents = []
|
|
169
|
+
reasoning_contents = []
|
|
170
|
+
call_index = 0
|
|
171
|
+
|
|
172
|
+
parts.each do |part|
|
|
173
|
+
if part["thought"]
|
|
174
|
+
flush_text_contents!(output, text_contents)
|
|
175
|
+
block = {
|
|
176
|
+
"type" => "thinking",
|
|
177
|
+
"thinking" => part["text"] || ""
|
|
178
|
+
}
|
|
179
|
+
block["signature"] = part["thoughtSignature"] if part["thoughtSignature"]
|
|
180
|
+
reasoning_contents << block
|
|
181
|
+
elsif part["functionCall"]
|
|
182
|
+
flush_text_contents!(output, text_contents)
|
|
183
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
184
|
+
|
|
185
|
+
function_call = part["functionCall"]
|
|
186
|
+
call_id = function_call["id"] || "gemini_call_#{call_id_seed}_#{call_index}"
|
|
187
|
+
|
|
188
|
+
output << Items::FunctionCall.new(
|
|
189
|
+
name: function_call["name"],
|
|
190
|
+
call_id: call_id,
|
|
191
|
+
arguments: JSON.generate(function_call["args"] || {}),
|
|
192
|
+
**(part["thoughtSignature"] ? {thought_signature: part["thoughtSignature"]} : {})
|
|
193
|
+
)
|
|
194
|
+
call_index += 1
|
|
195
|
+
elsif part.key?("text")
|
|
196
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
197
|
+
text_contents << Content::OutputText.new(
|
|
198
|
+
text: part["text"],
|
|
199
|
+
**(part["thoughtSignature"] ? {thought_signature: part["thoughtSignature"]} : {})
|
|
200
|
+
)
|
|
201
|
+
else
|
|
202
|
+
# Parts with no canonical Open Responses equivalent (inlineData,
|
|
203
|
+
# fileData, executableCode, etc.) are silently skipped.
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
flush_text_contents!(output, text_contents)
|
|
209
|
+
flush_reasoning_contents!(output, reasoning_contents)
|
|
210
|
+
output
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def flush_text_contents!(output, text_contents)
|
|
214
|
+
return if text_contents.empty?
|
|
215
|
+
|
|
216
|
+
output << Items::Message.new(
|
|
217
|
+
role: "assistant",
|
|
218
|
+
content: text_contents.dup
|
|
219
|
+
)
|
|
220
|
+
text_contents.clear
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def flush_reasoning_contents!(output, reasoning_contents)
|
|
224
|
+
return if reasoning_contents.empty?
|
|
225
|
+
|
|
226
|
+
output << Items::Reasoning.new(content: reasoning_contents.dup)
|
|
227
|
+
reasoning_contents.clear
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptBuilder
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for the Google Gemini API format.
|
|
6
|
+
# Delegates request and response handling to dedicated nested classes.
|
|
7
|
+
class Gemini < Base
|
|
8
|
+
autoload :Request, File.expand_path("gemini/request", __dir__)
|
|
9
|
+
autoload :Response, File.expand_path("gemini/response", __dir__)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Export a session to Gemini 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 Gemini response into a PromptBuilder::Response.
|
|
21
|
+
#
|
|
22
|
+
# @param hash [Hash] the response hash in Gemini 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
|