ai-chat 0.5.8 → 0.6.1
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/README.md +303 -671
- data/ai-chat.gemspec +2 -2
- data/lib/ai/chat.rb +73 -31
- metadata +3 -3
data/ai-chat.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "ai-chat"
|
|
5
|
-
spec.version = "0.
|
|
5
|
+
spec.version = "0.6.1"
|
|
6
6
|
spec.authors = ["Raghu Betina", "Jelani Woods"]
|
|
7
7
|
spec.email = ["raghu@firstdraft.com", "jelani@firstdraft.com"]
|
|
8
8
|
spec.homepage = "https://github.com/firstdraft/ai-chat"
|
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
spec.required_ruby_version = ">= 3.2"
|
|
22
|
-
spec.add_runtime_dependency "openai", "~> 0.
|
|
22
|
+
spec.add_runtime_dependency "openai", "~> 0.59"
|
|
23
23
|
spec.add_runtime_dependency "marcel", "~> 1.0"
|
|
24
24
|
spec.add_runtime_dependency "base64", "~> 0.1", "> 0.1.1"
|
|
25
25
|
spec.add_runtime_dependency "json", "~> 2.0"
|
data/lib/ai/chat.rb
CHANGED
|
@@ -18,14 +18,19 @@ module AI
|
|
|
18
18
|
# :reek:IrresponsibleModule
|
|
19
19
|
class Chat
|
|
20
20
|
# :reek:Attribute
|
|
21
|
-
attr_accessor :background, :code_interpreter, :conversation_id, :
|
|
22
|
-
attr_reader :client, :last_response_id, :proxy, :schema, :schema_file, :verbosity
|
|
21
|
+
attr_accessor :background, :code_interpreter, :conversation_id, :image_folder, :messages, :model, :reasoning_effort, :web_search
|
|
22
|
+
attr_reader :client, :image_generation, :last_response_id, :proxy, :schema, :schema_file, :verbosity
|
|
23
23
|
|
|
24
24
|
BASE_PROXY_URL = "https://prepend.me/api.openai.com/v1"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
PROXY_ENV = "AICHAT_PROXY"
|
|
26
|
+
PROXY_KEY_ENV = "AICHAT_PROXY_KEY"
|
|
27
|
+
OPENAI_KEY_ENV = "OPENAI_API_KEY"
|
|
28
|
+
|
|
29
|
+
def initialize(api_key: nil, api_key_env_var: nil, proxy: nil)
|
|
30
|
+
@api_key_arg = api_key
|
|
31
|
+
@api_key_env_var_arg = api_key_env_var
|
|
32
|
+
@proxy = proxy.nil? ? ENV[PROXY_ENV]&.downcase == "true" : !!proxy
|
|
33
|
+
@api_key = resolve_api_key
|
|
29
34
|
@messages = []
|
|
30
35
|
@reasoning_effort = nil
|
|
31
36
|
@model = "gpt-5.2"
|
|
@@ -40,8 +45,8 @@ module AI
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def self.generate_schema!(description, location: "schema.json", api_key: nil, api_key_env_var: nil, proxy: nil)
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
proxy = proxy.nil? ? ENV[PROXY_ENV]&.downcase == "true" : !!proxy
|
|
49
|
+
api_key = api_key || ENV.fetch(api_key_env_var || (proxy ? PROXY_KEY_ENV : OPENAI_KEY_ENV))
|
|
45
50
|
prompt_path = File.expand_path("../prompts/schema_generator.md", __dir__)
|
|
46
51
|
system_prompt = File.read(prompt_path)
|
|
47
52
|
|
|
@@ -73,16 +78,6 @@ module AI
|
|
|
73
78
|
content
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
def self.resolve_api_key(api_key: nil, api_key_env_var: nil)
|
|
77
|
-
return api_key if api_key
|
|
78
|
-
return ENV.fetch(api_key_env_var) if api_key_env_var
|
|
79
|
-
|
|
80
|
-
aichat_api_key = ENV["AICHAT_API_KEY"]
|
|
81
|
-
return aichat_api_key if aichat_api_key && !aichat_api_key.empty?
|
|
82
|
-
|
|
83
|
-
ENV.fetch("OPENAI_API_KEY")
|
|
84
|
-
end
|
|
85
|
-
|
|
86
81
|
# :reek:TooManyStatements
|
|
87
82
|
# :reek:NilCheck
|
|
88
83
|
def add(content, role: "user", response: nil, status: nil, image: nil, images: nil, file: nil, files: nil)
|
|
@@ -169,15 +164,20 @@ module AI
|
|
|
169
164
|
end
|
|
170
165
|
|
|
171
166
|
def proxy=(value)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
167
|
+
new_proxy = !!value
|
|
168
|
+
previous_proxy = @proxy
|
|
169
|
+
@proxy = new_proxy
|
|
170
|
+
new_key = resolve_api_key
|
|
171
|
+
client_options = {api_key: new_key}
|
|
172
|
+
client_options[:base_url] = BASE_PROXY_URL if new_proxy
|
|
173
|
+
new_client = OpenAI::Client.new(**client_options)
|
|
174
|
+
|
|
175
|
+
@api_key = new_key
|
|
176
|
+
@api_key_validated = false
|
|
177
|
+
@client = new_client
|
|
178
|
+
rescue => error
|
|
179
|
+
@proxy = previous_proxy
|
|
180
|
+
raise
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def schema=(value)
|
|
@@ -205,6 +205,19 @@ module AI
|
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
def image_generation=(value)
|
|
209
|
+
case value
|
|
210
|
+
when true
|
|
211
|
+
@image_generation = true
|
|
212
|
+
when false, nil
|
|
213
|
+
@image_generation = false
|
|
214
|
+
when Hash
|
|
215
|
+
@image_generation = value.transform_keys(&:to_sym)
|
|
216
|
+
else
|
|
217
|
+
raise ArgumentError, "Invalid image_generation value: #{value.inspect}. Must be true, false, or a Hash of tool options (e.g., { size: \"1536x1024\", quality: \"low\" })."
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
208
221
|
def last
|
|
209
222
|
messages.last
|
|
210
223
|
end
|
|
@@ -262,6 +275,21 @@ module AI
|
|
|
262
275
|
|
|
263
276
|
private
|
|
264
277
|
|
|
278
|
+
def resolve_api_key
|
|
279
|
+
env_var = @api_key_env_var_arg || (@proxy ? PROXY_KEY_ENV : OPENAI_KEY_ENV)
|
|
280
|
+
@api_key_arg || ENV.fetch(env_var) {
|
|
281
|
+
if @proxy
|
|
282
|
+
raise KeyError, "Proxy mode is enabled but #{PROXY_KEY_ENV} is not set. " \
|
|
283
|
+
"Create an environment variable called #{PROXY_KEY_ENV} " \
|
|
284
|
+
"with your API key from Prepend.me."
|
|
285
|
+
else
|
|
286
|
+
raise KeyError, "#{OPENAI_KEY_ENV} is not set. " \
|
|
287
|
+
"Create an environment variable called #{OPENAI_KEY_ENV} " \
|
|
288
|
+
"with your API key from https://platform.openai.com/api-keys."
|
|
289
|
+
end
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
265
293
|
class InputClassificationError < StandardError; end
|
|
266
294
|
|
|
267
295
|
class WrongAPITokenUsedError < StandardError; end
|
|
@@ -508,7 +536,8 @@ module AI
|
|
|
508
536
|
tools_list << {type: "web_search"}
|
|
509
537
|
end
|
|
510
538
|
if image_generation
|
|
511
|
-
|
|
539
|
+
options = image_generation.is_a?(Hash) ? image_generation : {}
|
|
540
|
+
tools_list << options.merge(type: "image_generation")
|
|
512
541
|
end
|
|
513
542
|
if code_interpreter
|
|
514
543
|
tools_list << {type: "code_interpreter", container: {type: "auto"}}
|
|
@@ -562,7 +591,8 @@ module AI
|
|
|
562
591
|
result = output.result
|
|
563
592
|
image_data = Base64.strict_decode64(result)
|
|
564
593
|
|
|
565
|
-
|
|
594
|
+
extension = image_extension_for(image_data)
|
|
595
|
+
filename = "#{(index + 1).to_s.rjust(3, "0")}.#{extension}"
|
|
566
596
|
file_path = File.join(subfolder_path, filename)
|
|
567
597
|
|
|
568
598
|
File.binwrite(file_path, image_data)
|
|
@@ -574,6 +604,18 @@ module AI
|
|
|
574
604
|
image_filenames
|
|
575
605
|
end
|
|
576
606
|
|
|
607
|
+
IMAGE_EXTENSIONS_BY_MIME_TYPE = {
|
|
608
|
+
"image/png" => "png",
|
|
609
|
+
"image/jpeg" => "jpg",
|
|
610
|
+
"image/webp" => "webp"
|
|
611
|
+
}.freeze
|
|
612
|
+
|
|
613
|
+
# :reek:UtilityFunction
|
|
614
|
+
def image_extension_for(bytes)
|
|
615
|
+
mime_type = Marcel::MimeType.for(StringIO.new(bytes))
|
|
616
|
+
IMAGE_EXTENSIONS_BY_MIME_TYPE.fetch(mime_type, "png")
|
|
617
|
+
end
|
|
618
|
+
|
|
577
619
|
def create_images_folder(response_id)
|
|
578
620
|
# ISO 8601 basic format with centisecond precision
|
|
579
621
|
timestamp = Time.now.strftime("%Y%m%dT%H%M%S%2N")
|
|
@@ -597,11 +639,11 @@ module AI
|
|
|
597
639
|
rescue OpenAI::Errors::AuthenticationError
|
|
598
640
|
message = if proxy
|
|
599
641
|
<<~STRING
|
|
600
|
-
|
|
642
|
+
Your API key was not accepted by Prepend.me. Since proxy mode is enabled, you need a valid API key from Prepend.me in the #{PROXY_KEY_ENV} environment variable.
|
|
601
643
|
STRING
|
|
602
644
|
else
|
|
603
645
|
<<~STRING
|
|
604
|
-
|
|
646
|
+
Your API key was not accepted by OpenAI. Make sure the #{OPENAI_KEY_ENV} environment variable contains a valid key from https://platform.openai.com/api-keys.
|
|
605
647
|
STRING
|
|
606
648
|
end
|
|
607
649
|
raise WrongAPITokenUsedError, message, cause: nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ai-chat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raghu Betina
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
19
|
+
version: '0.59'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.
|
|
26
|
+
version: '0.59'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: marcel
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|