Class: Seo::GeminiBatchClient
- Inherits:
-
Object
- Object
- Seo::GeminiBatchClient
- Defined in:
- app/services/seo/gemini_batch_client.rb
Overview
Faraday client for the Gemini Batch API (v1beta).
Submits SEO analysis requests at 50% of standard Gemini pricing.
API reference: https://ai.google.dev/gemini-api/docs/batch-api
Defined Under Namespace
Classes: BatchError, RateLimitError
Constant Summary collapse
- BASE_URL =
URL for base.
'https://generativelanguage.googleapis.com'- API_VERSION =
Api version.
'v1beta'- TERMINAL_STATES =
Recognised terminal states. The Gemini Developer API
(generativelanguage v1beta) reports batch state as BATCH_STATE_; older
Vertex-style payloads use JOB_STATE_. Accept both so polling terminates
regardless of which enum the endpoint returns. %w[ BATCH_STATE_SUCCEEDED BATCH_STATE_FAILED BATCH_STATE_CANCELLED BATCH_STATE_EXPIRED JOB_STATE_SUCCEEDED JOB_STATE_FAILED JOB_STATE_CANCELLED JOB_STATE_EXPIRED ].freeze
Class Method Summary collapse
-
.build_request(custom_id:, user_prompt:, system_prompt: nil, schema: nil, cached_content: nil, temperature: Seo::PageAnalysisService::TEMPERATURE, max_tokens: Seo::PageAnalysisService::MAX_OUTPUT_TOKENS) ⇒ Hash
Build a single inline batch request entry.
-
.normalize_schema(schema) ⇒ Object
Normalize the ANALYSIS_SCHEMA (Ruby symbol keys) into the Gemini-expected format (string keys, no additionalProperties which Gemini doesn't use).
Instance Method Summary collapse
-
#cancel_batch(batch_name) ⇒ Hash
Cancel a batch job in progress.
-
#create_cache(model:, system_prompt:, ttl: '14400s') ⇒ String
Create a cached content object for the system prompt.
-
#create_inline_batch(model, requests, display_name: nil) ⇒ Hash
Create a batch job with inline requests (suitable for batches <20MB).
-
#delete_cache(cache_name) ⇒ Object
Delete a cached content object to stop ongoing storage charges.
-
#download_results(file_name) ⇒ String
Download results from a file-based batch (for future use with large batches).
-
#get_batch(batch_name) ⇒ Hash
Retrieve the current status of a batch job.
-
#initialize(api_key: nil) ⇒ GeminiBatchClient
constructor
A new instance of GeminiBatchClient.
-
#inline_responses(batch_response) ⇒ Array<Hash>
Extract inline responses from a completed batch.
-
#state(batch_response) ⇒ Object
Extract the state string from a batch response.
-
#terminal?(batch_response) ⇒ Boolean
Check if a batch job has reached a terminal state.
Constructor Details
#initialize(api_key: nil) ⇒ GeminiBatchClient
Returns a new instance of GeminiBatchClient.
35 36 37 38 |
# File 'app/services/seo/gemini_batch_client.rb', line 35 def initialize(api_key: nil) @api_key = api_key || Heatwave::Configuration.fetch(:google, :gemini, :api_key) raise ArgumentError, 'Gemini API key is required' if @api_key.blank? end |
Class Method Details
.build_request(custom_id:, user_prompt:, system_prompt: nil, schema: nil, cached_content: nil, temperature: Seo::PageAnalysisService::TEMPERATURE, max_tokens: Seo::PageAnalysisService::MAX_OUTPUT_TOKENS) ⇒ Hash
Build a single inline batch request entry.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'app/services/seo/gemini_batch_client.rb', line 168 def self.build_request(custom_id:, user_prompt:, system_prompt: nil, schema: nil, cached_content: nil, temperature: Seo::PageAnalysisService::TEMPERATURE, max_tokens: Seo::PageAnalysisService::MAX_OUTPUT_TOKENS) request_body = { contents: [ { role: 'user', parts: [{ text: user_prompt }] } ], generationConfig: { temperature: temperature, maxOutputTokens: max_tokens, responseMimeType: 'application/json' } } if cached_content request_body[:cachedContent] = cached_content elsif system_prompt request_body[:systemInstruction] = { parts: [{ text: system_prompt }] } end request_body[:generationConfig][:responseJsonSchema] = normalize_schema(schema) if schema { request: request_body, metadata: { key: custom_id } } end |
.normalize_schema(schema) ⇒ Object
Normalize the ANALYSIS_SCHEMA (Ruby symbol keys) into the Gemini-expected
format (string keys, no additionalProperties which Gemini doesn't use).
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'app/services/seo/gemini_batch_client.rb', line 199 def self.normalize_schema(schema) deep_stringify = lambda do |obj| case obj when Hash obj.each_with_object({}) do |(k, v), h| next if k.to_s == 'additionalProperties' h[k.to_s] = deep_stringify.call(v) end when Array obj.map { |v| deep_stringify.call(v) } else obj end end deep_stringify.call(schema) end |
Instance Method Details
#cancel_batch(batch_name) ⇒ Hash
Cancel a batch job in progress.
122 123 124 125 |
# File 'app/services/seo/gemini_batch_client.rb', line 122 def cancel_batch(batch_name) response = connection.post("/#{API_VERSION}/#{batch_name}:cancel") handle_response(response) end |
#create_cache(model:, system_prompt:, ttl: '14400s') ⇒ String
Create a cached content object for the system prompt.
Returns the cache name (e.g. "cachedContents/abc123") for use in requests.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/services/seo/gemini_batch_client.rb', line 134 def create_cache(model:, system_prompt:, ttl: '14400s') body = { model: "models/#{model}", systemInstruction: { parts: [{ text: system_prompt }] }, ttl: ttl } response = connection.post( "/#{API_VERSION}/cachedContents", body.to_json ) result = handle_response(response) result['name'] end |
#create_inline_batch(model, requests, display_name: nil) ⇒ Hash
Create a batch job with inline requests (suitable for batches <20MB).
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'app/services/seo/gemini_batch_client.rb', line 46 def create_inline_batch(model, requests, display_name: nil) display_name ||= "seo-batch-#{Time.current.strftime('%Y%m%d-%H%M')}" body = { batch: { display_name: display_name, input_config: { requests: { requests: requests } } } } response = connection.post( "/#{API_VERSION}/models/#{model}:batchGenerateContent", body.to_json ) handle_response(response) end |
#delete_cache(cache_name) ⇒ Object
Delete a cached content object to stop ongoing storage charges.
153 154 155 156 |
# File 'app/services/seo/gemini_batch_client.rb', line 153 def delete_cache(cache_name) response = connection.delete("/#{API_VERSION}/#{cache_name}") raise BatchError, "Failed to delete cache: #{response.status}" unless response.success? end |
#download_results(file_name) ⇒ String
Download results from a file-based batch (for future use with large batches).
110 111 112 113 114 115 116 |
# File 'app/services/seo/gemini_batch_client.rb', line 110 def download_results(file_name) response = connection.get("/download/#{API_VERSION}/#{file_name}:download", alt: 'media') raise BatchError, "Failed to download results: #{response.status}" unless response.success? response.body end |
#get_batch(batch_name) ⇒ Hash
Retrieve the current status of a batch job.
72 73 74 75 |
# File 'app/services/seo/gemini_batch_client.rb', line 72 def get_batch(batch_name) response = connection.get("/#{API_VERSION}/#{batch_name}") handle_response(response) end |
#inline_responses(batch_response) ⇒ Array<Hash>
Extract inline responses from a completed batch.
The v1beta inline batch response double-nests the array:
response.inlinedResponses.inlinedResponses => [ metadata, … ]
(mirroring the request's input_config.requests.requests). File-based and
older payloads return the array directly, so unwrap one level only when
the intermediate value is the wrapper Hash.
100 101 102 103 104 |
# File 'app/services/seo/gemini_batch_client.rb', line 100 def inline_responses(batch_response) inlined = batch_response.dig('response', 'inlinedResponses') inlined = inlined['inlinedResponses'] if inlined.is_a?(Hash) inlined || [] end |
#state(batch_response) ⇒ Object
Extract the state string from a batch response.
86 87 88 |
# File 'app/services/seo/gemini_batch_client.rb', line 86 def state(batch_response) batch_response.dig('metadata', 'state') end |
#terminal?(batch_response) ⇒ Boolean
Check if a batch job has reached a terminal state. The top-level done
flag is the most reliable terminal signal; fall back to the state enum.
79 80 81 82 83 |
# File 'app/services/seo/gemini_batch_client.rb', line 79 def terminal?(batch_response) return true if batch_response['done'] == true TERMINAL_STATES.include?(state(batch_response)) end |