open_ai_bot 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +1 -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: 269af8f1a7f00af420ae1dfc7abc7f819dd145c9b3d63c757c0b40422c5391a1
|
4
|
+
data.tar.gz: 3befe567139afa676bdda80bfe153a9d56b6125d8bf284da496b376b08ecebbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c0b64fed5c62342bea3d5f9a1a8d129e5929184d17f50c90ccd610eda03377a955bd294ac8b54d960df05a6b170d31b06c52dd2fd517e60a8b3f7d278b47745
|
7
|
+
data.tar.gz: 1bcb5a576fbcc634e08c52db97d0fbed5283f24963b7174157841f925dd77df43d4c2a6422a4ce0bf6779f18333cc991a821c4f0828618bb651ba3ef952a9f02
|
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
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.3
|
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
|