open_ai_bot 0.2.2 → 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 +7 -0
- data/lib/ext/blank.rb +27 -0
- data/lib/ext/in.rb +5 -0
- data/lib/open_ai/chat_gpt.rb +108 -84
- data/lib/open_ai/chat_thread.rb +19 -10
- data/lib/open_ai/dalle.rb +18 -16
- data/lib/open_ai/message.rb +68 -0
- data/lib/open_ai/utils.rb +19 -17
- data/lib/open_ai/whisper.rb +47 -45
- data/lib/open_ai_bot.rb +5 -4
- data/main.rb +3 -0
- data/open_ai_bot.gemspec +4 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 867a815eac31b0a468c48e4318cdf2d235d575a7f950fb6dda6e084947b5b31f
|
4
|
+
data.tar.gz: 1d18f4c71a5725d06325f0e6725328af9acae9d9afa25dd1a91696b8e93d6e3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz: '
|
6
|
+
metadata.gz: ccae7c2e54c883609af20f20c7b19dd3c59e14844d44964cfdd445a2357312c8aa3eebc6bfeb169f2d78fe62a92c49bcf8e7cf8958a810a47624b7a4c6e5b79c
|
7
|
+
data.tar.gz: '076387746b93f4d11cdf594727ac2b575cfe02b7b983b50e5928a938671937cda73d17760f5f246efd1059ce66597386f343a168640d29819c62ab1dcb6dd5c7'
|
data/README.md
CHANGED
@@ -1,4 +1,11 @@
|
|
1
1
|
# OpenAI Telegram bot
|
2
|
+
## What it does
|
3
|
+
- ChatGPT
|
4
|
+
Send any message to the bot, or ping its @username or reply ot its message in a group chat. It will forward your message to ChatGPT and return a response, keeping track of context.
|
5
|
+
- Whisper
|
6
|
+
Record a voice message in any chat with the bot or reply to a forwarded message with the `/transcribe` command. It will reply with a transcript, automatically detecting language(s).
|
7
|
+
- DALL-e
|
8
|
+
Send `/dalle {prompt/description}` command. The bot will reply with a picture based on your prompt.
|
2
9
|
|
3
10
|
## Dependencies
|
4
11
|
1. Ruby (`ruby -v` should return something, preferrably > 3.2)
|
data/lib/ext/blank.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Object.class_eval do
|
2
|
+
def blank?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
def present?
|
7
|
+
!blank?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
NilClass.class_eval do
|
12
|
+
def blank?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
String.class_eval do
|
18
|
+
def blank?
|
19
|
+
gsub(/\s/, '').empty?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Enumerable.module_eval do
|
24
|
+
def blank?
|
25
|
+
size == 0
|
26
|
+
end
|
27
|
+
end
|
data/lib/ext/in.rb
ADDED
data/lib/open_ai/chat_gpt.rb
CHANGED
@@ -1,97 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module OpenAI
|
4
|
+
module ChatGPT
|
5
|
+
module ClassMethods
|
6
|
+
def threads
|
7
|
+
@threads ||= {}
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def new_thread(chat_id)
|
11
|
+
new_thread = ChatThread.new(initial_messages)
|
12
|
+
threads[chat_id] = new_thread
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
15
|
+
def default_instruction
|
16
|
+
msg = <<~MSG
|
17
|
+
You are in a group chat. In the first line of the message, you will receive the name of the user who sent that message.
|
18
|
+
Do not sign your messages this way
|
19
|
+
Do not use users' names without need (for example, if you are replying to @foo, do not add "@foo" to your message)
|
20
|
+
You can still use names to mention other users you're not replying to directly.
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
<@tyradee>:
|
25
|
-
I drank some tea today.
|
26
|
-
MSG
|
27
|
-
end
|
22
|
+
Different languages can be used.
|
23
|
+
MSG
|
28
24
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
25
|
+
SystemMessage.new(
|
26
|
+
body: msg
|
27
|
+
)
|
28
|
+
end
|
34
29
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
end
|
30
|
+
def first_user_message
|
31
|
+
Message.new(
|
32
|
+
from: "@tyradee",
|
33
|
+
body: "I drank some tea today."
|
34
|
+
)
|
35
|
+
end
|
43
36
|
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
def first_bot_message
|
38
|
+
BotMessage.new(
|
39
|
+
body: "Good for you!"
|
40
|
+
)
|
41
|
+
end
|
47
42
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
def initial_messages
|
44
|
+
[
|
45
|
+
default_instruction,
|
46
|
+
first_user_message,
|
47
|
+
first_bot_message
|
48
|
+
]
|
49
|
+
end
|
50
|
+
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
def self.included(base)
|
53
|
+
base.extend ClassMethods
|
54
|
+
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
def init_session
|
57
|
+
self.class.new_thread(@chat.id)
|
58
|
+
send_message(session_restart_message)
|
60
59
|
end
|
61
60
|
|
62
|
-
|
63
|
-
|
61
|
+
def current_thread
|
62
|
+
self.class.threads[@chat.id] || self.class.new_thread(@chat.id)
|
63
|
+
end
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
def username(user)
|
66
|
+
return unless user
|
67
|
+
return "@" + user.username if user.username.present?
|
68
|
+
return user.first_name if user.first_name.present?
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
target_text = nil if @target&.username == config.bot_username
|
70
|
+
"NULL"
|
71
|
+
end
|
72
72
|
|
73
|
+
def handle_gpt_command
|
74
|
+
return unless bot_mentioned? || bot_replied_to? || private_chat?
|
75
|
+
return if self.class.registered_commands.keys.any? { @text.include? _1 }
|
73
76
|
|
74
|
-
|
75
|
-
|
77
|
+
if !allowed_chat?
|
78
|
+
reply(chat_not_allowed_message, parse_mode: "Markdown") if chat_not_allowed_message
|
79
|
+
return
|
80
|
+
end
|
76
81
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
current_message = Message.new(
|
83
|
+
id: @message_id,
|
84
|
+
replies_to: @replies_to&.message_id,
|
85
|
+
from: username(@user),
|
86
|
+
body: @text_without_bot_mentions,
|
87
|
+
chat_id: @chat.id
|
88
|
+
)
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
return unless current_message.valid?
|
91
|
+
|
92
|
+
replies_to =
|
93
|
+
if @replies_to && !bot_replied_to?
|
94
|
+
Message.new(
|
95
|
+
id: @replies_to.message_id,
|
96
|
+
replies_to: @replies_to.reply_to_message&.message_id,
|
97
|
+
from: username(@target),
|
98
|
+
body: @replies_to.text.to_s.gsub(/@#{config.bot_username}\b/, ""),
|
99
|
+
chat_id: @chat.id
|
100
|
+
)
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
current_thread.add(replies_to)
|
106
|
+
current_thread.add(current_message)
|
107
|
+
|
108
|
+
send_request!
|
109
|
+
end
|
86
110
|
|
87
|
-
|
88
|
-
attempt(3) do
|
111
|
+
def send_request!
|
89
112
|
send_chat_action(:typing)
|
90
113
|
|
91
114
|
response = open_ai.chat(
|
92
115
|
parameters: {
|
93
116
|
model: config.open_ai["chat_gpt_model"],
|
94
|
-
messages:
|
117
|
+
messages: current_thread.as_json
|
95
118
|
}
|
96
119
|
)
|
97
120
|
|
@@ -101,25 +124,26 @@ module ChatGPT
|
|
101
124
|
send_chat_gpt_error(error_text.strip)
|
102
125
|
else
|
103
126
|
text = response.dig("choices", 0, "message", "content")
|
104
|
-
|
127
|
+
tokens = response.dig("usage", "total_tokens")
|
105
128
|
|
106
|
-
send_chat_gpt_response(text)
|
129
|
+
send_chat_gpt_response(text, tokens)
|
107
130
|
end
|
108
131
|
end
|
109
|
-
end
|
110
132
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
def send_chat_gpt_response(text)
|
116
|
-
reply(text)
|
117
|
-
@thread.add!(:assistant, text)
|
118
|
-
end
|
119
|
-
|
120
|
-
def add_name(name, text)
|
121
|
-
return "" if text.nil? || text.empty?
|
133
|
+
def send_chat_gpt_error(text)
|
134
|
+
reply(text, parse_mode: "Markdown")
|
135
|
+
end
|
122
136
|
|
123
|
-
|
137
|
+
def send_chat_gpt_response(text, tokens)
|
138
|
+
id = reply(text).dig("result", "message_id")
|
139
|
+
bot_message = BotMessage.new(
|
140
|
+
id: id,
|
141
|
+
replies_to: @message_id,
|
142
|
+
body: text,
|
143
|
+
chat_id: @chat.id,
|
144
|
+
tokens: tokens
|
145
|
+
)
|
146
|
+
current_thread.add(bot_message)
|
147
|
+
end
|
124
148
|
end
|
125
149
|
end
|
data/lib/open_ai/chat_thread.rb
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module OpenAI
|
4
|
+
class ChatThread
|
5
|
+
def initialize(defaults = [])
|
6
|
+
@history ||= defaults
|
7
|
+
puts @history
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :history
|
11
|
+
|
12
|
+
def add(message)
|
13
|
+
return false unless message&.valid?
|
14
|
+
return false if @history.any? { message.id == _1.id}
|
8
15
|
|
9
|
-
|
16
|
+
@history << message
|
17
|
+
puts message
|
10
18
|
|
11
|
-
|
12
|
-
|
19
|
+
true
|
20
|
+
end
|
13
21
|
|
14
|
-
|
15
|
-
|
22
|
+
def as_json
|
23
|
+
@history.map(&:as_json)
|
24
|
+
end
|
16
25
|
end
|
17
26
|
end
|
data/lib/open_ai/dalle.rb
CHANGED
@@ -1,27 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
3
|
+
module OpenAI
|
4
|
+
module Dalle
|
5
|
+
def dalle
|
6
|
+
return unless allowed_chat?
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
attempt(3) do
|
9
|
+
puts "Received a /dalle command"
|
10
|
+
prompt = @replies_to&.text || @text_without_command
|
11
|
+
send_chat_action(:upload_photo)
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
puts "Sending request"
|
14
|
+
response = open_ai.images.generate(parameters: { prompt: prompt })
|
14
15
|
|
15
|
-
|
16
|
+
send_chat_action(:upload_photo)
|
16
17
|
|
17
|
-
|
18
|
+
url = response.dig("data", 0, "url")
|
18
19
|
|
19
|
-
|
20
|
+
puts "DALL-E finished, sending photo to Telegram..."
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
if response["error"]
|
23
|
+
reply_code(response)
|
24
|
+
else
|
25
|
+
send_photo(url, reply_to_message_id: @msg.message_id)
|
26
|
+
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module OpenAI
|
2
|
+
# An over-engineered solution that ultimately wasn't used for its intent.
|
3
|
+
# (ChatGPT isn't brilliant at parsing JSON sructures without starting to reply in JSON, so most of it is useless)
|
4
|
+
|
5
|
+
class Message
|
6
|
+
attr_accessor :body, :from, :id, :replies_to, :tokens, :chat_id
|
7
|
+
attr_reader :role
|
8
|
+
|
9
|
+
def initialize(**kwargs)
|
10
|
+
kwargs.each_pair { public_send("#{_1}=", _2) }
|
11
|
+
@role = :user
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
[body, from, id, chat_id].all?(&:present?)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Format for OpenAI API
|
19
|
+
def as_json
|
20
|
+
content = [from, body].join("\n")
|
21
|
+
{ role:, content: content }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Format for human-readable logs
|
25
|
+
def to_s
|
26
|
+
msg_lines = {
|
27
|
+
"Chat ID" => chat_id,
|
28
|
+
"Message ID" => id,
|
29
|
+
"From" => from,
|
30
|
+
"To" => replies_to,
|
31
|
+
"Body" => body,
|
32
|
+
"Tokens used" => tokens
|
33
|
+
}.reject { |_k, v|
|
34
|
+
v.blank?
|
35
|
+
}.map { |k, v|
|
36
|
+
"#{k}: #{v}"
|
37
|
+
}
|
38
|
+
|
39
|
+
[Time.now.utc, *msg_lines].join("\n") + "\n\n"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class SystemMessage < Message
|
44
|
+
def initialize(...)
|
45
|
+
super(...)
|
46
|
+
@role = :system
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
[Time.now.utc, "SYSTEM INSTRUCTION", body].join("\n") + "\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
body.present?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class BotMessage < Message
|
59
|
+
def initialize(...)
|
60
|
+
super(...)
|
61
|
+
@role = :assistant
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid?
|
65
|
+
[body, id, chat_id, tokens].all?(&:present?)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/open_ai/utils.rb
CHANGED
@@ -1,25 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
module OpenAI
|
4
|
+
module Utils
|
5
|
+
def attempt(times, exception=Net::ReadTimeout)
|
6
|
+
retries ||= 0
|
7
|
+
yield
|
8
|
+
rescue exception => e
|
9
|
+
retries += 1
|
10
|
+
if retries < times
|
11
|
+
retry
|
12
|
+
else
|
13
|
+
reply(e.message, parse_mode: "Markdown")
|
14
|
+
end
|
13
15
|
end
|
14
|
-
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
def download_file(voice)
|
18
|
+
file_path = @api.get_file(file_id: voice.file_id)["result"]["file_path"]
|
18
19
|
|
19
|
-
|
20
|
+
url = "https://api.telegram.org/file/bot#{config.token}/#{file_path}"
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
file = Down.download(url)
|
23
|
+
FileUtils.mv(file.path, "./#{file.original_filename}")
|
24
|
+
file
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
data/lib/open_ai/whisper.rb
CHANGED
@@ -1,61 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
if
|
7
|
-
reply(chat_not_allowed_message) if chat_not_allowed_message
|
8
|
-
end
|
9
|
-
|
10
|
-
return
|
11
|
-
end
|
12
|
-
|
13
|
-
attempt(3) do
|
14
|
-
voice =
|
3
|
+
module OpenAI
|
4
|
+
module Whisper
|
5
|
+
def transcribe
|
6
|
+
if !allowed_chat?
|
15
7
|
if @command == "/transcribe"
|
16
|
-
|
17
|
-
else
|
18
|
-
@msg.voice
|
8
|
+
reply(chat_not_allowed_message) if chat_not_allowed_message
|
19
9
|
end
|
20
10
|
|
21
|
-
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
attempt(3) do
|
15
|
+
voice =
|
16
|
+
if @command == "/transcribe"
|
17
|
+
@replies_to&.voice
|
18
|
+
else
|
19
|
+
@msg.voice
|
20
|
+
end
|
22
21
|
|
23
|
-
|
22
|
+
return unless voice
|
24
23
|
|
25
|
-
|
26
|
-
response = send_whisper_request(file[:file])
|
24
|
+
send_chat_action(:typing)
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
file = ogg_to_mp3(download_file(voice))
|
27
|
+
response = send_whisper_request(file[:file])
|
28
|
+
|
29
|
+
if response["error"]
|
30
|
+
send_whisper_error(response["error"])
|
31
|
+
else
|
32
|
+
send_whisper_response(response["text"])
|
33
|
+
end
|
34
|
+
ensure
|
35
|
+
FileUtils.rm_rf(file[:names]) if file
|
32
36
|
end
|
33
|
-
ensure
|
34
|
-
FileUtils.rm_rf(file[:names]) if file
|
35
37
|
end
|
36
|
-
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
def send_whisper_response(text)
|
40
|
+
reply(text)
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
def send_whisper_error
|
44
|
+
reply_code(text)
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
def ogg_to_mp3(file)
|
48
|
+
ogg = file.original_filename
|
49
|
+
mp3 = ogg.sub(/og.\z/, "mp3")
|
50
|
+
`ffmpeg -i ./#{ogg} -acodec libmp3lame ./#{mp3} -y 2>/dev/null`
|
51
|
+
{ file: File.open("./#{mp3}", "rb"), names: [ogg, mp3] }
|
52
|
+
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
54
|
+
def send_whisper_request(file)
|
55
|
+
open_ai.audio.transcribe(
|
56
|
+
parameters: {
|
57
|
+
model: "whisper-1",
|
58
|
+
file: file
|
59
|
+
}
|
60
|
+
)
|
61
|
+
end
|
60
62
|
end
|
61
63
|
end
|
data/lib/open_ai_bot.rb
CHANGED
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
require_relative "open_ai/chat_gpt"
|
4
4
|
require_relative "open_ai/chat_thread"
|
5
|
+
require_relative "open_ai/message"
|
5
6
|
require_relative "open_ai/dalle"
|
6
7
|
require_relative "open_ai/utils"
|
7
8
|
require_relative "open_ai/whisper"
|
8
9
|
|
9
10
|
class OpenAIBot < Rubydium::Bot
|
10
|
-
include ChatGPT
|
11
|
-
include Dalle
|
12
|
-
include Utils
|
13
|
-
include Whisper
|
11
|
+
include OpenAI::ChatGPT
|
12
|
+
include OpenAI::Dalle
|
13
|
+
include OpenAI::Utils
|
14
|
+
include OpenAI::Whisper
|
14
15
|
|
15
16
|
on_every_message :handle_gpt_command
|
16
17
|
on_every_message :transcribe
|
data/main.rb
CHANGED
data/open_ai_bot.gemspec
CHANGED
@@ -3,9 +3,12 @@
|
|
3
3
|
require "rubydium"
|
4
4
|
require_relative "lib/open_ai_bot"
|
5
5
|
|
6
|
+
require_relative "lib/ext/blank"
|
7
|
+
require_relative "lib/ext/in"
|
8
|
+
|
6
9
|
Gem::Specification.new do |spec|
|
7
10
|
spec.name = "open_ai_bot"
|
8
|
-
spec.version = "0.2.
|
11
|
+
spec.version = "0.2.4"
|
9
12
|
spec.authors = ["bulgakke"]
|
10
13
|
spec.email = ["vvp835@yandex.ru"]
|
11
14
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: open_ai_bot
|
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
|
- bulgakke
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-09-
|
11
|
+
date: 2023-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|
@@ -94,10 +94,13 @@ files:
|
|
94
94
|
- README.md
|
95
95
|
- config.yaml.example
|
96
96
|
- lib/clean_bot.rb
|
97
|
+
- lib/ext/blank.rb
|
98
|
+
- lib/ext/in.rb
|
97
99
|
- lib/my_custom_bot.rb
|
98
100
|
- lib/open_ai/chat_gpt.rb
|
99
101
|
- lib/open_ai/chat_thread.rb
|
100
102
|
- lib/open_ai/dalle.rb
|
103
|
+
- lib/open_ai/message.rb
|
101
104
|
- lib/open_ai/utils.rb
|
102
105
|
- lib/open_ai/whisper.rb
|
103
106
|
- lib/open_ai_bot.rb
|