geminize 1.1.0 → 1.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.
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Extends ContentRequest with function calling and JSON mode support
6
+ class ContentRequest
7
+ # Additional attributes for function calling and JSON mode
8
+
9
+ # @return [Array<Geminize::Models::Tool>] The tools for this request
10
+ attr_reader :tools
11
+
12
+ # @return [Geminize::Models::ToolConfig, nil] The tool configuration
13
+ attr_reader :tool_config
14
+
15
+ # @return [String, nil] The MIME type for the response format
16
+ attr_accessor :response_mime_type
17
+
18
+ # Add a function to the request
19
+ # @param name [String] The name of the function
20
+ # @param description [String] A description of what the function does
21
+ # @param parameters [Hash] JSON schema for function parameters
22
+ # @return [self] The request object for chaining
23
+ # @raise [Geminize::ValidationError] If the function is invalid
24
+ def add_function(name, description, parameters)
25
+ @tools ||= []
26
+
27
+ function_declaration = FunctionDeclaration.new(name, description, parameters)
28
+ tool = Tool.new(function_declaration)
29
+
30
+ @tools << tool
31
+ self
32
+ end
33
+
34
+ # Enable code execution for the request
35
+ # @return [self] The request object for chaining
36
+ def enable_code_execution
37
+ @tools ||= []
38
+ @tools << Tool.new(nil, true)
39
+ self
40
+ end
41
+
42
+ # Set the tool config for function execution
43
+ # @param execution_mode [String] The execution mode for functions ("AUTO", "MANUAL", or "NONE")
44
+ # @return [self] The request object for chaining
45
+ # @raise [Geminize::ValidationError] If the tool config is invalid
46
+ def set_tool_config(execution_mode = "AUTO")
47
+ @tool_config = ToolConfig.new(execution_mode)
48
+ self
49
+ end
50
+
51
+ # Enable JSON mode for structured output
52
+ # @return [self] The request object for chaining
53
+ def enable_json_mode
54
+ @response_mime_type = "application/json"
55
+ self
56
+ end
57
+
58
+ # Disable JSON mode and return to regular text output
59
+ # @return [self] The request object for chaining
60
+ def disable_json_mode
61
+ @response_mime_type = nil
62
+ self
63
+ end
64
+
65
+ # Override the to_hash method to include additional function calling features
66
+ # @return [Hash] The request as a hash
67
+ def to_hash
68
+ # First get the base implementation's hash by calling the standard method
69
+ # Use the implementation from ContentRequest directly
70
+ request = {
71
+ contents: [
72
+ {
73
+ parts: @content_parts.map do |part|
74
+ if part[:type] == "text"
75
+ {text: part[:text]}
76
+ elsif part[:type] == "image"
77
+ {
78
+ inlineData: {
79
+ mimeType: part[:mime_type],
80
+ data: part[:data]
81
+ }
82
+ }
83
+ end
84
+ end.compact
85
+ }
86
+ ]
87
+ }
88
+
89
+ # Add generation config
90
+ if @temperature || @max_tokens || @top_p || @top_k || @stop_sequences
91
+ request[:generationConfig] = {}
92
+ request[:generationConfig][:temperature] = @temperature if @temperature
93
+ request[:generationConfig][:maxOutputTokens] = @max_tokens if @max_tokens
94
+ request[:generationConfig][:topP] = @top_p if @top_p
95
+ request[:generationConfig][:topK] = @top_k if @top_k
96
+ request[:generationConfig][:stopSequences] = @stop_sequences if @stop_sequences
97
+ end
98
+
99
+ # Add system instruction
100
+ if @system_instruction
101
+ request[:systemInstruction] = {
102
+ parts: [
103
+ {
104
+ text: @system_instruction
105
+ }
106
+ ]
107
+ }
108
+ end
109
+
110
+ # Add tools if present
111
+ if @tools && !@tools.empty?
112
+ request[:tools] = @tools.map(&:to_hash)
113
+ end
114
+
115
+ # Add tool config if present
116
+ if @tool_config
117
+ request[:toolConfig] = @tool_config.to_hash
118
+ end
119
+
120
+ # Add response format if JSON mode is enabled
121
+ if @response_mime_type
122
+ request[:generationConfig] ||= {}
123
+ request[:generationConfig][:responseSchema] = {
124
+ type: "object",
125
+ properties: {
126
+ # Add a sample property to satisfy the API requirement
127
+ # This is a generic structure that will be overridden by the model's understanding
128
+ # of what properties to include based on the prompt
129
+ result: {
130
+ type: "array",
131
+ items: {
132
+ type: "object",
133
+ properties: {
134
+ name: {type: "string"},
135
+ value: {type: "string"}
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ request[:generationConfig][:responseMimeType] = @response_mime_type
142
+ end
143
+
144
+ request
145
+ end
146
+
147
+ # Original validate! method includes validation for tools and JSON mode
148
+ alias_method :original_validate!, :validate!
149
+
150
+ def validate!
151
+ # Don't call super, instead call the original specific validations directly
152
+ validate_prompt!
153
+ validate_system_instruction! if @system_instruction
154
+ validate_temperature! if @temperature
155
+ validate_max_tokens! if @max_tokens
156
+ validate_top_p! if @top_p
157
+ validate_top_k! if @top_k
158
+ validate_stop_sequences! if @stop_sequences
159
+ validate_content_parts!
160
+
161
+ # Then validate our extensions
162
+ validate_tools!
163
+ validate_tool_config!
164
+ validate_response_mime_type!
165
+ true
166
+ end
167
+
168
+ private
169
+
170
+ # Validate the tools
171
+ # @raise [Geminize::ValidationError] If the tools are invalid
172
+ def validate_tools!
173
+ return if @tools.nil? || @tools.empty?
174
+
175
+ unless @tools.is_a?(Array)
176
+ raise Geminize::ValidationError.new(
177
+ "Tools must be an array, got #{@tools.class}",
178
+ "INVALID_ARGUMENT"
179
+ )
180
+ end
181
+
182
+ @tools.each_with_index do |tool, index|
183
+ unless tool.is_a?(Tool)
184
+ raise Geminize::ValidationError.new(
185
+ "Tool at index #{index} must be a Tool, got #{tool.class}",
186
+ "INVALID_ARGUMENT"
187
+ )
188
+ end
189
+ end
190
+ end
191
+
192
+ # Validate the tool config
193
+ # @raise [Geminize::ValidationError] If the tool config is invalid
194
+ def validate_tool_config!
195
+ return if @tool_config.nil?
196
+
197
+ unless @tool_config.is_a?(ToolConfig)
198
+ raise Geminize::ValidationError.new(
199
+ "Tool config must be a ToolConfig, got #{@tool_config.class}",
200
+ "INVALID_ARGUMENT"
201
+ )
202
+ end
203
+ end
204
+
205
+ # Validate the response MIME type
206
+ # @raise [Geminize::ValidationError] If the response MIME type is invalid
207
+ def validate_response_mime_type!
208
+ return if @response_mime_type.nil?
209
+
210
+ unless @response_mime_type.is_a?(String)
211
+ raise Geminize::ValidationError.new(
212
+ "Response MIME type must be a string, got #{@response_mime_type.class}",
213
+ "INVALID_ARGUMENT"
214
+ )
215
+ end
216
+
217
+ # For now, only allow JSON
218
+ unless @response_mime_type == "application/json"
219
+ raise Geminize::ValidationError.new(
220
+ "Response MIME type must be 'application/json', got #{@response_mime_type}",
221
+ "INVALID_ARGUMENT"
222
+ )
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Extends ContentRequest with safety settings support
6
+ class ContentRequest
7
+ # @return [Array<Geminize::Models::SafetySetting>] The safety settings for this request
8
+ attr_reader :safety_settings
9
+
10
+ # Add a safety setting to the request
11
+ # @param category [String] The harm category this setting applies to
12
+ # @param threshold [String] The threshold level for filtering
13
+ # @return [self] The request object for chaining
14
+ # @raise [Geminize::ValidationError] If the safety setting is invalid
15
+ def add_safety_setting(category, threshold)
16
+ @safety_settings ||= []
17
+
18
+ safety_setting = SafetySetting.new(category, threshold)
19
+ @safety_settings << safety_setting
20
+
21
+ self
22
+ end
23
+
24
+ # Set default safety settings for all harm categories
25
+ # @param threshold [String] The threshold level to apply to all categories
26
+ # @return [self] The request object for chaining
27
+ # @raise [Geminize::ValidationError] If the threshold is invalid
28
+ def set_default_safety_settings(threshold)
29
+ @safety_settings = []
30
+
31
+ SafetySetting::HARM_CATEGORIES.each do |category|
32
+ add_safety_setting(category, threshold)
33
+ end
34
+
35
+ self
36
+ end
37
+
38
+ # Block all harmful content (most conservative setting)
39
+ # @return [self] The request object for chaining
40
+ def block_all_harmful_content
41
+ set_default_safety_settings("BLOCK_LOW_AND_ABOVE")
42
+ end
43
+
44
+ # Block only high-risk content (least conservative setting)
45
+ # @return [self] The request object for chaining
46
+ def block_only_high_risk_content
47
+ set_default_safety_settings("BLOCK_ONLY_HIGH")
48
+ end
49
+
50
+ # Remove all safety settings (use with caution)
51
+ # @return [self] The request object for chaining
52
+ def remove_safety_settings
53
+ @safety_settings = []
54
+ self
55
+ end
56
+
57
+ # Get the base to_hash method - this will use the one defined in content_request_extensions.rb if available
58
+ alias_method :safety_original_to_hash, :to_hash unless method_defined?(:safety_original_to_hash)
59
+
60
+ # Override the to_hash method to include safety settings
61
+ # @return [Hash] The request as a hash
62
+ def to_hash
63
+ # Get the base hash (will include tools if that extension is loaded)
64
+ request = defined?(original_to_hash) ? original_to_hash : safety_original_to_hash
65
+
66
+ # Add safety settings if present
67
+ if @safety_settings && !@safety_settings.empty?
68
+ request[:safetySettings] = @safety_settings.map(&:to_hash)
69
+ end
70
+
71
+ request
72
+ end
73
+
74
+ # Validate method for safety settings - should only be called if not overridden by content_request_extensions.rb
75
+ # If that file's validate! is called, it should also call validate_safety_settings!
76
+ def validate!
77
+ # Don't call super, instead call the necessary validations directly
78
+ # Check if original_validate! is defined from the extensions
79
+ if defined?(original_validate!)
80
+ original_validate!
81
+ else
82
+ # Call the original internal validation methods
83
+ validate_prompt!
84
+ validate_system_instruction! if @system_instruction
85
+ validate_temperature! if @temperature
86
+ validate_max_tokens! if @max_tokens
87
+ validate_top_p! if @top_p
88
+ validate_top_k! if @top_k
89
+ validate_stop_sequences! if @stop_sequences
90
+ validate_content_parts!
91
+ end
92
+
93
+ # Add our safety validation
94
+ validate_safety_settings!
95
+ true
96
+ end
97
+
98
+ private
99
+
100
+ # Validate the safety settings
101
+ # @raise [Geminize::ValidationError] If the safety settings are invalid
102
+ def validate_safety_settings!
103
+ return if @safety_settings.nil? || @safety_settings.empty?
104
+
105
+ unless @safety_settings.is_a?(Array)
106
+ raise Geminize::ValidationError.new(
107
+ "Safety settings must be an array, got #{@safety_settings.class}",
108
+ "INVALID_ARGUMENT"
109
+ )
110
+ end
111
+
112
+ @safety_settings.each_with_index do |setting, index|
113
+ unless setting.is_a?(SafetySetting)
114
+ raise Geminize::ValidationError.new(
115
+ "Safety setting at index #{index} must be a SafetySetting, got #{setting.class}",
116
+ "INVALID_ARGUMENT"
117
+ )
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Extends ContentResponse with function calling capabilities
6
+ class ContentResponse
7
+ # @return [Geminize::Models::FunctionResponse, nil] The function call in the response, if any
8
+ attr_reader :function_call
9
+
10
+ # @return [String, nil] The raw JSON response content, if this is a JSON response
11
+ attr_reader :json_response
12
+
13
+ # @return [Geminize::Models::CodeExecution::ExecutableCode, nil] The executable code in the response, if any
14
+ attr_reader :executable_code
15
+
16
+ # @return [Geminize::Models::CodeExecution::CodeExecutionResult, nil] The code execution result in the response, if any
17
+ attr_reader :code_execution_result
18
+
19
+ # Store the response data for extensions
20
+ alias_method :original_initialize, :initialize
21
+ def initialize(response_data)
22
+ @response_data = response_data
23
+ original_initialize(response_data)
24
+ parse_function_call
25
+ parse_json_response
26
+ parse_code_execution
27
+ end
28
+
29
+ # Determine if the response contains a function call
30
+ # @return [Boolean] true if the response contains a function call
31
+ def has_function_call?
32
+ !@function_call.nil?
33
+ end
34
+
35
+ # Determine if the response contains a JSON response
36
+ # @return [Boolean] true if the response contains a JSON response
37
+ def has_json_response?
38
+ !@json_response.nil?
39
+ end
40
+
41
+ # Determine if the response contains executable code
42
+ # @return [Boolean] true if the response contains executable code
43
+ def has_executable_code?
44
+ !@executable_code.nil?
45
+ end
46
+
47
+ # Determine if the response contains a code execution result
48
+ # @return [Boolean] true if the response contains a code execution result
49
+ def has_code_execution_result?
50
+ !@code_execution_result.nil?
51
+ end
52
+
53
+ private
54
+
55
+ # Parse the function call from the response
56
+ def parse_function_call
57
+ candidates = @response_data.dig("candidates") || []
58
+ return if candidates.empty?
59
+
60
+ content = candidates[0].dig("content") || {}
61
+ parts = content.dig("parts") || []
62
+ function_call_part = parts.find { |part| part.dig("functionCall") }
63
+
64
+ if function_call_part
65
+ function_call_data = function_call_part["functionCall"]
66
+ function_name = function_call_data["name"]
67
+ function_args = function_call_data["args"] || {}
68
+
69
+ @function_call = FunctionResponse.new(function_name, function_args)
70
+ end
71
+ end
72
+
73
+ # Parse JSON response if available
74
+ def parse_json_response
75
+ # First try to check if it's explicitly returned as JSON
76
+ if @response_data.dig("candidates", 0, "content", "parts", 0, "text")
77
+ text = @response_data.dig("candidates", 0, "content", "parts", 0, "text")
78
+ begin
79
+ @json_response = JSON.parse(text)
80
+ return
81
+ rescue JSON::ParserError
82
+ # Not valid JSON, continue checking other methods
83
+ end
84
+ end
85
+
86
+ # Next check if it's returned in structured format
87
+ candidates = @response_data.dig("candidates") || []
88
+ return if candidates.empty?
89
+
90
+ content = candidates[0].dig("content") || {}
91
+ parts = content.dig("parts") || []
92
+ json_part = parts.find { |part| part.key?("structuredValue") }
93
+
94
+ if json_part && json_part["structuredValue"]
95
+ @json_response = json_part["structuredValue"]
96
+ end
97
+ end
98
+
99
+ # Parse code execution data from the response
100
+ def parse_code_execution
101
+ candidates = @response_data.dig("candidates") || []
102
+ return if candidates.empty?
103
+
104
+ content = candidates[0].dig("content") || {}
105
+ parts = content.dig("parts") || []
106
+
107
+ # Find executable code
108
+ executable_code_part = parts.find { |part| part.dig("executableCode") }
109
+ if executable_code_part && executable_code_part["executableCode"]
110
+ code_data = executable_code_part["executableCode"]
111
+ language = code_data["language"] || "PYTHON"
112
+ code = code_data["code"] || ""
113
+
114
+ @executable_code = Geminize::Models::CodeExecution::ExecutableCode.new(language, code)
115
+ end
116
+
117
+ # Find code execution result
118
+ result_part = parts.find { |part| part.dig("codeExecutionResult") }
119
+ if result_part && result_part["codeExecutionResult"]
120
+ result_data = result_part["codeExecutionResult"]
121
+ outcome = result_data["outcome"] || "OUTCOME_OK"
122
+ output = result_data["output"] || ""
123
+
124
+ @code_execution_result = Geminize::Models::CodeExecution::CodeExecutionResult.new(outcome, output)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents a function declaration for function calling in Gemini API
6
+ class FunctionDeclaration
7
+ # @return [String] Name of the function
8
+ attr_reader :name
9
+
10
+ # @return [String] Description of what the function does
11
+ attr_reader :description
12
+
13
+ # @return [Hash] JSON schema for function parameters
14
+ attr_reader :parameters
15
+
16
+ # Initialize a new function declaration
17
+ # @param name [String] The name of the function
18
+ # @param description [String] A description of what the function does
19
+ # @param parameters [Hash] JSON schema for function parameters
20
+ # @raise [Geminize::ValidationError] If the function declaration is invalid
21
+ def initialize(name, description, parameters)
22
+ @name = name
23
+ @description = description
24
+ @parameters = parameters
25
+ validate!
26
+ end
27
+
28
+ # Validate the function declaration
29
+ # @raise [Geminize::ValidationError] If the function declaration is invalid
30
+ # @return [Boolean] true if validation passes
31
+ def validate!
32
+ validate_name!
33
+ validate_description!
34
+ validate_parameters!
35
+ true
36
+ end
37
+
38
+ # Convert the function declaration to a hash for API requests
39
+ # @return [Hash] The function declaration as a hash
40
+ def to_hash
41
+ {
42
+ name: @name,
43
+ description: @description,
44
+ parameters: @parameters
45
+ }
46
+ end
47
+
48
+ # Alias for to_hash
49
+ # @return [Hash] The function declaration as a hash
50
+ def to_h
51
+ to_hash
52
+ end
53
+
54
+ private
55
+
56
+ # Validate the function name
57
+ # @raise [Geminize::ValidationError] If the name is invalid
58
+ def validate_name!
59
+ unless @name.is_a?(String)
60
+ raise Geminize::ValidationError.new(
61
+ "Function name must be a string, got #{@name.class}",
62
+ "INVALID_ARGUMENT"
63
+ )
64
+ end
65
+
66
+ if @name.empty?
67
+ raise Geminize::ValidationError.new(
68
+ "Function name cannot be empty",
69
+ "INVALID_ARGUMENT"
70
+ )
71
+ end
72
+ end
73
+
74
+ # Validate the function description
75
+ # @raise [Geminize::ValidationError] If the description is invalid
76
+ def validate_description!
77
+ unless @description.is_a?(String)
78
+ raise Geminize::ValidationError.new(
79
+ "Function description must be a string, got #{@description.class}",
80
+ "INVALID_ARGUMENT"
81
+ )
82
+ end
83
+
84
+ if @description.empty?
85
+ raise Geminize::ValidationError.new(
86
+ "Function description cannot be empty",
87
+ "INVALID_ARGUMENT"
88
+ )
89
+ end
90
+ end
91
+
92
+ # Validate the function parameters
93
+ # @raise [Geminize::ValidationError] If the parameters are invalid
94
+ def validate_parameters!
95
+ unless @parameters.is_a?(Hash)
96
+ raise Geminize::ValidationError.new(
97
+ "Function parameters must be a hash, got #{@parameters.class}",
98
+ "INVALID_ARGUMENT"
99
+ )
100
+ end
101
+
102
+ # Validate that the parameters follow JSON Schema format
103
+ unless @parameters.key?(:type) || @parameters.key?("type")
104
+ raise Geminize::ValidationError.new(
105
+ "Function parameters must include a 'type' field",
106
+ "INVALID_ARGUMENT"
107
+ )
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents a function response from Gemini API
6
+ class FunctionResponse
7
+ # @return [String] The name of the function that was called
8
+ attr_reader :name
9
+
10
+ # @return [Hash, Array, String, Numeric, Boolean, nil] The response from the function
11
+ attr_reader :response
12
+
13
+ # Initialize a new function response
14
+ # @param name [String] The name of the function that was called
15
+ # @param response [Hash, Array, String, Numeric, Boolean, nil] The response from the function
16
+ def initialize(name, response)
17
+ @name = name
18
+ @response = response
19
+ validate!
20
+ end
21
+
22
+ # Validate the function response
23
+ # @raise [Geminize::ValidationError] If the function response is invalid
24
+ # @return [Boolean] true if validation passes
25
+ def validate!
26
+ if @name.nil? || @name.empty?
27
+ raise Geminize::ValidationError.new(
28
+ "Function name cannot be empty",
29
+ "INVALID_ARGUMENT"
30
+ )
31
+ end
32
+
33
+ true
34
+ end
35
+
36
+ # Create a FunctionResponse from a hash
37
+ # @param hash [Hash] The hash representation of a function response
38
+ # @return [Geminize::Models::FunctionResponse] The function response
39
+ # @raise [Geminize::ValidationError] If the hash is invalid
40
+ def self.from_hash(hash)
41
+ unless hash.is_a?(Hash)
42
+ raise Geminize::ValidationError.new(
43
+ "Expected a Hash, got #{hash.class}",
44
+ "INVALID_ARGUMENT"
45
+ )
46
+ end
47
+
48
+ name = hash["name"] || hash[:name]
49
+ response = hash["response"] || hash[:response]
50
+
51
+ new(name, response)
52
+ end
53
+
54
+ # Convert the function response to a hash
55
+ # @return [Hash] The function response as a hash
56
+ def to_hash
57
+ {
58
+ name: @name,
59
+ response: @response
60
+ }
61
+ end
62
+
63
+ # Alias for to_hash
64
+ # @return [Hash] The function response as a hash
65
+ def to_h
66
+ to_hash
67
+ end
68
+ end
69
+ end
70
+ end