aigen-google 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/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +383 -0
- data/Rakefile +10 -0
- data/lib/aigen/google/chat.rb +251 -0
- data/lib/aigen/google/client.rb +243 -0
- data/lib/aigen/google/configuration.rb +20 -0
- data/lib/aigen/google/content.rb +82 -0
- data/lib/aigen/google/errors.rb +56 -0
- data/lib/aigen/google/generation_config.rb +171 -0
- data/lib/aigen/google/http_client.rb +173 -0
- data/lib/aigen/google/image_response.rb +124 -0
- data/lib/aigen/google/safety_settings.rb +78 -0
- data/lib/aigen/google/version.rb +18 -0
- data/lib/aigen/google.rb +26 -0
- data/sig/aigen/google.rbs +6 -0
- metadata +77 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigen
|
|
4
|
+
module Google
|
|
5
|
+
# GenerationConfig controls generation parameters for the Gemini API.
|
|
6
|
+
# Validates parameter ranges and raises InvalidRequestError before API calls.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic configuration
|
|
9
|
+
# config = Aigen::Google::GenerationConfig.new(temperature: 0.7, max_output_tokens: 1024)
|
|
10
|
+
# config.to_h # => {temperature: 0.7, maxOutputTokens: 1024}
|
|
11
|
+
#
|
|
12
|
+
# @example All parameters
|
|
13
|
+
# config = Aigen::Google::GenerationConfig.new(
|
|
14
|
+
# temperature: 0.9,
|
|
15
|
+
# top_p: 0.95,
|
|
16
|
+
# top_k: 40,
|
|
17
|
+
# max_output_tokens: 2048
|
|
18
|
+
# )
|
|
19
|
+
class GenerationConfig
|
|
20
|
+
# Initializes a GenerationConfig instance with optional parameters.
|
|
21
|
+
# Validates all parameters and raises InvalidRequestError if invalid.
|
|
22
|
+
#
|
|
23
|
+
# @param temperature [Float, nil] controls randomness (0.0-1.0)
|
|
24
|
+
# @param top_p [Float, nil] nucleus sampling threshold (0.0-1.0)
|
|
25
|
+
# @param top_k [Integer, nil] top-k sampling limit (> 0)
|
|
26
|
+
# @param max_output_tokens [Integer, nil] maximum response tokens (> 0)
|
|
27
|
+
# @param response_modalities [Array<String>, nil] output modalities: ["TEXT"], ["IMAGE"], or ["TEXT", "IMAGE"]
|
|
28
|
+
# @param aspect_ratio [String, nil] image aspect ratio: "1:1", "16:9", "9:16", "4:3", "3:4", "5:4", "4:5"
|
|
29
|
+
# @param image_size [String, nil] image resolution: "1K", "2K", "4K" (uppercase required)
|
|
30
|
+
#
|
|
31
|
+
# @raise [InvalidRequestError] if any parameter is out of valid range
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# config = GenerationConfig.new(temperature: 0.5)
|
|
35
|
+
#
|
|
36
|
+
# @example Image generation
|
|
37
|
+
# config = GenerationConfig.new(
|
|
38
|
+
# response_modalities: ["TEXT", "IMAGE"],
|
|
39
|
+
# aspect_ratio: "16:9",
|
|
40
|
+
# image_size: "2K"
|
|
41
|
+
# )
|
|
42
|
+
def initialize(temperature: nil, top_p: nil, top_k: nil, max_output_tokens: nil, response_modalities: nil, aspect_ratio: nil, image_size: nil)
|
|
43
|
+
validate_temperature(temperature) if temperature
|
|
44
|
+
validate_top_p(top_p) if top_p
|
|
45
|
+
validate_top_k(top_k) if top_k
|
|
46
|
+
validate_max_output_tokens(max_output_tokens) if max_output_tokens
|
|
47
|
+
validate_response_modalities(response_modalities) if response_modalities
|
|
48
|
+
validate_aspect_ratio(aspect_ratio) if aspect_ratio
|
|
49
|
+
validate_image_size(image_size) if image_size
|
|
50
|
+
|
|
51
|
+
@temperature = temperature
|
|
52
|
+
@top_p = top_p
|
|
53
|
+
@top_k = top_k
|
|
54
|
+
@max_output_tokens = max_output_tokens
|
|
55
|
+
@response_modalities = response_modalities
|
|
56
|
+
@aspect_ratio = aspect_ratio
|
|
57
|
+
@image_size = image_size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Serializes the configuration to Gemini API format with camelCase keys.
|
|
61
|
+
# Omits nil values from the output.
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] the configuration hash with camelCase keys
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# config = GenerationConfig.new(temperature: 0.7, max_output_tokens: 1024)
|
|
67
|
+
# config.to_h # => {temperature: 0.7, maxOutputTokens: 1024}
|
|
68
|
+
def to_h
|
|
69
|
+
result = {}
|
|
70
|
+
result[:temperature] = @temperature unless @temperature.nil?
|
|
71
|
+
result[:topP] = @top_p unless @top_p.nil?
|
|
72
|
+
result[:topK] = @top_k unless @top_k.nil?
|
|
73
|
+
result[:maxOutputTokens] = @max_output_tokens unless @max_output_tokens.nil?
|
|
74
|
+
result[:responseModalities] = @response_modalities unless @response_modalities.nil?
|
|
75
|
+
|
|
76
|
+
# Image generation parameters go under imageConfig
|
|
77
|
+
if @aspect_ratio || @image_size
|
|
78
|
+
image_config = {}
|
|
79
|
+
image_config[:aspectRatio] = @aspect_ratio unless @aspect_ratio.nil?
|
|
80
|
+
image_config[:imageSize] = @image_size unless @image_size.nil?
|
|
81
|
+
result[:imageConfig] = image_config
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def validate_temperature(value)
|
|
90
|
+
unless value.between?(0.0, 1.0)
|
|
91
|
+
raise InvalidRequestError.new(
|
|
92
|
+
"temperature must be between 0.0 and 1.0, got #{value}",
|
|
93
|
+
status_code: nil
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_top_p(value)
|
|
99
|
+
unless value.between?(0.0, 1.0)
|
|
100
|
+
raise InvalidRequestError.new(
|
|
101
|
+
"top_p must be between 0.0 and 1.0, got #{value}",
|
|
102
|
+
status_code: nil
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_top_k(value)
|
|
108
|
+
unless value > 0
|
|
109
|
+
raise InvalidRequestError.new(
|
|
110
|
+
"top_k must be greater than 0, got #{value}",
|
|
111
|
+
status_code: nil
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_max_output_tokens(value)
|
|
117
|
+
unless value > 0
|
|
118
|
+
raise InvalidRequestError.new(
|
|
119
|
+
"max_output_tokens must be greater than 0, got #{value}",
|
|
120
|
+
status_code: nil
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_response_modalities(value)
|
|
126
|
+
unless value.is_a?(Array)
|
|
127
|
+
raise InvalidRequestError.new(
|
|
128
|
+
"response_modalities must be an array",
|
|
129
|
+
status_code: nil
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if value.empty?
|
|
134
|
+
raise InvalidRequestError.new(
|
|
135
|
+
"response_modalities must not be empty",
|
|
136
|
+
status_code: nil
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
valid_modalities = ["TEXT", "IMAGE"]
|
|
141
|
+
invalid = value - valid_modalities
|
|
142
|
+
unless invalid.empty?
|
|
143
|
+
raise InvalidRequestError.new(
|
|
144
|
+
"response_modalities must only contain TEXT or IMAGE, got: #{invalid.join(", ")}",
|
|
145
|
+
status_code: nil
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_aspect_ratio(value)
|
|
151
|
+
valid_ratios = ["1:1", "16:9", "9:16", "4:3", "3:4", "5:4", "4:5"]
|
|
152
|
+
unless valid_ratios.include?(value)
|
|
153
|
+
raise InvalidRequestError.new(
|
|
154
|
+
"aspect_ratio must be one of #{valid_ratios.join(", ")}, got #{value}",
|
|
155
|
+
status_code: nil
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_image_size(value)
|
|
161
|
+
valid_sizes = ["1K", "2K", "4K"]
|
|
162
|
+
unless valid_sizes.include?(value)
|
|
163
|
+
raise InvalidRequestError.new(
|
|
164
|
+
"image_size must be one of #{valid_sizes.join(", ")}, got #{value}",
|
|
165
|
+
status_code: nil
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Aigen
|
|
7
|
+
module Google
|
|
8
|
+
class HttpClient
|
|
9
|
+
BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|
10
|
+
|
|
11
|
+
def initialize(api_key:, timeout: 30, retry_count: 3)
|
|
12
|
+
@api_key = api_key
|
|
13
|
+
@timeout = timeout
|
|
14
|
+
@retry_count = retry_count
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def post(path, payload, attempt: 1)
|
|
18
|
+
response = connection.post(path) do |req|
|
|
19
|
+
req.headers["Content-Type"] = "application/json"
|
|
20
|
+
req.headers["x-goog-api-key"] = @api_key
|
|
21
|
+
req.body = payload.to_json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
handle_response(response, path, payload, attempt)
|
|
25
|
+
rescue Faraday::Error => e
|
|
26
|
+
# Handle timeout-like errors (including WebMock's .to_timeout)
|
|
27
|
+
is_timeout = e.is_a?(Faraday::TimeoutError) ||
|
|
28
|
+
e.is_a?(::Timeout::Error) ||
|
|
29
|
+
e.message.include?("execution expired") ||
|
|
30
|
+
e.message.include?("timed out")
|
|
31
|
+
|
|
32
|
+
if is_timeout
|
|
33
|
+
if attempt >= @retry_count
|
|
34
|
+
raise TimeoutError, "Request timed out after #{@retry_count} retries"
|
|
35
|
+
end
|
|
36
|
+
backoff_seconds = 2**(attempt - 1) # 1s, 2s, 4s
|
|
37
|
+
sleep backoff_seconds
|
|
38
|
+
post(path, payload, attempt: attempt + 1)
|
|
39
|
+
else
|
|
40
|
+
# Network connection errors
|
|
41
|
+
raise ServerError.new("Network error: #{e.message}", status_code: nil)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Makes a streaming POST request to the Gemini API.
|
|
46
|
+
# Processes chunked responses and yields each parsed chunk to the provided block.
|
|
47
|
+
#
|
|
48
|
+
# @param path [String] the API endpoint path
|
|
49
|
+
# @param payload [Hash] the request payload (will be JSON encoded)
|
|
50
|
+
# @yieldparam chunk [Hash] parsed JSON chunk from the streaming response
|
|
51
|
+
#
|
|
52
|
+
# @return [nil] always returns nil when block is given
|
|
53
|
+
#
|
|
54
|
+
# @raise [ArgumentError] if no block is provided
|
|
55
|
+
# @raise [Aigen::Google::AuthenticationError] if API key is invalid (401/403)
|
|
56
|
+
# @raise [Aigen::Google::InvalidRequestError] if request is malformed (400/404)
|
|
57
|
+
# @raise [Aigen::Google::RateLimitError] if rate limit is exceeded (429)
|
|
58
|
+
# @raise [Aigen::Google::ServerError] if server error occurs (500+) or network error
|
|
59
|
+
#
|
|
60
|
+
# @example Stream generated content chunks
|
|
61
|
+
# http_client.post_stream("models/gemini-pro:streamGenerateContent", payload) do |chunk|
|
|
62
|
+
# text = chunk["candidates"][0]["content"]["parts"][0]["text"]
|
|
63
|
+
# print text
|
|
64
|
+
# end
|
|
65
|
+
def post_stream(path, payload, &block)
|
|
66
|
+
raise ArgumentError, "block required for streaming" unless block_given?
|
|
67
|
+
|
|
68
|
+
buffer = ""
|
|
69
|
+
|
|
70
|
+
response = connection.post(path) do |req|
|
|
71
|
+
req.headers["Content-Type"] = "application/json"
|
|
72
|
+
req.headers["x-goog-api-key"] = @api_key
|
|
73
|
+
req.body = payload.to_json
|
|
74
|
+
|
|
75
|
+
req.options.on_data = proc do |chunk, _total_bytes|
|
|
76
|
+
buffer += chunk
|
|
77
|
+
|
|
78
|
+
# Process complete lines (newline-delimited JSON)
|
|
79
|
+
while (newline_index = buffer.index("\n"))
|
|
80
|
+
line = buffer.slice!(0, newline_index + 1).strip
|
|
81
|
+
next if line.empty?
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
parsed_chunk = JSON.parse(line)
|
|
85
|
+
block.call(parsed_chunk)
|
|
86
|
+
rescue JSON::ParserError => e
|
|
87
|
+
raise ServerError.new("Invalid JSON in stream chunk: #{e.message}", status_code: nil)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check for non-200 status codes
|
|
94
|
+
handle_stream_response_status(response)
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
rescue Faraday::Error => e
|
|
98
|
+
raise ServerError.new("Network error during streaming: #{e.message}", status_code: nil)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def connection
|
|
104
|
+
@connection ||= Faraday.new(url: BASE_URL) do |conn|
|
|
105
|
+
conn.options.timeout = @timeout
|
|
106
|
+
conn.adapter Faraday.default_adapter do |http|
|
|
107
|
+
http.open_timeout = 5
|
|
108
|
+
http.read_timeout = @timeout
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_response(response, path, payload, attempt)
|
|
114
|
+
case response.status
|
|
115
|
+
when 200..299
|
|
116
|
+
begin
|
|
117
|
+
JSON.parse(response.body)
|
|
118
|
+
rescue JSON::ParserError => e
|
|
119
|
+
raise ServerError.new("Invalid JSON response from API: #{e.message}", status_code: response.status)
|
|
120
|
+
end
|
|
121
|
+
when 400
|
|
122
|
+
raise InvalidRequestError.new(extract_error_message(response), status_code: 400)
|
|
123
|
+
when 401, 403
|
|
124
|
+
raise AuthenticationError.new("Invalid API key. Get one at https://makersuite.google.com/app/apikey", status_code: response.status)
|
|
125
|
+
when 404
|
|
126
|
+
raise InvalidRequestError.new("Resource not found. Check model name and endpoint.", status_code: 404)
|
|
127
|
+
when 429
|
|
128
|
+
retry_response(path, payload, attempt, RateLimitError.new(extract_error_message(response), status_code: 429))
|
|
129
|
+
when 500..599
|
|
130
|
+
retry_response(path, payload, attempt, ServerError.new(extract_error_message(response), status_code: response.status))
|
|
131
|
+
else
|
|
132
|
+
raise ApiError.new("Unexpected status code: #{response.status}", status_code: response.status)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def retry_response(path, payload, attempt, error)
|
|
137
|
+
if attempt >= @retry_count
|
|
138
|
+
raise error
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
backoff_seconds = 2**(attempt - 1) # 1s, 2s, 4s
|
|
142
|
+
sleep backoff_seconds
|
|
143
|
+
post(path, payload, attempt: attempt + 1)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_error_message(response)
|
|
147
|
+
body = JSON.parse(response.body)
|
|
148
|
+
body.dig("error", "message") || response.body
|
|
149
|
+
rescue JSON::ParserError
|
|
150
|
+
response.body
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def handle_stream_response_status(response)
|
|
154
|
+
case response.status
|
|
155
|
+
when 200..299
|
|
156
|
+
# Success - streaming completed
|
|
157
|
+
when 400
|
|
158
|
+
raise InvalidRequestError.new(extract_error_message(response), status_code: 400)
|
|
159
|
+
when 401, 403
|
|
160
|
+
raise AuthenticationError.new("Invalid API key. Get one at https://makersuite.google.com/app/apikey", status_code: response.status)
|
|
161
|
+
when 404
|
|
162
|
+
raise InvalidRequestError.new("Resource not found. Check model name and endpoint.", status_code: 404)
|
|
163
|
+
when 429
|
|
164
|
+
raise RateLimitError.new(extract_error_message(response), status_code: 429)
|
|
165
|
+
when 500..599
|
|
166
|
+
raise ServerError.new(extract_error_message(response), status_code: response.status)
|
|
167
|
+
else
|
|
168
|
+
raise ApiError.new("Unexpected status code: #{response.status}", status_code: response.status)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Aigen
|
|
6
|
+
module Google
|
|
7
|
+
# Wraps an image generation API response with convenient helper methods.
|
|
8
|
+
# Provides easy access to generated images, text descriptions, and status information.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# response = client.generate_image("A cute puppy")
|
|
12
|
+
# if response.success?
|
|
13
|
+
# response.save("puppy.png")
|
|
14
|
+
# puts response.text
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Checking for failures
|
|
18
|
+
# response = client.generate_image("problematic prompt")
|
|
19
|
+
# unless response.success?
|
|
20
|
+
# puts "Failed: #{response.failure_reason}"
|
|
21
|
+
# puts response.failure_message
|
|
22
|
+
# end
|
|
23
|
+
class ImageResponse
|
|
24
|
+
attr_reader :raw_response
|
|
25
|
+
|
|
26
|
+
# Creates a new ImageResponse from a Gemini API response.
|
|
27
|
+
#
|
|
28
|
+
# @param response [Hash] the raw API response hash
|
|
29
|
+
def initialize(response)
|
|
30
|
+
@raw_response = response
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Checks if the image generation was successful.
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean] true if generation succeeded, false otherwise
|
|
36
|
+
def success?
|
|
37
|
+
finish_reason == "STOP"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Checks if an image is present in the response.
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] true if image data exists, false otherwise
|
|
43
|
+
def has_image?
|
|
44
|
+
!image_part.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the text description that accompanied the generated image.
|
|
48
|
+
#
|
|
49
|
+
# @return [String, nil] the text description or nil if not present
|
|
50
|
+
def text
|
|
51
|
+
text_part&.dig("text")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the decoded binary image data.
|
|
55
|
+
#
|
|
56
|
+
# @return [String, nil] binary image data or nil if no image present
|
|
57
|
+
def image_data
|
|
58
|
+
return nil unless has_image?
|
|
59
|
+
|
|
60
|
+
Base64.decode64(image_part["inlineData"]["data"])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the MIME type of the generated image.
|
|
64
|
+
#
|
|
65
|
+
# @return [String, nil] MIME type (e.g., "image/png") or nil if no image
|
|
66
|
+
def mime_type
|
|
67
|
+
image_part&.dig("inlineData", "mimeType")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Saves the generated image to the specified file path.
|
|
71
|
+
#
|
|
72
|
+
# @param path [String] the file path to save the image
|
|
73
|
+
# @raise [Aigen::Google::Error] if no image data is present
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# response.save("output.png")
|
|
77
|
+
def save(path)
|
|
78
|
+
raise Error, "No image data to save" unless has_image?
|
|
79
|
+
|
|
80
|
+
File.write(path, image_data)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the finish reason for failed generations.
|
|
84
|
+
#
|
|
85
|
+
# @return [String, nil] the finish reason or nil if successful
|
|
86
|
+
def failure_reason
|
|
87
|
+
return nil if success?
|
|
88
|
+
|
|
89
|
+
candidate.dig("finishReason")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the failure message for failed generations.
|
|
93
|
+
#
|
|
94
|
+
# @return [String, nil] the failure message or nil if successful
|
|
95
|
+
def failure_message
|
|
96
|
+
return nil if success?
|
|
97
|
+
|
|
98
|
+
candidate.dig("finishMessage")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def candidate
|
|
104
|
+
@raw_response.dig("candidates", 0) || {}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parts
|
|
108
|
+
candidate.dig("content", "parts") || []
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def text_part
|
|
112
|
+
parts.find { |p| p.key?("text") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def image_part
|
|
116
|
+
parts.find { |p| p.key?("inlineData") }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def finish_reason
|
|
120
|
+
candidate.dig("finishReason")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigen
|
|
4
|
+
module Google
|
|
5
|
+
# SafetySettings configures content filtering for the Gemini API.
|
|
6
|
+
# Provides constants for harm categories and thresholds, with sensible defaults.
|
|
7
|
+
#
|
|
8
|
+
# @example Using default settings (BLOCK_MEDIUM_AND_ABOVE for all categories)
|
|
9
|
+
# settings = Aigen::Google::SafetySettings.default
|
|
10
|
+
#
|
|
11
|
+
# @example Custom settings
|
|
12
|
+
# settings = Aigen::Google::SafetySettings.new([
|
|
13
|
+
# {
|
|
14
|
+
# category: Aigen::Google::SafetySettings::HARM_CATEGORY_HATE_SPEECH,
|
|
15
|
+
# threshold: Aigen::Google::SafetySettings::BLOCK_LOW_AND_ABOVE
|
|
16
|
+
# }
|
|
17
|
+
# ])
|
|
18
|
+
class SafetySettings
|
|
19
|
+
# Harm category constants
|
|
20
|
+
HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH"
|
|
21
|
+
HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT"
|
|
22
|
+
HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT"
|
|
23
|
+
HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT"
|
|
24
|
+
|
|
25
|
+
# Threshold constants
|
|
26
|
+
BLOCK_NONE = "BLOCK_NONE"
|
|
27
|
+
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE"
|
|
28
|
+
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE"
|
|
29
|
+
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH"
|
|
30
|
+
|
|
31
|
+
# Returns default safety settings with BLOCK_MEDIUM_AND_ABOVE for all categories.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<Hash>] array of default safety settings
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# defaults = SafetySettings.default
|
|
37
|
+
# # => [
|
|
38
|
+
# # {category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE"},
|
|
39
|
+
# # {category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_MEDIUM_AND_ABOVE"},
|
|
40
|
+
# # ...
|
|
41
|
+
# # ]
|
|
42
|
+
def self.default
|
|
43
|
+
[
|
|
44
|
+
{category: HARM_CATEGORY_HATE_SPEECH, threshold: BLOCK_MEDIUM_AND_ABOVE},
|
|
45
|
+
{category: HARM_CATEGORY_DANGEROUS_CONTENT, threshold: BLOCK_MEDIUM_AND_ABOVE},
|
|
46
|
+
{category: HARM_CATEGORY_HARASSMENT, threshold: BLOCK_MEDIUM_AND_ABOVE},
|
|
47
|
+
{category: HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: BLOCK_MEDIUM_AND_ABOVE}
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Initializes a SafetySettings instance with an array of settings.
|
|
52
|
+
#
|
|
53
|
+
# @param settings [Array<Hash>] array of safety settings
|
|
54
|
+
# Each setting: {category: "HARM_CATEGORY_...", threshold: "BLOCK_..."}
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# settings = SafetySettings.new([
|
|
58
|
+
# {category: HARM_CATEGORY_HATE_SPEECH, threshold: BLOCK_LOW_AND_ABOVE}
|
|
59
|
+
# ])
|
|
60
|
+
def initialize(settings)
|
|
61
|
+
@settings = settings
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Serializes the safety settings to Gemini API format.
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<Hash>] the settings array in API format
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# settings = SafetySettings.new([
|
|
70
|
+
# {category: HARM_CATEGORY_HATE_SPEECH, threshold: BLOCK_MEDIUM_AND_ABOVE}
|
|
71
|
+
# ])
|
|
72
|
+
# settings.to_h # => [{category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE"}]
|
|
73
|
+
def to_h
|
|
74
|
+
@settings
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigen
|
|
4
|
+
module Google
|
|
5
|
+
def self.gem_version
|
|
6
|
+
Gem::Version.new(VERSION::STRING)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module VERSION
|
|
10
|
+
MAJOR = 0
|
|
11
|
+
MINOR = 1
|
|
12
|
+
TINY = 0
|
|
13
|
+
PRE = nil
|
|
14
|
+
BUILD = nil
|
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE, BUILD].compact.join(".")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/aigen/google.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "google/version"
|
|
4
|
+
require_relative "google/errors"
|
|
5
|
+
require_relative "google/configuration"
|
|
6
|
+
require_relative "google/content"
|
|
7
|
+
require_relative "google/generation_config"
|
|
8
|
+
require_relative "google/safety_settings"
|
|
9
|
+
require_relative "google/http_client"
|
|
10
|
+
require_relative "google/image_response"
|
|
11
|
+
require_relative "google/chat"
|
|
12
|
+
require_relative "google/client"
|
|
13
|
+
|
|
14
|
+
module Aigen
|
|
15
|
+
module Google
|
|
16
|
+
class << self
|
|
17
|
+
attr_accessor :configuration
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
self.configuration ||= Configuration.new
|
|
21
|
+
yield(configuration) if block_given?
|
|
22
|
+
configuration
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aigen-google
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lauri Jutila
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
description: A Ruby-native SDK for Google's Generative AI (Gemini) APIs, providing
|
|
27
|
+
text generation, chat, streaming, multimodal content, and image generation (Nano
|
|
28
|
+
Banana) support.
|
|
29
|
+
email:
|
|
30
|
+
- ljuti@nmux.dev
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".rspec"
|
|
36
|
+
- ".standard.yml"
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- Rakefile
|
|
41
|
+
- lib/aigen/google.rb
|
|
42
|
+
- lib/aigen/google/chat.rb
|
|
43
|
+
- lib/aigen/google/client.rb
|
|
44
|
+
- lib/aigen/google/configuration.rb
|
|
45
|
+
- lib/aigen/google/content.rb
|
|
46
|
+
- lib/aigen/google/errors.rb
|
|
47
|
+
- lib/aigen/google/generation_config.rb
|
|
48
|
+
- lib/aigen/google/http_client.rb
|
|
49
|
+
- lib/aigen/google/image_response.rb
|
|
50
|
+
- lib/aigen/google/safety_settings.rb
|
|
51
|
+
- lib/aigen/google/version.rb
|
|
52
|
+
- sig/aigen/google.rbs
|
|
53
|
+
homepage: https://github.com/neuralmux/aigen-google
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://github.com/neuralmux/aigen-google
|
|
58
|
+
source_code_uri: https://github.com/neuralmux/aigen-google
|
|
59
|
+
changelog_uri: https://github.com/neuralmux/aigen-google/blob/main/CHANGELOG.md
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.1.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Ruby SDK for Google Generative AI (Gemini API)
|
|
77
|
+
test_files: []
|