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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +303 -671
  3. data/ai-chat.gemspec +2 -2
  4. data/lib/ai/chat.rb +73 -31
  5. 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.8"
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.43"
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, :image_generation, :image_folder, :messages, :model, :reasoning_effort, :web_search
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
- def initialize(api_key: nil, api_key_env_var: nil)
27
- @api_key = self.class.resolve_api_key(api_key: api_key, api_key_env_var: api_key_env_var)
28
- @proxy = ENV["AICHAT_PROXY"] == "true"
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
- api_key = resolve_api_key(api_key: api_key, api_key_env_var: api_key_env_var)
44
- proxy = ENV["AICHAT_PROXY"] == "true" if proxy.nil?
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
- @proxy = value
173
- @client = if value
174
- OpenAI::Client.new(
175
- api_key: @api_key,
176
- base_url: BASE_PROXY_URL
177
- )
178
- else
179
- OpenAI::Client.new(api_key: @api_key)
180
- end
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
- tools_list << {type: "image_generation"}
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
- filename = "#{(index + 1).to_s.rjust(3, "0")}.png"
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
- It looks like you're using an invalid API key. Proxying is enabled, so you must use an OpenAI API key from prepend.me. Please disable proxy or update your API key before generating a response.
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
- It looks like you're using an invalid API key. Check to make sure your API key is valid before generating a response.
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.5.8
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.43'
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.43'
26
+ version: '0.59'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: marcel
29
29
  requirement: !ruby/object:Gem::Requirement