ai-chat 0.2.3 → 0.2.4
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 +21 -11
- data/ai-chat.gemspec +2 -1
- data/lib/ai/amazing_print.rb +7 -1
- data/lib/ai/chat.rb +198 -71
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12727b49e500d9c3c117b0d8a4df291f0b050eae0b12c90d7cd992ac93881c42
|
4
|
+
data.tar.gz: 2ebf913f409bdfbebcc5cd12d8a4a87346be894564d75a6c7811157b9619ecce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3707fdf80a68ce541b0c4fb3ab362f2404e2b0e78a3e7bd86ec140702f4b2939a6c2740b12a58d39212aceb24607c87648948f5d93bb5fcacffbf7200e35e2e0
|
7
|
+
data.tar.gz: 9d3e23578c90251da1fb08351aafe48fa5c7a82ec330644a6cffbe156fd42d58bcfbebd42be93df5a7a680cddfe0855c8dc2770ef3c8a0a70f2c4d4e8fa894f8
|
data/README.md
CHANGED
@@ -82,7 +82,7 @@ pp a.messages
|
|
82
82
|
# => [{:role=>"user", :content=>"If the Ruby community had an official motto, what might it be?"}]
|
83
83
|
|
84
84
|
# Generate the next message using AI
|
85
|
-
a.generate! # => "Matz is nice and so we are nice" (or similar)
|
85
|
+
a.generate! # => { :role => "assistant", :content => "Matz is nice and so we are nice" (or similar) }
|
86
86
|
|
87
87
|
# Your array now includes the assistant's response
|
88
88
|
pp a.messages
|
@@ -93,7 +93,7 @@ pp a.messages
|
|
93
93
|
|
94
94
|
# Continue the conversation
|
95
95
|
a.add("What about Rails?")
|
96
|
-
a.generate! # => "Convention over configuration."
|
96
|
+
a.generate! # => { :role => "assistant", :content => "Convention over configuration."}
|
97
97
|
```
|
98
98
|
|
99
99
|
## Understanding the Data Structure
|
@@ -135,7 +135,7 @@ pp b.messages
|
|
135
135
|
# ]
|
136
136
|
|
137
137
|
# Generate a response
|
138
|
-
b.generate! # => "Methinks 'tis 'Ruby doth bring joy to all who craft with care'"
|
138
|
+
b.generate! # => { :role => "assistant", :content => "Methinks 'tis 'Ruby doth bring joy to all who craft with care'" }
|
139
139
|
```
|
140
140
|
|
141
141
|
### Convenience Methods
|
@@ -237,7 +237,7 @@ h.messages.last[:content]
|
|
237
237
|
# => "Here's how to boil an egg..."
|
238
238
|
|
239
239
|
# Or use the convenient shortcut
|
240
|
-
h.last
|
240
|
+
h.last[:content]
|
241
241
|
# => "Here's how to boil an egg..."
|
242
242
|
```
|
243
243
|
|
@@ -277,10 +277,11 @@ i.schema = '{"name": "nutrition_values","strict": true,"schema": {"type": "objec
|
|
277
277
|
i.user("1 slice of pizza")
|
278
278
|
|
279
279
|
response = i.generate!
|
280
|
+
data = response[:content]
|
280
281
|
# => {:fat=>15, :protein=>12, :carbs=>35, :total_calories=>285}
|
281
282
|
|
282
283
|
# The response is parsed JSON, not a string!
|
283
|
-
|
284
|
+
data[:total_calories] # => 285
|
284
285
|
```
|
285
286
|
|
286
287
|
### Schema Formats
|
@@ -442,14 +443,14 @@ a = AI::Chat.new
|
|
442
443
|
a.user("What color is the object in this photo?", image: "thing.png")
|
443
444
|
a.generate! # => "Red"
|
444
445
|
a.user("What is the object in the photo?")
|
445
|
-
a.generate! # => "I don't see a photo"
|
446
|
+
a.generate! # => { :content => "I don't see a photo", ... }
|
446
447
|
|
447
448
|
b = AI::Chat.new
|
448
449
|
b.user("What color is the object in this photo?", image: "thing.png")
|
449
450
|
b.generate! # => "Red"
|
450
451
|
b.user("What is the object in the photo?")
|
451
452
|
b.previous_response_id = nil
|
452
|
-
b.generate! # => "An apple"
|
453
|
+
b.generate! # => { :content => "An apple", ... }
|
453
454
|
```
|
454
455
|
|
455
456
|
If you don't set `previous_response_id` to `nil`, the model won't have the old image(s) to work with.
|
@@ -462,7 +463,7 @@ You can enable OpenAI's image generation tool:
|
|
462
463
|
a = AI::Chat.new
|
463
464
|
a.image_generation = true
|
464
465
|
a.user("Draw a picture of a kitten")
|
465
|
-
a.generate! # => "Here is your picture of a kitten:"
|
466
|
+
a.generate! # => { :content => "Here is your picture of a kitten:", ... }
|
466
467
|
```
|
467
468
|
|
468
469
|
By default, images are saved to `./images`. You can configure a different location:
|
@@ -472,7 +473,7 @@ a = AI::Chat.new
|
|
472
473
|
a.image_generation = true
|
473
474
|
a.image_folder = "./my_images"
|
474
475
|
a.user("Draw a picture of a kitten")
|
475
|
-
a.generate! # => "Here is your picture of a kitten:"
|
476
|
+
a.generate! # => { :content => "Here is your picture of a kitten:", ... }
|
476
477
|
```
|
477
478
|
|
478
479
|
Images are saved in timestamped subfolders using ISO 8601 basic format. For example:
|
@@ -510,9 +511,18 @@ a = AI::Chat.new
|
|
510
511
|
a.image_generation = true
|
511
512
|
a.image_folder = "./images"
|
512
513
|
a.user("Draw a picture of a kitten")
|
513
|
-
a.generate! # => "Here is a picture of a kitten:"
|
514
|
+
a.generate! # => { :content => "Here is a picture of a kitten:", ... }
|
514
515
|
a.user("Make it even cuter")
|
515
|
-
a.generate! # => "Here is the kitten, but even cuter:"
|
516
|
+
a.generate! # => { :content => "Here is the kitten, but even cuter:", ... }
|
517
|
+
```
|
518
|
+
|
519
|
+
## Code Interpreter
|
520
|
+
|
521
|
+
```ruby
|
522
|
+
y = AI::Chat.new
|
523
|
+
y.code_interpreter = true
|
524
|
+
y.user("Plot y = 2x*3 when x is -5 to 5.")
|
525
|
+
y.generate! # => {:content => "Here is the graph.", ... }
|
516
526
|
```
|
517
527
|
|
518
528
|
## Building Conversations Without API Calls
|
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.2.
|
5
|
+
spec.version = "0.2.4"
|
6
6
|
spec.authors = ["Raghu Betina"]
|
7
7
|
spec.email = ["raghu@firstdraft.com"]
|
8
8
|
spec.homepage = "https://github.com/firstdraft/ai-chat"
|
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_runtime_dependency "marcel", "~> 1.0"
|
24
24
|
spec.add_runtime_dependency "base64", "> 0.1.1"
|
25
25
|
spec.add_runtime_dependency "json", "~> 2.0"
|
26
|
+
spec.add_runtime_dependency "tty-spinner", "~> 0.9.3"
|
26
27
|
|
27
28
|
spec.add_development_dependency "dotenv"
|
28
29
|
spec.add_development_dependency "refinements", "~> 11.1"
|
data/lib/ai/amazing_print.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require "amazing_print"
|
2
|
-
|
2
|
+
# :reek:IrresponsibleModule
|
3
3
|
module AmazingPrint
|
4
4
|
module AI
|
5
5
|
def self.included(base)
|
@@ -27,6 +27,10 @@ module AmazingPrint
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
# :reek:DuplicateMethodCall
|
31
|
+
# :reek:FeatureEnvy
|
32
|
+
# :reek:NilCheck
|
33
|
+
# :reek:TooManyStatements
|
30
34
|
def format_ai_chat(chat)
|
31
35
|
vars = []
|
32
36
|
|
@@ -53,6 +57,8 @@ module AmazingPrint
|
|
53
57
|
format_object(chat, vars)
|
54
58
|
end
|
55
59
|
|
60
|
+
# :reek:TooManyStatements
|
61
|
+
# :reek:DuplicateMethodCall
|
56
62
|
def format_object(object, vars)
|
57
63
|
data = vars.map do |(name, value)|
|
58
64
|
name = colorize(name, :variable) unless @options[:plain]
|
data/lib/ai/chat.rb
CHANGED
@@ -7,6 +7,8 @@ require "openai"
|
|
7
7
|
require "pathname"
|
8
8
|
require "stringio"
|
9
9
|
require "fileutils"
|
10
|
+
require "tty-spinner"
|
11
|
+
require "timeout"
|
10
12
|
|
11
13
|
module AI
|
12
14
|
# :reek:MissingSafeMethod { exclude: [ generate! ] }
|
@@ -16,7 +18,7 @@ module AI
|
|
16
18
|
# :reek:IrresponsibleModule
|
17
19
|
class Chat
|
18
20
|
# :reek:Attribute
|
19
|
-
attr_accessor :
|
21
|
+
attr_accessor :background, :code_interpreter, :image_generation, :image_folder, :messages, :model, :previous_response_id, :web_search
|
20
22
|
attr_reader :reasoning_effort, :client, :schema
|
21
23
|
|
22
24
|
VALID_REASONING_EFFORTS = [:low, :medium, :high].freeze
|
@@ -34,16 +36,16 @@ module AI
|
|
34
36
|
|
35
37
|
# :reek:TooManyStatements
|
36
38
|
# :reek:NilCheck
|
37
|
-
def add(content, role: "user", response: nil, image: nil, images: nil, file: nil, files: nil)
|
39
|
+
def add(content, role: "user", response: nil, status: nil, image: nil, images: nil, file: nil, files: nil)
|
38
40
|
if image.nil? && images.nil? && file.nil? && files.nil?
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
message = {
|
42
|
+
role: role,
|
43
|
+
content: content,
|
44
|
+
response: response
|
45
|
+
}
|
46
|
+
message[:content] = content if content
|
47
|
+
message[:status] = status if status
|
48
|
+
messages.push(message)
|
47
49
|
else
|
48
50
|
text_and_files_array = [
|
49
51
|
{
|
@@ -76,7 +78,8 @@ module AI
|
|
76
78
|
messages.push(
|
77
79
|
{
|
78
80
|
role: role,
|
79
|
-
content: text_and_files_array
|
81
|
+
content: text_and_files_array,
|
82
|
+
status: status
|
80
83
|
}
|
81
84
|
)
|
82
85
|
end
|
@@ -90,53 +93,31 @@ module AI
|
|
90
93
|
add(message, role: "user", image: image, images: images, file: file, files: files)
|
91
94
|
end
|
92
95
|
|
93
|
-
def assistant(message, response: nil)
|
94
|
-
add(message, role: "assistant", response: response)
|
96
|
+
def assistant(message, response: nil, status: nil)
|
97
|
+
add(message, role: "assistant", response: response, status: status)
|
95
98
|
end
|
96
99
|
|
97
100
|
# :reek:NilCheck
|
98
101
|
# :reek:TooManyStatements
|
99
102
|
def generate!
|
100
103
|
response = create_response
|
104
|
+
parse_response(response)
|
101
105
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
response_usage = response.usage.to_h.slice(:input_tokens, :output_tokens, :total_tokens)
|
106
|
-
|
107
|
-
chat_response = {
|
108
|
-
id: response.id,
|
109
|
-
model: response.model,
|
110
|
-
usage: response_usage,
|
111
|
-
total_tokens: response_usage[:total_tokens],
|
112
|
-
images: image_filenames
|
113
|
-
}
|
114
|
-
|
115
|
-
message = if schema
|
116
|
-
if text_response.nil? || text_response.empty?
|
117
|
-
raise ArgumentError, "No text content in response to parse as JSON for schema: #{schema.inspect}"
|
118
|
-
end
|
119
|
-
JSON.parse(text_response, symbolize_names: true)
|
120
|
-
else
|
121
|
-
text_response
|
122
|
-
end
|
106
|
+
self.previous_response_id = last.dig(:response, :id)
|
107
|
+
last
|
108
|
+
end
|
123
109
|
|
124
|
-
|
125
|
-
|
110
|
+
# :reek:BooleanParameter
|
111
|
+
# :reek:ControlParameter
|
112
|
+
# :reek:DuplicateMethodCall
|
113
|
+
# :reek:TooManyStatements
|
114
|
+
def get_response(wait: false, timeout: 600)
|
115
|
+
response = if wait
|
116
|
+
wait_for_response(timeout)
|
126
117
|
else
|
127
|
-
|
128
|
-
{
|
129
|
-
role: "assistant",
|
130
|
-
content: message,
|
131
|
-
images: image_filenames,
|
132
|
-
response: chat_response
|
133
|
-
}.compact
|
134
|
-
)
|
118
|
+
client.responses.retrieve(previous_response_id)
|
135
119
|
end
|
136
|
-
|
137
|
-
self.previous_response_id = chat_response[:id]
|
138
|
-
|
139
|
-
message
|
120
|
+
parse_response(response)
|
140
121
|
end
|
141
122
|
|
142
123
|
# :reek:NilCheck
|
@@ -177,6 +158,11 @@ module AI
|
|
177
158
|
end
|
178
159
|
|
179
160
|
# Support for Ruby's pp (pretty print)
|
161
|
+
# :reek:TooManyStatements
|
162
|
+
# :reek:NilCheck
|
163
|
+
# :reek:FeatureEnvy
|
164
|
+
# :reek:DuplicateMethodCall
|
165
|
+
# :reek:UncommunicativeParameterName
|
180
166
|
def pretty_print(q)
|
181
167
|
q.group(1, "#<#{self.class}", '>') do
|
182
168
|
q.breakable
|
@@ -207,7 +193,6 @@ module AI
|
|
207
193
|
end
|
208
194
|
end
|
209
195
|
|
210
|
-
|
211
196
|
private
|
212
197
|
|
213
198
|
class InputClassificationError < StandardError; end
|
@@ -231,6 +216,7 @@ module AI
|
|
231
216
|
model: model
|
232
217
|
}
|
233
218
|
|
219
|
+
parameters[:background] = background if background
|
234
220
|
parameters[:tools] = tools unless tools.empty?
|
235
221
|
parameters[:text] = schema if schema
|
236
222
|
parameters[:reasoning] = {effort: reasoning_effort} if reasoning_effort
|
@@ -242,6 +228,56 @@ module AI
|
|
242
228
|
client.responses.create(**parameters)
|
243
229
|
end
|
244
230
|
|
231
|
+
# :reek:NilCheck
|
232
|
+
# :reek:TooManyStatements
|
233
|
+
def parse_response(response)
|
234
|
+
text_response = response.output_text
|
235
|
+
image_filenames = extract_and_save_images(response)
|
236
|
+
response_id = response.id
|
237
|
+
response_usage = response.usage.to_h.slice(:input_tokens, :output_tokens, :total_tokens)
|
238
|
+
|
239
|
+
chat_response = {
|
240
|
+
id: response_id,
|
241
|
+
model: response.model,
|
242
|
+
usage: response_usage,
|
243
|
+
total_tokens: response_usage[:total_tokens],
|
244
|
+
images: image_filenames
|
245
|
+
}.compact
|
246
|
+
|
247
|
+
response_content = if schema
|
248
|
+
if text_response.nil? || text_response.empty?
|
249
|
+
raise ArgumentError, "No text content in response to parse as JSON for schema: #{schema.inspect}"
|
250
|
+
end
|
251
|
+
JSON.parse(text_response, symbolize_names: true)
|
252
|
+
else
|
253
|
+
text_response
|
254
|
+
end
|
255
|
+
|
256
|
+
existing_message_position = messages.find_index do |message|
|
257
|
+
message.dig(:response, :id) == response_id
|
258
|
+
end
|
259
|
+
|
260
|
+
message = {
|
261
|
+
role: "assistant",
|
262
|
+
content: response_content,
|
263
|
+
response: chat_response,
|
264
|
+
status: response.status
|
265
|
+
}
|
266
|
+
|
267
|
+
message.store(:images, image_filenames) unless image_filenames.empty?
|
268
|
+
|
269
|
+
if existing_message_position
|
270
|
+
messages[existing_message_position] = message
|
271
|
+
else
|
272
|
+
messages.push(message)
|
273
|
+
message
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def cancel_request
|
278
|
+
client.responses.cancel(previous_response_id)
|
279
|
+
end
|
280
|
+
|
245
281
|
def prepare_messages_for_api
|
246
282
|
return messages unless previous_response_id
|
247
283
|
|
@@ -389,19 +425,12 @@ module AI
|
|
389
425
|
if image_generation
|
390
426
|
tools_list << {type: "image_generation"}
|
391
427
|
end
|
428
|
+
if code_interpreter
|
429
|
+
tools_list << {type: "code_interpreter", container: {type: "auto"}}
|
430
|
+
end
|
392
431
|
tools_list
|
393
432
|
end
|
394
433
|
|
395
|
-
# :reek:UtilityFunction
|
396
|
-
# :reek:ManualDispatch
|
397
|
-
def extract_text_from_response(response)
|
398
|
-
response.output.flat_map { |output|
|
399
|
-
output.respond_to?(:content) ? output.content : []
|
400
|
-
}.compact.find { |content|
|
401
|
-
content.is_a?(OpenAI::Models::Responses::ResponseOutputText)
|
402
|
-
}&.text
|
403
|
-
end
|
404
|
-
|
405
434
|
# :reek:FeatureEnvy
|
406
435
|
# :reek:UtilityFunction
|
407
436
|
def wrap_schema_if_needed(schema)
|
@@ -438,31 +467,129 @@ module AI
|
|
438
467
|
|
439
468
|
return image_filenames if image_outputs.empty?
|
440
469
|
|
441
|
-
|
442
|
-
timestamp = Time.now.strftime("%Y%m%dT%H%M%S%2N")
|
443
|
-
|
444
|
-
subfolder_name = "#{timestamp}_#{response.id}"
|
445
|
-
subfolder_path = File.join(image_folder || "./images", subfolder_name)
|
446
|
-
FileUtils.mkdir_p(subfolder_path)
|
470
|
+
subfolder_path = create_images_folder(response.id)
|
447
471
|
|
448
472
|
image_outputs.each_with_index do |output, index|
|
449
473
|
next unless output.respond_to?(:result) && output.result
|
450
474
|
|
451
|
-
|
475
|
+
warn_if_file_fails_to_save do
|
452
476
|
image_data = Base64.strict_decode64(output.result)
|
453
477
|
|
454
478
|
filename = "#{(index + 1).to_s.rjust(3, "0")}.png"
|
455
|
-
|
479
|
+
file_path = File.join(subfolder_path, filename)
|
456
480
|
|
457
|
-
File.binwrite(
|
481
|
+
File.binwrite(file_path, image_data)
|
458
482
|
|
459
|
-
image_filenames <<
|
460
|
-
rescue => error
|
461
|
-
warn "Failed to save image: #{error.message}"
|
483
|
+
image_filenames << file_path
|
462
484
|
end
|
463
485
|
end
|
464
486
|
|
465
487
|
image_filenames
|
466
488
|
end
|
489
|
+
|
490
|
+
def create_images_folder(response_id)
|
491
|
+
# ISO 8601 basic format with centisecond precision
|
492
|
+
timestamp = Time.now.strftime("%Y%m%dT%H%M%S%2N")
|
493
|
+
|
494
|
+
subfolder_name = "#{timestamp}_#{response_id}"
|
495
|
+
subfolder_path = File.join(image_folder || "./images", subfolder_name)
|
496
|
+
FileUtils.mkdir_p(subfolder_path)
|
497
|
+
subfolder_path
|
498
|
+
end
|
499
|
+
|
500
|
+
def warn_if_file_fails_to_save
|
501
|
+
begin
|
502
|
+
yield
|
503
|
+
rescue => error
|
504
|
+
warn "Failed to save image: #{error.message}"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# :reek:FeatureEnvy
|
509
|
+
# :reek:ManualDispatch
|
510
|
+
# :reek:NestedIterators
|
511
|
+
# :reek:TooManyStatements
|
512
|
+
def extract_and_save_files(response)
|
513
|
+
filenames = []
|
514
|
+
|
515
|
+
message_outputs = response.output.select do |output|
|
516
|
+
output.respond_to?(:type) && output.type == :message
|
517
|
+
end
|
518
|
+
|
519
|
+
outputs_with_annotations = message_outputs.map do |message|
|
520
|
+
message.content.find do |content|
|
521
|
+
content.respond_to?(:annotations) && content.annotations.length.positive?
|
522
|
+
end
|
523
|
+
end.compact
|
524
|
+
|
525
|
+
return filenames if outputs_with_annotations.empty?
|
526
|
+
|
527
|
+
subfolder_path = create_images_folder(response.id)
|
528
|
+
annotations = outputs_with_annotations.map do |output|
|
529
|
+
output.annotations.find do |annotation|
|
530
|
+
annotation.respond_to?(:filename)
|
531
|
+
end
|
532
|
+
end.compact
|
533
|
+
|
534
|
+
annotations.each do |annotation|
|
535
|
+
container_id = annotation.container_id
|
536
|
+
file_id = annotation.file_id
|
537
|
+
filename = annotation.filename
|
538
|
+
|
539
|
+
warn_if_file_fails_to_save do
|
540
|
+
container_content = client.containers.files.content
|
541
|
+
file_content = container_content.retrieve(file_id, container_id: container_id)
|
542
|
+
file_path = File.join(subfolder_path, filename)
|
543
|
+
File.open(file_path, "wb") do |file|
|
544
|
+
file.write(file_content.read)
|
545
|
+
end
|
546
|
+
filenames << file_path
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
filenames
|
551
|
+
end
|
552
|
+
|
553
|
+
# This is similar to ActiveJob's :polynomially_longer retry option
|
554
|
+
# :reek:DuplicateMethodCall
|
555
|
+
# :reek:UtilityFunction
|
556
|
+
def calculate_wait(executions)
|
557
|
+
# cap the maximum wait time to ~110 seconds
|
558
|
+
executions = executions.clamp(1..10)
|
559
|
+
jitter = 0.15
|
560
|
+
((executions**2) + (Kernel.rand * (executions**2) * jitter)) + 2
|
561
|
+
end
|
562
|
+
|
563
|
+
def timeout_request(duration)
|
564
|
+
begin
|
565
|
+
Timeout.timeout(duration) do
|
566
|
+
yield
|
567
|
+
end
|
568
|
+
rescue Timeout::Error
|
569
|
+
client.responses.cancel(previous_response_id)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# :reek:DuplicateMethodCall
|
574
|
+
# :reek:TooManyStatements
|
575
|
+
def wait_for_response(timeout)
|
576
|
+
spinner = TTY::Spinner.new("[:spinner] Thinking ...", format: :dots)
|
577
|
+
spinner.auto_spin
|
578
|
+
api_response = client.responses.retrieve(previous_response_id)
|
579
|
+
number_of_times_polled = 0
|
580
|
+
response = timeout_request(timeout) do
|
581
|
+
while api_response.status != :completed
|
582
|
+
some_amount_of_seconds = calculate_wait(number_of_times_polled)
|
583
|
+
sleep some_amount_of_seconds
|
584
|
+
number_of_times_polled += 1
|
585
|
+
api_response = client.responses.retrieve(previous_response_id)
|
586
|
+
end
|
587
|
+
api_response
|
588
|
+
end
|
589
|
+
|
590
|
+
exit_message = response.status == :cancelled ? "request timed out" : "done!"
|
591
|
+
spinner.stop(exit_message)
|
592
|
+
response
|
593
|
+
end
|
467
594
|
end
|
468
595
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ai-chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Raghu Betina
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08
|
11
|
+
date: 2025-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: openai
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tty-spinner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.9.3
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.9.3
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: dotenv
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|