geminize 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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/.yardopts +14 -0
  5. data/CHANGELOG.md +24 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/CONTRIBUTING.md +109 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +423 -0
  10. data/Rakefile +10 -0
  11. data/examples/README.md +75 -0
  12. data/examples/configuration.rb +58 -0
  13. data/examples/embeddings.rb +195 -0
  14. data/examples/multimodal.rb +126 -0
  15. data/examples/rails_chat/README.md +69 -0
  16. data/examples/rails_chat/app/controllers/chat_controller.rb +26 -0
  17. data/examples/rails_chat/app/views/chat/index.html.erb +112 -0
  18. data/examples/rails_chat/config/routes.rb +8 -0
  19. data/examples/rails_initializer.rb +46 -0
  20. data/examples/system_instructions.rb +101 -0
  21. data/lib/geminize/chat.rb +98 -0
  22. data/lib/geminize/client.rb +318 -0
  23. data/lib/geminize/configuration.rb +98 -0
  24. data/lib/geminize/conversation_repository.rb +161 -0
  25. data/lib/geminize/conversation_service.rb +126 -0
  26. data/lib/geminize/embeddings.rb +145 -0
  27. data/lib/geminize/error_mapper.rb +96 -0
  28. data/lib/geminize/error_parser.rb +120 -0
  29. data/lib/geminize/errors.rb +185 -0
  30. data/lib/geminize/middleware/error_handler.rb +72 -0
  31. data/lib/geminize/model_info.rb +91 -0
  32. data/lib/geminize/models/chat_request.rb +186 -0
  33. data/lib/geminize/models/chat_response.rb +118 -0
  34. data/lib/geminize/models/content_request.rb +530 -0
  35. data/lib/geminize/models/content_response.rb +99 -0
  36. data/lib/geminize/models/conversation.rb +156 -0
  37. data/lib/geminize/models/embedding_request.rb +222 -0
  38. data/lib/geminize/models/embedding_response.rb +1064 -0
  39. data/lib/geminize/models/memory.rb +88 -0
  40. data/lib/geminize/models/message.rb +140 -0
  41. data/lib/geminize/models/model.rb +171 -0
  42. data/lib/geminize/models/model_list.rb +124 -0
  43. data/lib/geminize/models/stream_response.rb +99 -0
  44. data/lib/geminize/rails/app/controllers/concerns/geminize/controller.rb +105 -0
  45. data/lib/geminize/rails/app/helpers/geminize_helper.rb +125 -0
  46. data/lib/geminize/rails/controller_additions.rb +41 -0
  47. data/lib/geminize/rails/engine.rb +29 -0
  48. data/lib/geminize/rails/helper_additions.rb +37 -0
  49. data/lib/geminize/rails.rb +50 -0
  50. data/lib/geminize/railtie.rb +33 -0
  51. data/lib/geminize/request_builder.rb +57 -0
  52. data/lib/geminize/text_generation.rb +285 -0
  53. data/lib/geminize/validators.rb +150 -0
  54. data/lib/geminize/vector_utils.rb +164 -0
  55. data/lib/geminize/version.rb +5 -0
  56. data/lib/geminize.rb +527 -0
  57. data/lib/generators/geminize/install_generator.rb +22 -0
  58. data/lib/generators/geminize/templates/README +31 -0
  59. data/lib/generators/geminize/templates/initializer.rb +38 -0
  60. data/sig/geminize.rbs +4 -0
  61. metadata +218 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "ostruct"
