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,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Geminize
6
+ module Models
7
+ # Represents a message in the Gemini format
8
+ class Memory
9
+ # @return [String] The role of the sender (user, model, system)
10
+ attr_reader :role
11
+
12
+ # @return [Array<Hash>] The parts of the message (e.g., text content)
13
+ attr_reader :parts
14
+
15
+ # Initialize a new memory
16
+ # @param role [String] The role of the sender (user, model, system)
17
+ # @param parts [Array<Hash>] The parts of the message
18
+ def initialize(role = "", parts = nil)
19
+ @role = role
20
+ @parts = parts || [{text: ""}]
21
+ @parts.freeze
22
+ end
23
+
24
+ # Convert the memory to a hash suitable for API requests
25
+ # @return [Hash] The memory as a hash
26
+ def to_h
27
+ {
28
+ role: @role,
29
+ parts: @parts
30
+ }
31
+ end
32
+
33
+ # Convert the memory to a JSON string
34
+ # @param opts [Hash] JSON generate options
35
+ # @return [String] The memory as a JSON string
36
+ def to_json(*opts)
37
+ if opts.first && opts.first[:pretty]
38
+ JSON.pretty_generate(to_h)
39
+ else
40
+ to_h.to_json(*opts)
41
+ end
42
+ end
43
+
44
+ # Equality comparison
45
+ # @param other [Object] The object to compare with
46
+ # @return [Boolean] True if the objects are equal
47
+ def ==(other)
48
+ return false unless other.is_a?(Memory)
49
+ role == other.role && parts == other.parts
50
+ end
51
+
52
+ # Create a Memory object from a hash
53
+ # @param hash [Hash] The hash to create from
54
+ # @return [Memory] A new Memory object
55
+ def self.from_hash(hash)
56
+ # Handle both string and symbol keys
57
+ role = hash[:role] || hash["role"] || ""
58
+
59
+ # Handle parts
60
+ parts_data = hash[:parts] || hash["parts"]
61
+ parts = if parts_data
62
+ # Convert string keys to symbols
63
+ parts_data.map do |part|
64
+ if part.is_a?(Hash)
65
+ part_with_symbol_keys = {}
66
+ part.each { |k, v| part_with_symbol_keys[k.to_sym] = v }
67
+ part_with_symbol_keys
68
+ else
69
+ part
70
+ end
71
+ end
72
+ else
73
+ [{text: ""}]
74
+ end
75
+
76
+ new(role, parts)
77
+ end
78
+
79
+ # Create a Memory object from a JSON string
80
+ # @param json [String] The JSON string
81
+ # @return [Memory] A new Memory object
82
+ def self.from_json(json)
83
+ hash = JSON.parse(json)
84
+ from_hash(hash)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Base class for messages in a conversation
6
+ class Message
7
+ # @return [String] The content of the message
8
+ attr_reader :content
9
+
10
+ # @return [Time] When the message was created
11
+ attr_reader :timestamp
12
+
13
+ # @return [String] The role of the message sender (user or model)
14
+ attr_reader :role
15
+
16
+ # Initialize a new message
17
+ # @param content [String] The content of the message
18
+ # @param role [String] The role of the message sender
19
+ # @param timestamp [Time, nil] When the message was created
20
+ def initialize(content, role, timestamp = nil)
21
+ @content = content
22
+ @role = role
23
+ @timestamp = timestamp || Time.now
24
+ validate!
25
+ end
26
+
27
+ # Convert the message to a hash suitable for the API
28
+ # @return [Hash] The message as a hash
29
+ def to_hash
30
+ {
31
+ role: @role,
32
+ parts: [
33
+ {
34
+ text: @content
35
+ }
36
+ ]
37
+ }
38
+ end
39
+
40
+ # Alias for to_hash for consistency with Ruby conventions
41
+ # @return [Hash] The message as a hash
42
+ def to_h
43
+ to_hash
44
+ end
45
+
46
+ # Check if this message is from the user
47
+ # @return [Boolean] True if the message is from the user
48
+ def user?
49
+ @role == "user"
50
+ end
51
+
52
+ # Check if this message is from the model
53
+ # @return [Boolean] True if the message is from the model
54
+ def model?
55
+ @role == "model"
56
+ end
57
+
58
+ # Serialize the message to a JSON string
59
+ # @return [String] The message as a JSON string
60
+ def to_json(*args)
61
+ to_h.to_json(*args)
62
+ end
63
+
64
+ # Create a message from a hash
65
+ # @param hash [Hash] The hash to create the message from
66
+ # @return [Message] A new message object
67
+ def self.from_hash(hash)
68
+ content = extract_content(hash)
69
+ role = hash["role"]
70
+ timestamp = hash["timestamp"] ? Time.parse(hash["timestamp"]) : Time.now
71
+
72
+ case role
73
+ when "user"
74
+ UserMessage.new(content, timestamp)
75
+ when "model"
76
+ ModelMessage.new(content, timestamp)
77
+ else
78
+ new(content, role, timestamp)
79
+ end
80
+ end
81
+
82
+ # Extract content from a message hash
83
+ # @param hash [Hash] The message hash
84
+ # @return [String] The extracted content
85
+ def self.extract_content(hash)
86
+ parts = hash["parts"]
87
+ return "" unless parts && !parts.empty?
88
+
89
+ parts.map { |part| part["text"] }.compact.join(" ")
90
+ end
91
+
92
+ private_class_method :extract_content
93
+
94
+ # Validate the message parameters
95
+ # @raise [Geminize::ValidationError] If any parameter is invalid
96
+ def validate!
97
+ validate_content!
98
+ validate_role!
99
+ end
100
+
101
+ # Validate the content parameter
102
+ # @raise [Geminize::ValidationError] If the content is invalid
103
+ def validate_content!
104
+ Validators.validate_not_empty!(@content, "Content")
105
+ end
106
+
107
+ # Validate the role parameter
108
+ # @raise [Geminize::ValidationError] If the role is invalid
109
+ def validate_role!
110
+ allowed_roles = ["user", "model", "system"]
111
+ unless allowed_roles.include?(@role)
112
+ raise Geminize::ValidationError.new(
113
+ "Role must be one of: #{allowed_roles.join(", ")}",
114
+ "INVALID_ARGUMENT"
115
+ )
116
+ end
117
+ end
118
+ end
119
+
120
+ # Represents a message from the user
121
+ class UserMessage < Message
122
+ # Initialize a new user message
123
+ # @param content [String] The content of the message
124
+ # @param timestamp [Time, nil] When the message was created
125
+ def initialize(content, timestamp = nil)
126
+ super(content, "user", timestamp)
127
+ end
128
+ end
129
+
130
+ # Represents a message from the model
131
+ class ModelMessage < Message
132
+ # Initialize a new model message
133
+ # @param content [String] The content of the message
134
+ # @param timestamp [Time, nil] When the message was created
135
+ def initialize(content, timestamp = nil)
136
+ super(content, "model", timestamp)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents an AI model from the Gemini API.
6
+ class Model
7
+ # @return [String] The unique identifier for the model
8
+ attr_reader :id
9
+
10
+ # @return [String] The display name of the model
11
+ attr_reader :name
12
+
13
+ # @return [String] The model version
14
+ attr_reader :version
15
+
16
+ # @return [String] The model description
17
+ attr_reader :description
18
+
19
+ # @return [Array<String>] List of supported capabilities (e.g., 'text', 'vision', 'embedding')
20
+ attr_reader :capabilities
21
+
22
+ # @return [Hash] Model limitations and constraints
23
+ attr_reader :limitations
24
+
25
+ # @return [Array<String>] Recommended use cases for this model
26
+ attr_reader :use_cases
27
+
28
+ # @return [Hash] Raw model data from the API
29
+ attr_reader :raw_data
30
+
31
+ # Create a new Model instance
32
+ # @param attributes [Hash] Model attributes
33
+ # @option attributes [String] :id The model ID
34
+ # @option attributes [String] :name The model name
35
+ # @option attributes [String] :version The model version
36
+ # @option attributes [String] :description The model description
37
+ # @option attributes [Array<String>] :capabilities List of capabilities
38
+ # @option attributes [Hash] :limitations Model limitations
39
+ # @option attributes [Array<String>] :use_cases Recommended use cases
40
+ # @option attributes [Hash] :raw_data Raw model data from API
41
+ def initialize(attributes = {})
42
+ @id = attributes[:id]
43
+ @name = attributes[:name]
44
+ @version = attributes[:version]
45
+ @description = attributes[:description]
46
+ @capabilities = attributes[:capabilities] || []
47
+ @limitations = attributes[:limitations] || {}
48
+ @use_cases = attributes[:use_cases] || []
49
+ @raw_data = attributes[:raw_data] || {}
50
+ end
51
+
52
+ # Check if model supports a specific capability
53
+ # @param capability [String] Capability to check for
54
+ # @return [Boolean] True if the model supports the capability
55
+ def supports?(capability)
56
+ capabilities.include?(capability.to_s.downcase)
57
+ end
58
+
59
+ # Convert model to a hash representation
60
+ # @return [Hash] Hash representation of the model
61
+ def to_h
62
+ {
63
+ id: id,
64
+ name: name,
65
+ version: version,
66
+ description: description,
67
+ capabilities: capabilities,
68
+ limitations: limitations,
69
+ use_cases: use_cases
70
+ }
71
+ end
72
+
73
+ # Convert model to JSON string
74
+ # @return [String] JSON representation of the model
75
+ def to_json(*args)
76
+ to_h.to_json(*args)
77
+ end
78
+
79
+ # Create a Model from API response data
80
+ # @param data [Hash] Raw API response data
81
+ # @return [Model] New Model instance
82
+ def self.from_api_data(data)
83
+ # Extract capabilities from model data
84
+ capabilities = extract_capabilities(data)
85
+
86
+ # Extract limitations from model data
87
+ limitations = extract_limitations(data)
88
+
89
+ # Extract use cases from model data
90
+ use_cases = extract_use_cases(data)
91
+
92
+ new(
93
+ id: data["name"]&.split("/")&.last,
94
+ name: data["displayName"],
95
+ version: extract_version(data),
96
+ description: data["description"],
97
+ capabilities: capabilities,
98
+ limitations: limitations,
99
+ use_cases: use_cases,
100
+ raw_data: data
101
+ )
102
+ end
103
+
104
+ private_class_method def self.extract_version(data)
105
+ # Extract version from model name or other fields
106
+ # Example: if name is "gemini-1.0-pro", extract "1.0"
107
+ if data["displayName"]
108
+ match = data["displayName"].match(/[-_](\d+\.\d+)[-_]/)
109
+ return match[1] if match
110
+
111
+ # Try another pattern (e.g., "Gemini 1.5 Pro")
112
+ match = data["displayName"].match(/\s(\d+\.\d+)\s/)
113
+ return match[1] if match
114
+ end
115
+ nil
116
+ end
117
+
118
+ private_class_method def self.extract_capabilities(data)
119
+ capabilities = []
120
+
121
+ # Example capability extraction, adjust based on actual API response format
122
+ capabilities << "text" if data.dig("supportedGenerationMethods")&.include?("generateText")
123
+ capabilities << "chat" if data.dig("supportedGenerationMethods")&.include?("generateMessage")
124
+ capabilities << "vision" if data.dig("supportedGenerationMethods")&.include?("generateContent") &&
125
+ data.dig("inputSetting", "supportMultiModal")
126
+ capabilities << "embedding" if data.dig("supportedGenerationMethods")&.include?("embedContent")
127
+
128
+ capabilities
129
+ end
130
+
131
+ private_class_method def self.extract_limitations(data)
132
+ limitations = {}
133
+
134
+ # Extract token limits
135
+ if data.dig("inputTokenLimit")
136
+ limitations[:input_token_limit] = data["inputTokenLimit"]
137
+ end
138
+
139
+ if data.dig("outputTokenLimit")
140
+ limitations[:output_token_limit] = data["outputTokenLimit"]
141
+ end
142
+
143
+ # Extract any other limitations from the API data
144
+ limitations
145
+ end
146
+
147
+ private_class_method def self.extract_use_cases(data)
148
+ # Extract use cases from the description or other fields
149
+ # This is a simple implementation - adjust based on actual API data
150
+ use_cases = []
151
+
152
+ if data["description"]
153
+ if data["description"].include?("chat")
154
+ use_cases << "conversational_ai"
155
+ end
156
+
157
+ if data["description"].include?("vision") || data["description"].include?("image")
158
+ use_cases << "image_understanding"
159
+ end
160
+
161
+ if data["description"].include?("embedding")
162
+ use_cases << "semantic_search"
163
+ use_cases << "clustering"
164
+ end
165
+ end
166
+
167
+ use_cases
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Geminize
6
+ module Models
7
+ # Represents a collection of AI models with filtering capabilities
8
+ class ModelList
9
+ include Enumerable
10
+ extend Forwardable
11
+
12
+ # @return [Array<Model>] The list of models
13
+ attr_reader :models
14
+
15
+ # Delegate array methods to the underlying models array
16
+ def_delegators :@models, :[], :size, :length, :empty?, :first, :last
17
+
18
+ # Create a new ModelList
19
+ # @param models [Array<Model>] Initial list of models
20
+ def initialize(models = [])
21
+ @models = models
22
+ end
23
+
24
+ # Implement Enumerable's required each method
25
+ # @yield [Model] Each model in the list
26
+ def each(&block)
27
+ @models.each(&block)
28
+ end
29
+
30
+ # Add a model to the list
31
+ # @param model [Model] The model to add
32
+ # @return [ModelList] The updated model list
33
+ def add(model)
34
+ @models << model
35
+ self
36
+ end
37
+
38
+ # Find a model by its ID
39
+ # @param id [String] The model ID to search for
40
+ # @return [Model, nil] The found model or nil
41
+ def find_by_id(id)
42
+ @models.find { |model| model.id == id }
43
+ end
44
+
45
+ # Find all models that support a specific capability
46
+ # @param capability [String] The capability to filter by
47
+ # @return [ModelList] A new ModelList containing only matching models
48
+ def filter_by_capability(capability)
49
+ filtered = @models.select { |model| model.supports?(capability) }
50
+ ModelList.new(filtered)
51
+ end
52
+
53
+ # Find all models that support vision capabilities
54
+ # @return [ModelList] A new ModelList containing only vision-capable models
55
+ def vision_models
56
+ filter_by_capability("vision")
57
+ end
58
+
59
+ # Find all models that support embedding capabilities
60
+ # @return [ModelList] A new ModelList containing only embedding-capable models
61
+ def embedding_models
62
+ filter_by_capability("embedding")
63
+ end
64
+
65
+ # Find all models that support text generation
66
+ # @return [ModelList] A new ModelList containing only text generation models
67
+ def text_models
68
+ filter_by_capability("text")
69
+ end
70
+
71
+ # Find all models that support chat/conversation
72
+ # @return [ModelList] A new ModelList containing only chat-capable models
73
+ def chat_models
74
+ filter_by_capability("chat")
75
+ end
76
+
77
+ # Filter models by version
78
+ # @param version [String] The version to filter by
79
+ # @return [ModelList] A new ModelList containing only matching models
80
+ def filter_by_version(version)
81
+ filtered = @models.select { |model| model.version == version }
82
+ ModelList.new(filtered)
83
+ end
84
+
85
+ # Filter models by name pattern
86
+ # @param pattern [String, Regexp] The pattern to match model names against
87
+ # @return [ModelList] A new ModelList containing only matching models
88
+ def filter_by_name(pattern)
89
+ pattern = Regexp.new(pattern.to_s, Regexp::IGNORECASE) if pattern.is_a?(String)
90
+ filtered = @models.select { |model| model.name&.match?(pattern) }
91
+ ModelList.new(filtered)
92
+ end
93
+
94
+ # Create a ModelList from API response data
95
+ # @param data [Hash] API response containing models
96
+ # @return [ModelList] New ModelList instance
97
+ def self.from_api_data(data)
98
+ models = []
99
+
100
+ # Process model data from API response
101
+ # The exact structure will depend on the Gemini API response format
102
+ if data.key?("models")
103
+ models = data["models"].map do |model_data|
104
+ Model.from_api_data(model_data)
105
+ end
106
+ end
107
+
108
+ new(models)
109
+ end
110
+
111
+ # Convert to array of hashes representation
112
+ # @return [Array<Hash>] Array of model hashes
113
+ def to_a
114
+ @models.map(&:to_h)
115
+ end
116
+
117
+ # Convert to JSON string
118
+ # @return [String] JSON representation
119
+ def to_json(*args)
120
+ to_a.to_json(*args)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geminize
4
+ module Models
5
+ # Represents a streaming response chunk from the Gemini API
6
+ class StreamResponse
7
+ # @return [Hash] The raw API response data for this chunk
8
+ attr_reader :raw_chunk
9
+
10
+ # @return [String, nil] The text content in this chunk
11
+ attr_reader :text
12
+
13
+ # @return [String, nil] The finish reason if this is the last chunk
14
+ attr_reader :finish_reason
15
+
16
+ # @return [Hash, nil] Token usage metrics (only available in final chunk)
17
+ attr_reader :usage_metrics
18
+
19
+ # Initialize a new streaming response chunk
20
+ # @param chunk_data [Hash] The raw API response chunk
21
+ def initialize(chunk_data)
22
+ @raw_chunk = chunk_data
23
+ parse_chunk
24
+ end
25
+
26
+ # Check if this is the final chunk in the stream
27
+ # @return [Boolean] True if this is the final chunk
28
+ def final_chunk?
29
+ !@finish_reason.nil?
30
+ end
31
+
32
+ # Check if this chunk has usage metrics
33
+ # @return [Boolean] True if this chunk contains usage metrics
34
+ def has_usage_metrics?
35
+ !@usage_metrics.nil?
36
+ end
37
+
38
+ # Get the prompt token count if available
39
+ # @return [Integer, nil] Prompt token count or nil if not available
40
+ def prompt_tokens
41
+ return nil unless @usage_metrics
42
+ @usage_metrics["promptTokenCount"]
43
+ end
44
+
45
+ # Get the completion token count if available
46
+ # @return [Integer, nil] Completion token count or nil if not available
47
+ def completion_tokens
48
+ return nil unless @usage_metrics
49
+ @usage_metrics["candidatesTokenCount"]
50
+ end
51
+
52
+ # Get the total token count if available
53
+ # @return [Integer, nil] Total token count or nil if not available
54
+ def total_tokens
55
+ return nil unless @usage_metrics
56
+ (prompt_tokens || 0) + (completion_tokens || 0)
57
+ end
58
+
59
+ # Create a StreamResponse object from a raw API response chunk
60
+ # @param chunk_data [Hash] The raw API response chunk
61
+ # @return [StreamResponse] A new StreamResponse object
62
+ def self.from_hash(chunk_data)
63
+ new(chunk_data)
64
+ end
65
+
66
+ private
67
+
68
+ # Parse the chunk data and extract relevant information
69
+ def parse_chunk
70
+ @text = nil
71
+ @finish_reason = nil
72
+ @usage_metrics = nil
73
+
74
+ # First check if the response has the expected fields
75
+ if @raw_chunk.is_a?(Hash)
76
+ candidates = @raw_chunk["candidates"]
77
+ if candidates && !candidates.empty?
78
+ # Extract finish reason if available (last chunk)
79
+ if candidates.first["finishReason"]
80
+ @finish_reason = candidates.first["finishReason"]
81
+ end
82
+
83
+ # Extract text content if available
84
+ content = candidates.first["content"]
85
+ if content && content["parts"] && !content["parts"].empty?
86
+ parts_text = content["parts"].map { |part| part["text"] }.compact
87
+ @text = parts_text.join(" ") unless parts_text.empty?
88
+ end
89
+ end
90
+
91
+ # Extract usage metrics if available
92
+ if @raw_chunk["usageMetadata"]
93
+ @usage_metrics = @raw_chunk["usageMetadata"]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end