genai-rb 0.0.2 → 0.2.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.
data/lib/genai/chat.rb CHANGED
@@ -1,233 +1,206 @@
1
- # Copyright 2025 Google LLC
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- module Genai
16
- # Chat history validation methods
17
- module ChatValidator
18
- def self.validate_content(content)
19
- return false unless content[:parts] && !content[:parts].empty?
20
- content[:parts].each do |part|
21
- return false if part.empty?
22
- return false if part[:text] && part[:text].empty?
23
- end
24
- true
25
- end
26
-
27
- def self.validate_contents(contents)
28
- return false if contents.empty?
29
- contents.each do |content|
30
- return false unless validate_content(content)
31
- end
32
- true
33
- end
34
-
35
- def self.validate_response(response)
36
- return false unless response[:candidates] && !response[:candidates].empty?
37
- return false unless response[:candidates][0][:content]
38
- validate_content(response[:candidates][0][:content])
39
- end
40
-
41
- def self.extract_curated_history(comprehensive_history)
42
- return [] if comprehensive_history.empty?
43
-
44
- curated_history = []
45
- length = comprehensive_history.length
46
- i = 0
47
-
48
- while i < length
49
- unless ["user", "model"].include?(comprehensive_history[i][:role])
50
- raise ArgumentError, "Role must be user or model, but got #{comprehensive_history[i][:role]}"
51
- end
52
-
53
- if comprehensive_history[i][:role] == "user"
54
- current_input = comprehensive_history[i]
55
- curated_history << current_input
56
- i += 1
57
- else
58
- current_output = []
59
- is_valid = true
60
-
61
- while i < length && comprehensive_history[i][:role] == "model"
62
- current_output << comprehensive_history[i]
63
- if is_valid && !validate_content(comprehensive_history[i])
64
- is_valid = false
65
- end
66
- i += 1
67
- end
68
-
69
- if is_valid
70
- curated_history.concat(current_output)
71
- elsif !curated_history.empty?
72
- curated_history.pop
73
- end
74
- end
75
- end
76
-
77
- curated_history
78
- end
79
- end
80
-
81
- class BaseChat
82
- attr_reader :model, :config, :comprehensive_history, :curated_history
83
-
84
- def initialize(model:, config: nil, history: [])
85
- @model = model
86
- @config = config
87
-
88
- # Convert history items to proper format
89
- content_models = history.map do |content|
90
- content.is_a?(Hash) ? content : content
91
- end
92
-
93
- @comprehensive_history = content_models
94
- @curated_history = ChatValidator.extract_curated_history(content_models)
95
- end
96
-
97
- def record_history(user_input:, model_output:, automatic_function_calling_history: [], is_valid: true)
98
- # Extract input contents from automatic function calling history or use user input
99
- input_contents = if !automatic_function_calling_history.empty?
100
- # Deduplicate existing chat history from AFC history
101
- automatic_function_calling_history[@curated_history.length..-1] || [user_input]
102
- else
103
- [user_input]
104
- end
105
-
106
- # Use model output or create empty content if no output
107
- output_contents = model_output.empty? ? [{ role: "model", parts: [] }] : model_output
108
-
109
- # Update comprehensive history
110
- @comprehensive_history.concat(input_contents)
111
- @comprehensive_history.concat(output_contents)
112
-
113
- # Update curated history only if valid
114
- if is_valid
115
- @curated_history.concat(input_contents)
116
- @curated_history.concat(output_contents)
117
- end
118
- end
119
-
120
- def get_history(curated: false)
121
- curated ? @curated_history : @comprehensive_history
122
- end
123
- end
124
-
125
- class Chat < BaseChat
126
- def initialize(client:, model:, config: nil, history: [])
127
- @client = client
128
- super(model: model, config: config, history: history)
129
- end
130
-
131
- def send_message(message, config: nil)
132
- # Convert message to proper content format
133
- input_content = case message
134
- when String
135
- { role: "user", parts: [{ text: message }] }
136
- when Array
137
- { role: "user", parts: message }
138
- when Hash
139
- message
140
- else
141
- raise ArgumentError, "Message must be a String, Array, or Hash"
142
- end
143
-
144
- # Generate content using the model
145
- model_instance = @client.model(@model)
146
- response = model_instance.generate_content(
147
- contents: @curated_history + [input_content],
148
- config: config || @config
149
- )
150
-
151
- # Extract model output from response
152
- model_output = if response[:candidates] && !response[:candidates].empty? && response[:candidates][0][:content]
153
- [response[:candidates][0][:content]]
154
- else
155
- []
156
- end
157
-
158
- # Get automatic function calling history if available
159
- automatic_function_calling_history = response[:automatic_function_calling_history] || []
160
-
161
- # Record the conversation in history
162
- record_history(
163
- user_input: input_content,
164
- model_output: model_output,
165
- automatic_function_calling_history: automatic_function_calling_history,
166
- is_valid: ChatValidator.validate_response(response)
167
- )
168
-
169
- response
170
- end
171
-
172
- def send_message_stream(message, config: nil)
173
- # Convert message to proper content format
174
- input_content = case message
175
- when String
176
- { role: "user", parts: [{ text: message }] }
177
- when Array
178
- { role: "user", parts: message }
179
- when Hash
180
- message
181
- else
182
- raise ArgumentError, "Message must be a String, Array, or Hash"
183
- end
184
-
185
- # This would need to be implemented in the Model class to support streaming
186
- # For now, we'll use regular send_message
187
- send_message(message, config: config)
188
- end
189
- end
190
-
191
- class Chats
192
- def initialize(client)
193
- @client = client
194
- @user_sessions = {}
195
- end
196
-
197
- def create(model:, config: nil, history: [])
198
- Chat.new(client: @client, model: model, config: config, history: history)
199
- end
200
-
201
- # 사용자별 세션 관리 메소드들
202
- def get_user_session(user_id, model: "gemini-2.0-flash", config: nil)
203
- # 사용자별 세션이 없으면 새로 생성
204
- unless @user_sessions[user_id]
205
- @user_sessions[user_id] = create(
206
- model: model,
207
- config: config || {
208
- temperature: 0.7,
209
- max_output_tokens: 2048
210
- }
211
- )
212
- end
213
-
214
- @user_sessions[user_id]
215
- end
216
-
217
- def clear_user_session(user_id)
218
- @user_sessions.delete(user_id)
219
- end
220
-
221
- def clear_all_sessions
222
- @user_sessions.clear
223
- end
224
-
225
- def get_session_count
226
- @user_sessions.length
227
- end
228
-
229
- def get_active_users
230
- @user_sessions.keys
231
- end
232
- end
233
- end
1
+ module Genai
2
+ module ChatValidator
3
+ def self.validate_content(content)
4
+ message_content = content[:content] || content["content"]
5
+ return true if message_content.is_a?(String) && !message_content.empty?
6
+
7
+ parts = content[:parts] || content["parts"]
8
+ return false unless parts && !parts.empty?
9
+ parts.each do |part|
10
+ return false if part.empty?
11
+ text = part[:text] || part["text"]
12
+ return false if text && text.empty?
13
+ end
14
+ true
15
+ end
16
+
17
+ def self.validate_contents(contents)
18
+ return false if contents.empty?
19
+ contents.each do |content|
20
+ return false unless validate_content(content)
21
+ end
22
+ true
23
+ end
24
+
25
+ def self.validate_response(response)
26
+ return false unless response[:candidates] && !response[:candidates].empty?
27
+ return false unless response[:candidates][0][:content]
28
+ validate_content(response[:candidates][0][:content])
29
+ end
30
+
31
+ def self.extract_curated_history(comprehensive_history)
32
+ return [] if comprehensive_history.empty?
33
+
34
+ curated_history = []
35
+ length = comprehensive_history.length
36
+ i = 0
37
+
38
+ while i < length
39
+ role = comprehensive_history[i][:role] || comprehensive_history[i]["role"]
40
+
41
+ unless ["user", "model", "assistant", "system", "developer"].include?(role)
42
+ raise ArgumentError, "Unsupported role: #{role}"
43
+ end
44
+
45
+ if ["user", "system", "developer"].include?(role)
46
+ current_input = comprehensive_history[i]
47
+ curated_history << current_input
48
+ i += 1
49
+ else
50
+ current_output = []
51
+ is_valid = true
52
+
53
+ while i < length
54
+ current_role = comprehensive_history[i][:role] || comprehensive_history[i]["role"]
55
+ break unless ["model", "assistant"].include?(current_role)
56
+
57
+ current_output << comprehensive_history[i]
58
+ if is_valid && !validate_content(comprehensive_history[i])
59
+ is_valid = false
60
+ end
61
+ i += 1
62
+ end
63
+
64
+ if is_valid
65
+ curated_history.concat(current_output)
66
+ elsif !curated_history.empty?
67
+ curated_history.pop
68
+ end
69
+ end
70
+ end
71
+
72
+ curated_history
73
+ end
74
+ end
75
+
76
+ class BaseChat
77
+ attr_reader :model, :config, :comprehensive_history, :curated_history
78
+
79
+ def initialize(model:, config: nil, history: [])
80
+ @model = model
81
+ @config = config
82
+
83
+ content_models = history.map do |content|
84
+ content.is_a?(Hash) ? content : content
85
+ end
86
+
87
+ @comprehensive_history = content_models
88
+ @curated_history = ChatValidator.extract_curated_history(content_models)
89
+ end
90
+
91
+ def record_history(user_input:, model_output:, automatic_function_calling_history: [], is_valid: true)
92
+ input_contents = if !automatic_function_calling_history.empty?
93
+ automatic_function_calling_history[@curated_history.length..-1] || [user_input]
94
+ else
95
+ [user_input]
96
+ end
97
+
98
+ output_contents = model_output.empty? ? [{ role: "model", parts: [] }] : model_output
99
+
100
+ @comprehensive_history.concat(input_contents)
101
+ @comprehensive_history.concat(output_contents)
102
+
103
+ if is_valid
104
+ @curated_history.concat(input_contents)
105
+ @curated_history.concat(output_contents)
106
+ end
107
+ end
108
+
109
+ def get_history(curated: false)
110
+ curated ? @curated_history : @comprehensive_history
111
+ end
112
+ end
113
+
114
+ class Chat < BaseChat
115
+ def initialize(client:, model:, config: nil, history: [])
116
+ @client = client
117
+ super(model: model, config: config, history: history)
118
+ end
119
+
120
+ def send_message(message, config: nil)
121
+ input_content = case message
122
+ when String
123
+ { role: "user", parts: [{ text: message }] }
124
+ when Array
125
+ { role: "user", parts: message }
126
+ when Hash
127
+ message
128
+ else
129
+ raise ArgumentError, "Message must be a String, Array, or Hash"
130
+ end
131
+
132
+ model_instance = @client.model(@model)
133
+ response = model_instance.generate_content(
134
+ contents: @curated_history + [input_content],
135
+ config: config || @config
136
+ )
137
+
138
+ model_output = response.to_s.empty? ? [] : [{ role: "model", parts: [{ text: response.to_s }] }]
139
+
140
+ record_history(
141
+ user_input: input_content,
142
+ model_output: model_output,
143
+ is_valid: !response.to_s.empty?
144
+ )
145
+
146
+ response
147
+ end
148
+
149
+ def send_message_stream(message, config: nil)
150
+ input_content = case message
151
+ when String
152
+ { role: "user", parts: [{ text: message }] }
153
+ when Array
154
+ { role: "user", parts: message }
155
+ when Hash
156
+ message
157
+ else
158
+ raise ArgumentError, "Message must be a String, Array, or Hash"
159
+ end
160
+
161
+
162
+ send_message(message, config: config)
163
+ end
164
+ end
165
+
166
+ class Chats
167
+ def initialize(client)
168
+ @client = client
169
+ @user_sessions = {}
170
+ end
171
+
172
+ def create(model:, config: nil, history: [])
173
+ Chat.new(client: @client, model: model, config: config, history: history)
174
+ end
175
+
176
+ def get_user_session(user_id, model: "gpt-5.5", config: nil)
177
+ unless @user_sessions[user_id]
178
+ @user_sessions[user_id] = create(
179
+ model: model,
180
+ config: config || {
181
+ temperature: 0.7,
182
+ max_tokens: 2048
183
+ }
184
+ )
185
+ end
186
+
187
+ @user_sessions[user_id]
188
+ end
189
+
190
+ def clear_user_session(user_id)
191
+ @user_sessions.delete(user_id)
192
+ end
193
+
194
+ def clear_all_sessions
195
+ @user_sessions.clear
196
+ end
197
+
198
+ def get_session_count
199
+ @user_sessions.length
200
+ end
201
+
202
+ def get_active_users
203
+ @user_sessions.keys
204
+ end
205
+ end
206
+ end
data/lib/genai/config.rb CHANGED
@@ -1,13 +1,13 @@
1
- require "net/http"
2
- require "json"
3
-
1
+ require "net/http"
2
+ require "json"
3
+
4
4
  module Genai