5
+
6
+ module Geminize
7
+ module Middleware
8
+ # Faraday middleware for handling API error responses
9
+ class ErrorHandler < Faraday::Middleware
10
+ # @return [Array<Integer>] HTTP status codes that trigger error handling
11
+ attr_reader :error_statuses
12
+
13
+ # Initialize the middleware
14
+ # @param app [#call] The Faraday app
15
+ # @param options [Hash] Configuration options
16
+ # @option options [Array<Integer>] :error_statuses HTTP status codes to handle as errors (default: 400..599)
17
+ def initialize(app, options = {})
18
+ super(app)
19
+ @error_statuses = options.fetch(:error_statuses, 400..599).to_a
20
+ end
21
+
22
+ # Execute the middleware
23
+ # @param env [Faraday::Env] The request environment
24
+ def call(env)
25
+ @app.call(env).on_complete do |response_env|
26
+ on_complete(response_env) if error_statuses.include?(response_env.status)
27
+ end
28
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
29
+ # Handle network errors
30
+ raise Geminize::RequestError.new(
31
+ "Network error: #{e.message}",
32
+ "CONNECTION_ERROR",
33
+ nil
34
+ )
35
+ end
36
+
37
+ # Process the API response
38
+ # @param env [Faraday::Env] The response environment
39
+ # @raise [Geminize::GeminizeError] The appropriate exception based on the error
40
+ def on_complete(env)
41
+ # Create a simplified response object that we can pass to our parser
42
+ response = build_response_for_parser(env)
43
+
44
+ # Parse the error response
45
+ error_info = ErrorParser.parse(response)
46
+
47
+ # Map to appropriate exception and raise
48
+ exception = ErrorMapper.map(error_info)
49
+ raise exception
50
+ end
51
+
52
+ private
53
+
54
+ # Build a simplified response object from the Faraday environment
55
+ # @param env [Faraday::Env] The Faraday environment
56
+ # @return [OpenStruct] A simplified response-like object
57
+ def build_response_for_parser(env)
58
+ # Create a simple struct that mimics the interface expected by ErrorParser
59
+ OpenStruct.new(
60
+ status: env.status,
61
+ body: env.body,
62
+ headers: env.response_headers
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # Register the middleware with Faraday
70
+ Faraday::Response.register_middleware(
71
+ geminize_error_handler: -> { Geminize::Middleware::ErrorHandler }
72
+ )
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "models/model"
4
+ require_relative "models/model_list"
5
+
6
+ module Geminize
7
+ # Handles retrieving model information from the Gemini API
8
+ class ModelInfo
9
+ # @return [Geminize::Client] The HTTP client
10
+ attr_reader :client
11
+
12
+ # Initialize a new ModelInfo instance
13
+ # @param client [Geminize::Client, nil] The HTTP client to use
14
+ # @param options [Hash] Additional options for the client
15
+ def initialize(client = nil, options = {})
16
+ @client = client || Client.new(options)
17
+ @cache = {}
18
+ @cache_expiry = {}
19
+ @cache_ttl = options[:cache_ttl] || 3600 # Default to 1 hour
20
+ end
21
+
22
+ # List available models from the Gemini API
23
+ # @param force_refresh [Boolean] Force a refresh from the API instead of using cache
24
+ # @return [Geminize::Models::ModelList] List of available models
25
+ # @raise [Geminize::GeminizeError] If the request fails
26
+ def list_models(force_refresh: false)
27
+ cache_key = "models_list"
28
+
29
+ # Check if we have a valid cached result
30
+ if !force_refresh && @cache[cache_key] && @cache_expiry[cache_key] > Time.now
31
+ return @cache[cache_key]
32
+ end
33
+
34
+ # Make the API request
35
+ response = client.get("models")
36
+
37
+ # Create a ModelList from the response
38
+ model_list = Models::ModelList.from_api_data(response)
39
+
40
+ # Cache the result
41
+ @cache[cache_key] = model_list
42
+ @cache_expiry[cache_key] = Time.now + @cache_ttl
43
+
44
+ model_list
45
+ end
46
+
47
+ # Get information about a specific model
48
+ # @param model_id [String] The model ID to retrieve
49
+ # @param force_refresh [Boolean] Force a refresh from the API instead of using cache
50
+ # @return [Geminize::Models::Model] The model information
51
+ # @raise [Geminize::GeminizeError] If the request fails or model is not found
52
+ def get_model(model_id, force_refresh: false)
53
+ cache_key = "model_#{model_id}"
54
+
55
+ # Check if we have a valid cached result
56
+ if !force_refresh && @cache[cache_key] && @cache_expiry[cache_key] > Time.now
57
+ return @cache[cache_key]
58
+ end
59
+
60
+ # Make the API request
61
+ begin
62
+ response = client.get("models/#{model_id}")
63
+
64
+ # Create a Model from the response
65
+ model = Models::Model.from_api_data(response)
66
+
67
+ # Cache the result
68
+ @cache[cache_key] = model
69
+ @cache_expiry[cache_key] = Time.now + @cache_ttl
70
+
71
+ model
72
+ rescue Geminize::NotFoundError => e
73
+ # Re-raise with a more descriptive message
74
+ raise Geminize::NotFoundError.new("Model '#{model_id}' not found", e.code, e.http_status)
75
+ end
76
+ end
77
+
78
+ # Clear all cached model information
79
+ # @return [void]
80
+ def clear_cache
81
+ @cache = {}
82
+ @cache_expiry = {}
83
+ nil
84
+ end
85
+
86
+ # Set the cache time-to-live (TTL)
87
+ # @param seconds [Integer] TTL in seconds
88
+ # @return [void]
89
+ attr_writer :cache_ttl
90
+ end
91
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents a chat request to the Gemini API
6
+ class ChatRequest
7
+ # @return [String] The user's message content
8
+ attr_reader :content
9
+
10
+ # @return [String, nil] Optional user identifier
11
+ attr_reader :user_id
12
+
13
+ # @return [String] The model name to use
14
+ attr_reader :model_name
15
+
16
+ # @return [Time] When the message was created
17
+ attr_reader :timestamp
18
+
19
+ # @return [Float] Temperature (controls randomness)
20
+ attr_accessor :temperature
21
+
22
+ # @return [Integer] Maximum tokens to generate
23
+ attr_accessor :max_tokens
24
+
25
+ # @return [Float] Top-p value for nucleus sampling
26
+ attr_accessor :top_p
27
+
28
+ # @return [Integer] Top-k value for sampling
29
+ attr_accessor :top_k
30
+
31
+ # @return [Array<String>] Stop sequences to end generation
32
+ attr_accessor :stop_sequences
33
+
34
+ # @return [String, nil] System instruction to guide model behavior
35
+ attr_accessor :system_instruction
36
+
37
+ # Initialize a new chat request
38
+ # @param content [String] The user's message content
39
+ # @param model_name [String, nil] The model name to use
40
+ # @param user_id [String, nil] Optional user identifier
41
+ # @param params [Hash] Additional parameters
42
+ # @option params [Float] :temperature Controls randomness (0.0-1.0)
43
+ # @option params [Integer] :max_tokens Maximum tokens to generate
44
+ # @option params [Float] :top_p Top-p value for nucleus sampling (0.0-1.0)
45
+ # @option params [Integer] :top_k Top-k value for sampling
46
+ # @option params [Array<String>] :stop_sequences Stop sequences to end generation
47
+ # @option params [String] :system_instruction System instruction to guide model behavior
48
+ def initialize(content, model_name = nil, user_id = nil, params = {})
49
+ @content = content
50
+ @model_name = model_name || Geminize.configuration.default_model
51
+ @user_id = user_id
52
+ @timestamp = Time.now
53
+ @temperature = params[:temperature]
54
+ @max_tokens = params[:max_tokens]
55
+ @top_p = params[:top_p]
56
+ @top_k = params[:top_k]
57
+ @stop_sequences = params[:stop_sequences]
58
+ @system_instruction = params[:system_instruction]
59
+
60
+ validate!
61
+ end
62
+
63
+ # Validate the request parameters
64
+ # @raise [Geminize::ValidationError] If any parameter is invalid
65
+ # @return [Boolean] true if all parameters are valid
66
+ def validate!
67
+ validate_content!
68
+ validate_temperature!
69
+ validate_max_tokens!
70
+ validate_top_p!
71
+ validate_top_k!
72
+ validate_stop_sequences!
73
+ validate_system_instruction!
74
+ true
75
+ end
76
+
77
+ # Convert the request to a hash for forming a single message
78
+ # @return [Hash] A hash representation of the user message
79
+ def to_message_hash
80
+ {
81
+ role: "user",
82
+ parts: [
83
+ {
84
+ text: @content
85
+ }
86
+ ]
87
+ }
88
+ end
89
+
90
+ # Convert the request to a hash suitable for the API
91
+ # @param history [Array<Hash>] Previous messages in the conversation
92
+ # @return [Hash] The request as a hash
93
+ def to_hash(history = [])
94
+ request = {
95
+ contents: history + [to_message_hash],
96
+ generationConfig: generation_config
97
+ }.compact
98
+
99
+ # Add system_instruction if provided
100
+ if @system_instruction
101
+ request[:systemInstruction] = {
102
+ parts: [
103
+ {
104
+ text: @system_instruction
105
+ }
106
+ ]
107
+ }
108
+ end
109
+
110
+ request
111
+ end
112
+
113
+ # Alias for to_hash for consistency with Ruby conventions
114
+ # @param history [Array<Hash>] Previous messages in the conversation
115
+ # @return [Hash] The request as a hash
116
+ def to_h(history = [])
117
+ to_hash(history)
118
+ end
119
+
120
+ private
121
+
122
+ # Build the generation configuration hash
123
+ # @return [Hash] The generation configuration
124
+ def generation_config
125
+ config = {}
126
+ config[:temperature] = @temperature if @temperature
127
+ config[:maxOutputTokens] = @max_tokens if @max_tokens
128
+ config[:topP] = @top_p if @top_p
129
+ config[:topK] = @top_k if @top_k
130
+ config[:stopSequences] = @stop_sequences if @stop_sequences && !@stop_sequences.empty?
131
+
132
+ config.empty? ? nil : config
133
+ end
134
+
135
+ # Validate the content parameter
136
+ # @raise [Geminize::ValidationError] If the content is invalid
137
+ def validate_content!
138
+ Validators.validate_not_empty!(@content, "Content")
139
+ end
140
+
141
+ # Validate the system_instruction parameter
142
+ # @raise [Geminize::ValidationError] If the system_instruction is invalid
143
+ def validate_system_instruction!
144
+ return if @system_instruction.nil?
145
+
146
+ unless @system_instruction.is_a?(String)
147
+ raise Geminize::ValidationError.new("System instruction must be a string", "INVALID_ARGUMENT")
148
+ end
149
+
150
+ if @system_instruction.empty?
151
+ raise Geminize::ValidationError.new("System instruction cannot be empty", "INVALID_ARGUMENT")
152
+ end
153
+ end
154
+
155
+ # Validate the temperature parameter
156
+ # @raise [Geminize::ValidationError] If the temperature is invalid
157
+ def validate_temperature!
158
+ Validators.validate_probability!(@temperature, "Temperature")
159
+ end
160
+
161
+ # Validate the max_tokens parameter
162
+ # @raise [Geminize::ValidationError] If the max_tokens is invalid
163
+ def validate_max_tokens!
164
+ Validators.validate_positive_integer!(@max_tokens, "Max tokens")
165
+ end
166
+
167
+ # Validate the top_p parameter
168
+ # @raise [Geminize::ValidationError] If the top_p is invalid
169
+ def validate_top_p!
170
+ Validators.validate_probability!(@top_p, "Top-p")
171
+ end
172
+
173
+ # Validate the top_k parameter
174
+ # @raise [Geminize::ValidationError] If the top_k is invalid
175
+ def validate_top_k!
176
+ Validators.validate_positive_integer!(@top_k, "Top-k")
177
+ end
178
+
179
+ # Validate the stop_sequences parameter
180
+ # @raise [Geminize::ValidationError] If the stop_sequences is invalid
181
+ def validate_stop_sequences!
182
+ Validators.validate_string_array!(@stop_sequences, "Stop sequences")
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents a chat response from the Gemini API
6
+ class ChatResponse
7
+ # @return [Hash] The raw API response data
8
+ attr_reader :raw_response
9
+
10
+ # @return [String, nil] The reason why generation stopped (if applicable)
11
+ attr_reader :finish_reason
12
+
13
+ # @return [Hash, nil] Token counts for the request and response
14
+ attr_reader :usage
15
+
16
+ # @return [Time] When the response was created
17
+ attr_reader :timestamp
18
+
19
+ # Initialize a new chat response
20
+ # @param response_data [Hash] The raw API response
21
+ def initialize(response_data)
22
+ @raw_response = response_data
23
+ @timestamp = Time.now
24
+ parse_response
25
+ end
26
+
27
+ # Get the generated text from the response
28
+ # @return [String, nil] The generated text or nil if no text was generated
29
+ def text
30
+ return @text if defined?(@text)
31
+
32
+ @text = nil
33
+ candidates = @raw_response["candidates"]
34
+ if candidates && !candidates.empty?
35
+ content = candidates.first["content"]
36
+ if content && content["parts"] && !content["parts"].empty?
37
+ parts_text = content["parts"].map { |part| part["text"] }.compact
38
+ @text = parts_text.join(" ") unless parts_text.empty?
39
+ end
40
+ end
41
+ @text
42
+ end
43
+
44
+ # Get the response as a formatted message hash
45
+ # @return [Hash] Message in format suitable for conversation history
46
+ def to_message_hash
47
+ candidates = @raw_response["candidates"]
48
+ return nil unless candidates && !candidates.empty?
49
+
50
+ content = candidates.first["content"]
51
+ return nil unless content && content["parts"] && !content["parts"].empty?
52
+
53
+ {
54
+ role: "model",
55
+ parts: content["parts"]
56
+ }
57
+ end
58
+
59
+ # Check if the response has generated text
60
+ # @return [Boolean] True if the response has generated text
61
+ def has_text?
62
+ !text.nil? && !text.empty?
63
+ end
64
+
65
+ # Get the total token count
66
+ # @return [Integer, nil] Total token count or nil if not available
67
+ def total_tokens
68
+ return nil unless @usage
69
+
70
+ (@usage["promptTokenCount"] || 0) + (@usage["candidatesTokenCount"] || 0)
71
+ end
72
+
73
+ # Get the prompt token count
74
+ # @return [Integer, nil] Prompt token count or nil if not available
75
+ def prompt_tokens
76
+ return nil unless @usage
77
+
78
+ @usage["promptTokenCount"]
79
+ end
80
+
81
+ # Get the completion token count
82
+ # @return [Integer, nil] Completion token count or nil if not available
83
+ def completion_tokens
84
+ return nil unless @usage
85
+
86
+ @usage["candidatesTokenCount"]
87
+ end
88
+
89
+ # Create a ChatResponse object from a raw API response
90
+ # @param response_data [Hash] The raw API response
91
+ # @return [ChatResponse] A new ChatResponse object
92
+ def self.from_hash(response_data)
93
+ new(response_data)
94
+ end
95
+
96
+ private
97
+
98
+ # Parse the response data and extract relevant information
99
+ def parse_response
100
+ parse_finish_reason
101
+ parse_usage
102
+ end
103
+
104
+ # Parse the finish reason from the response
105
+ def parse_finish_reason
106
+ candidates = @raw_response["candidates"]
107
+ if candidates && !candidates.empty? && candidates.first["finishReason"]
108
+ @finish_reason = candidates.first["finishReason"]
109
+ end
110
+ end
111
+
112
+ # Parse usage information from the response
113
+ def parse_usage
114
+ @usage = @raw_response["usageMetadata"] if @raw_response["usageMetadata"]
115
+ end
116
+ end
117
+ end
118
+ end