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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +24 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +109 -0
- data/LICENSE.txt +21 -0
- data/README.md +423 -0
- data/Rakefile +10 -0
- data/examples/README.md +75 -0
- data/examples/configuration.rb +58 -0
- data/examples/embeddings.rb +195 -0
- data/examples/multimodal.rb +126 -0
- data/examples/rails_chat/README.md +69 -0
- data/examples/rails_chat/app/controllers/chat_controller.rb +26 -0
- data/examples/rails_chat/app/views/chat/index.html.erb +112 -0
- data/examples/rails_chat/config/routes.rb +8 -0
- data/examples/rails_initializer.rb +46 -0
- data/examples/system_instructions.rb +101 -0
- data/lib/geminize/chat.rb +98 -0
- data/lib/geminize/client.rb +318 -0
- data/lib/geminize/configuration.rb +98 -0
- data/lib/geminize/conversation_repository.rb +161 -0
- data/lib/geminize/conversation_service.rb +126 -0
- data/lib/geminize/embeddings.rb +145 -0
- data/lib/geminize/error_mapper.rb +96 -0
- data/lib/geminize/error_parser.rb +120 -0
- data/lib/geminize/errors.rb +185 -0
- data/lib/geminize/middleware/error_handler.rb +72 -0
- data/lib/geminize/model_info.rb +91 -0
- data/lib/geminize/models/chat_request.rb +186 -0
- data/lib/geminize/models/chat_response.rb +118 -0
- data/lib/geminize/models/content_request.rb +530 -0
- data/lib/geminize/models/content_response.rb +99 -0
- data/lib/geminize/models/conversation.rb +156 -0
- data/lib/geminize/models/embedding_request.rb +222 -0
- data/lib/geminize/models/embedding_response.rb +1064 -0
- data/lib/geminize/models/memory.rb +88 -0
- data/lib/geminize/models/message.rb +140 -0
- data/lib/geminize/models/model.rb +171 -0
- data/lib/geminize/models/model_list.rb +124 -0
- data/lib/geminize/models/stream_response.rb +99 -0
- data/lib/geminize/rails/app/controllers/concerns/geminize/controller.rb +105 -0
- data/lib/geminize/rails/app/helpers/geminize_helper.rb +125 -0
- data/lib/geminize/rails/controller_additions.rb +41 -0
- data/lib/geminize/rails/engine.rb +29 -0
- data/lib/geminize/rails/helper_additions.rb +37 -0
- data/lib/geminize/rails.rb +50 -0
- data/lib/geminize/railtie.rb +33 -0
- data/lib/geminize/request_builder.rb +57 -0
- data/lib/geminize/text_generation.rb +285 -0
- data/lib/geminize/validators.rb +150 -0
- data/lib/geminize/vector_utils.rb +164 -0
- data/lib/geminize/version.rb +5 -0
- data/lib/geminize.rb +527 -0
- data/lib/generators/geminize/install_generator.rb +22 -0
- data/lib/generators/geminize/templates/README +31 -0
- data/lib/generators/geminize/templates/initializer.rb +38 -0
- data/sig/geminize.rbs +4 -0
- 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
|