5
5
  class Config
6
6
  attr_accessor :api_key, :base_url, :timeout, :max_retries
7
7
 
8
8
  def initialize(api_key: nil, base_url: nil, timeout: 60, max_retries: 3)
9
- @api_key = api_key || ENV["GEMINI_API_KEY"]
10
- @base_url = base_url || "https://generativelanguage.googleapis.com"
9
+ @api_key = api_key || ENV["OPENAI_API_KEY"]
10
+ @base_url = (base_url || ENV["OPENAI_BASE_URL"] || "https://api.openai.com/v1").sub(%r{/\z}, "")
11
11
  @timeout = timeout
12
12
  @max_retries = max_retries
13
13
 
@@ -15,18 +15,18 @@ module Genai
15
15
  end
16
16
 
17
17
  def validate_config!
18
- raise Error, "API key is required. Set GEMINI_API_KEY environment variable or pass api_key parameter." if @api_key.nil? || @api_key.empty?
18
+ raise Error, "API key is required. Set OPENAI_API_KEY environment variable or pass api_key parameter." if @api_key.nil? || @api_key.empty?
19
19
  end
20
20
 
21
21
  def api_url(endpoint)
22
- "#{@base_url}/v1beta/#{endpoint}"
22
+ "#{@base_url}/#{endpoint}"
23
23
  end
24
24
 
25
25
  def headers
26
26
  {
27
27
  "Content-Type" => "application/json",
28
- "x-goog-api-key" => @api_key
28
+ "Authorization" => "Bearer #{@api_key}"
29
29
  }
30
30
  end
31
31
  end
32
- end
32
+ end