genai-rb 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +156 -107
- data/lib/genai/chat.rb +206 -203
- data/lib/genai/config.rb +9 -9
- data/lib/genai/model.rb +229 -395
- data/lib/genai/version.rb +1 -1
- data/lib/genai.rb +32 -32
- metadata +11 -54
data/lib/genai/chat.rb
CHANGED
|
@@ -1,203 +1,206 @@
|
|
|
1
|
-
module Genai
|
|
2
|
-
module ChatValidator
|
|
3
|
-
def self.validate_content(content)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return []
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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["
|
|
10
|
-
@base_url = base_url || "https://
|
|
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
|
|
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}
|
|
22
|
+
"#{@base_url}/#{endpoint}"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def headers
|
|
26
26
|
{
|
|
27
27
|
"Content-Type" => "application/json",
|
|
28
|
-
"
|
|
28
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
|
-
end
|
|
32
|
+
end
|