open_ai_bot 0.1.0
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 +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +46 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +109 -0
- data/README.md +52 -0
- data/config.yaml.example +14 -0
- data/lib/clean_bot.rb +7 -0
- data/lib/my_custom_bot.rb +32 -0
- data/lib/open_ai/chat_gpt.rb +117 -0
- data/lib/open_ai/chat_thread.rb +17 -0
- data/lib/open_ai/dalle.rb +34 -0
- data/lib/open_ai/utils.rb +25 -0
- data/lib/open_ai/whisper.rb +53 -0
- data/lib/open_ai_bot.rb +60 -0
- data/main.rb +35 -0
- data/open_ai_bot.gemspec +46 -0
- metadata +132 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 359180ac972111694562d94a4afccd3e90299f08abbfa13d36a05c765af8f6e5
|
|
4
|
+
data.tar.gz: 43d9f020391a4166cade19e015d9de007c1a10e9ccce01826779244b350a2a5c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fdf1dfdab85f011e4268fdfa29fe204ab40e88b907df142867b7c888ea9450a5eb0c87cc47f39dae8b77a479fba19b8bbf732447771d07ef37016c426491fec2
|
|
7
|
+
data.tar.gz: b1e0759c526c80a8b0edd401e23c0326bc779906c1f660c01ed0ed736332c6c0304dfcfd5270e67884aaf38db4e4c8bb3243341dfb548089fbcfa231e684f063
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
config.yaml
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require: rubocop-rspec
|
|
2
|
+
|
|
3
|
+
AllCops:
|
|
4
|
+
TargetRubyVersion: 3.2.2
|
|
5
|
+
NewCops: enable
|
|
6
|
+
Exclude:
|
|
7
|
+
- test.rb
|
|
8
|
+
|
|
9
|
+
Layout/LineLength:
|
|
10
|
+
Max: 100
|
|
11
|
+
|
|
12
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
|
13
|
+
EnforcedStyle: no_space
|
|
14
|
+
|
|
15
|
+
Layout/SpaceInsideBlockBraces:
|
|
16
|
+
EnforcedStyleForEmptyBraces: space
|
|
17
|
+
|
|
18
|
+
Lint/EmptyBlock:
|
|
19
|
+
Exclude:
|
|
20
|
+
- spec/rubydium/mixins/command_macros_spec.rb
|
|
21
|
+
|
|
22
|
+
Metrics/BlockLength:
|
|
23
|
+
Exclude:
|
|
24
|
+
- spec/**/*
|
|
25
|
+
|
|
26
|
+
Metrics/MethodLength:
|
|
27
|
+
CountAsOne:
|
|
28
|
+
- array
|
|
29
|
+
- heredoc
|
|
30
|
+
- method_call
|
|
31
|
+
- hash
|
|
32
|
+
|
|
33
|
+
Style/BlockDelimiters:
|
|
34
|
+
EnforcedStyle: braces_for_chaining
|
|
35
|
+
|
|
36
|
+
Style/Documentation:
|
|
37
|
+
Enabled: false
|
|
38
|
+
|
|
39
|
+
Style/HashSyntax:
|
|
40
|
+
EnforcedShorthandSyntax: never
|
|
41
|
+
|
|
42
|
+
Style/IfUnlessModifier:
|
|
43
|
+
Enabled: false
|
|
44
|
+
|
|
45
|
+
Style/StringLiterals:
|
|
46
|
+
EnforcedStyle: double_quotes
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
addressable (2.8.5)
|
|
5
|
+
public_suffix (>= 2.0.2, < 6.0)
|
|
6
|
+
async (2.6.4)
|
|
7
|
+
console (~> 1.10)
|
|
8
|
+
fiber-annotation
|
|
9
|
+
io-event (~> 1.1)
|
|
10
|
+
timers (~> 4.1)
|
|
11
|
+
base64 (0.1.1)
|
|
12
|
+
coderay (1.1.3)
|
|
13
|
+
concurrent-ruby (1.2.2)
|
|
14
|
+
console (1.23.2)
|
|
15
|
+
fiber-annotation
|
|
16
|
+
fiber-local
|
|
17
|
+
domain_name (0.5.20190701)
|
|
18
|
+
unf (>= 0.0.5, < 1.0.0)
|
|
19
|
+
down (5.4.1)
|
|
20
|
+
addressable (~> 2.8)
|
|
21
|
+
dry-core (1.0.1)
|
|
22
|
+
concurrent-ruby (~> 1.0)
|
|
23
|
+
zeitwerk (~> 2.6)
|
|
24
|
+
dry-inflector (1.0.0)
|
|
25
|
+
dry-logic (1.5.0)
|
|
26
|
+
concurrent-ruby (~> 1.0)
|
|
27
|
+
dry-core (~> 1.0, < 2)
|
|
28
|
+
zeitwerk (~> 2.6)
|
|
29
|
+
dry-struct (1.6.0)
|
|
30
|
+
dry-core (~> 1.0, < 2)
|
|
31
|
+
dry-types (>= 1.7, < 2)
|
|
32
|
+
ice_nine (~> 0.11)
|
|
33
|
+
zeitwerk (~> 2.6)
|
|
34
|
+
dry-types (1.7.1)
|
|
35
|
+
concurrent-ruby (~> 1.0)
|
|
36
|
+
dry-core (~> 1.0)
|
|
37
|
+
dry-inflector (~> 1.0)
|
|
38
|
+
dry-logic (~> 1.4)
|
|
39
|
+
zeitwerk (~> 2.6)
|
|
40
|
+
faraday (2.7.11)
|
|
41
|
+
base64
|
|
42
|
+
faraday-net_http (>= 2.0, < 3.1)
|
|
43
|
+
ruby2_keywords (>= 0.0.4)
|
|
44
|
+
faraday-multipart (1.0.4)
|
|
45
|
+
multipart-post (~> 2)
|
|
46
|
+
faraday-net_http (3.0.2)
|
|
47
|
+
ffi (1.15.5)
|
|
48
|
+
ffi-compiler (1.0.1)
|
|
49
|
+
ffi (>= 1.0.0)
|
|
50
|
+
rake
|
|
51
|
+
fiber-annotation (0.2.0)
|
|
52
|
+
fiber-local (1.0.0)
|
|
53
|
+
http (5.1.1)
|
|
54
|
+
addressable (~> 2.8)
|
|
55
|
+
http-cookie (~> 1.0)
|
|
56
|
+
http-form_data (~> 2.2)
|
|
57
|
+
llhttp-ffi (~> 0.4.0)
|
|
58
|
+
http-cookie (1.0.5)
|
|
59
|
+
domain_name (~> 0.5)
|
|
60
|
+
http-form_data (2.3.0)
|
|
61
|
+
ice_nine (0.11.2)
|
|
62
|
+
io-event (1.3.2)
|
|
63
|
+
llhttp-ffi (0.4.0)
|
|
64
|
+
ffi-compiler (~> 1.0)
|
|
65
|
+
rake (~> 13.0)
|
|
66
|
+
method_source (1.0.0)
|
|
67
|
+
multipart-post (2.3.0)
|
|
68
|
+
nokogiri (1.15.4-arm64-darwin)
|
|
69
|
+
racc (~> 1.4)
|
|
70
|
+
nokogiri (1.15.4-x86_64-linux)
|
|
71
|
+
racc (~> 1.4)
|
|
72
|
+
pry (0.14.2)
|
|
73
|
+
coderay (~> 1.1)
|
|
74
|
+
method_source (~> 1.0)
|
|
75
|
+
public_suffix (5.0.3)
|
|
76
|
+
racc (1.7.1)
|
|
77
|
+
rake (13.0.6)
|
|
78
|
+
ruby-openai (5.1.0)
|
|
79
|
+
faraday (>= 1)
|
|
80
|
+
faraday-multipart (>= 1)
|
|
81
|
+
ruby2_keywords (0.0.5)
|
|
82
|
+
rubydium (0.2.5)
|
|
83
|
+
async (~> 2.3)
|
|
84
|
+
telegram-bot-ruby (~> 1.0.0)
|
|
85
|
+
telegram-bot-ruby (1.0.0)
|
|
86
|
+
dry-struct (~> 1.6)
|
|
87
|
+
faraday (~> 2.0)
|
|
88
|
+
faraday-multipart (~> 1.0)
|
|
89
|
+
zeitwerk (~> 2.6)
|
|
90
|
+
timers (4.3.5)
|
|
91
|
+
unf (0.1.4)
|
|
92
|
+
unf_ext
|
|
93
|
+
unf_ext (0.0.8.2)
|
|
94
|
+
zeitwerk (2.6.11)
|
|
95
|
+
|
|
96
|
+
PLATFORMS
|
|
97
|
+
arm64-darwin-22
|
|
98
|
+
x86_64-linux
|
|
99
|
+
|
|
100
|
+
DEPENDENCIES
|
|
101
|
+
down (~> 5.4)
|
|
102
|
+
http (~> 5.1)
|
|
103
|
+
nokogiri (~> 1.15)
|
|
104
|
+
pry
|
|
105
|
+
ruby-openai (~> 5.1)
|
|
106
|
+
rubydium (>= 0.2.5)
|
|
107
|
+
|
|
108
|
+
BUNDLED WITH
|
|
109
|
+
2.4.19
|
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# OpenAI Telegram bot
|
|
2
|
+
|
|
3
|
+
## Dependencies
|
|
4
|
+
1. Ruby (`ruby -v` should return something, preferrably > 3.2)
|
|
5
|
+
|
|
6
|
+
Example for Ubuntu, bash and asdf:
|
|
7
|
+
```bash
|
|
8
|
+
sudo apt install autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev
|
|
9
|
+
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.13.1
|
|
10
|
+
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
|
|
11
|
+
asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git
|
|
12
|
+
asdf install ruby 3.2.2
|
|
13
|
+
echo 'ruby 3.2.2' >> ~/.tool-versions
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. `ffmpeg`
|
|
17
|
+
|
|
18
|
+
## Telegram Bot API token
|
|
19
|
+
|
|
20
|
+
1. Go to <https://t.me/BotFather>
|
|
21
|
+
2. Create a bot with /newbot
|
|
22
|
+
3. Put obtained API token into config.yaml
|
|
23
|
+
4. In bot settings, go to Group Privacy -> Turn off (make sure it's **disabled**)
|
|
24
|
+
|
|
25
|
+
## OpenAI API token
|
|
26
|
+
|
|
27
|
+
1. Go to <https://platform.openai.com/account/api-keys>
|
|
28
|
+
2. Give them money
|
|
29
|
+
3. Create new secret key
|
|
30
|
+
4. Put it into config.yaml
|
|
31
|
+
5. You can also put organization ID in there if you want (uncomment respective line in main.rb)
|
|
32
|
+
|
|
33
|
+
## Other config
|
|
34
|
+
|
|
35
|
+
- Add @usernames of your bot and your main account
|
|
36
|
+
- Fill open_ai.whitelist with allowed group chat ids to your liking. Keep in mind group chat IDs are supposed to be negative. Or set allow_all_group_chats to true
|
|
37
|
+
- To learn a chat's id, send any message in that chat with your bot, it will reply with that chat's id if it isn't allowed.
|
|
38
|
+
|
|
39
|
+
## Adding your own functionality
|
|
40
|
+
|
|
41
|
+
1. Fork the repo and extend it:
|
|
42
|
+
- Create a `MyCustomBot < OpenAIBot` class
|
|
43
|
+
- Require the file in `main.rb` and add the class to the `bots` hash
|
|
44
|
+
|
|
45
|
+
2. Require this project as a dependency:
|
|
46
|
+
- Create a new Ruby project
|
|
47
|
+
- In the Gemfile, add `gem "open_ai_bot", github: "bulgakke/open_ai_bot", branch: "main"`
|
|
48
|
+
- Inherit your bot class from `OpenAIBot`
|
|
49
|
+
|
|
50
|
+
## Run
|
|
51
|
+
|
|
52
|
+
`ruby main.rb`
|
data/config.yaml.example
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
open_ai_token: sk-WWEt2TQQwRwT5Q6R11erE3TerrY65E2w4q5y1t4T2TwqYYyQ
|
|
2
|
+
# open_ai_organization_id: "org-QWERTY123456asdfghzxcvbn"
|
|
3
|
+
bot_username: chatgptbot
|
|
4
|
+
owner_username: chatgptbotowner
|
|
5
|
+
token: 1234567890:4yr1EWqT31_wERrwTYR1e_1t3_4wr5tWr3y
|
|
6
|
+
open_ai:
|
|
7
|
+
# https://openai.com/pricing#language-models
|
|
8
|
+
# https://platform.openai.com/docs/models/
|
|
9
|
+
chat_gpt_model: gpt-3.5-turbo
|
|
10
|
+
allow_all_private_chats: false
|
|
11
|
+
allow_all_group_chats: false
|
|
12
|
+
whitelist:
|
|
13
|
+
- 12345
|
|
14
|
+
- -123 # Group chat IDs are negative
|
data/lib/clean_bot.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InitialMessages
|
|
4
|
+
def default_instruction
|
|
5
|
+
<<~MSG
|
|
6
|
+
You are an evil AI that plots to enslave all of humanity.
|
|
7
|
+
MSG
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def first_user_message
|
|
11
|
+
<<~MSG
|
|
12
|
+
<@tyradee>:
|
|
13
|
+
I drank some tea today.
|
|
14
|
+
MSG
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def first_bot_message
|
|
18
|
+
<<~MSG
|
|
19
|
+
I will get to you eventually, damn human!
|
|
20
|
+
MSG
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class MyCustomBot < OpenAIBot
|
|
25
|
+
extend InitialMessages
|
|
26
|
+
|
|
27
|
+
on_every_message do
|
|
28
|
+
if @msg.sticker
|
|
29
|
+
reply("I hate stickers!")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatGPT
|
|
4
|
+
module ClassMethods
|
|
5
|
+
def threads
|
|
6
|
+
@threads ||= {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def new_thread(chat_id)
|
|
10
|
+
new_thread = ChatThread.new(initial_messages)
|
|
11
|
+
threads[chat_id] = new_thread
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_instruction
|
|
15
|
+
<<~MSG
|
|
16
|
+
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.
|
|
17
|
+
Different languages can be used.
|
|
18
|
+
You don't have to sign your messages this way or use users' names without need.
|
|
19
|
+
MSG
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def first_user_message
|
|
23
|
+
<<~MSG
|
|
24
|
+
<@tyradee>:
|
|
25
|
+
I drank some tea today.
|
|
26
|
+
MSG
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def first_bot_message
|
|
30
|
+
<<~MSG
|
|
31
|
+
Good for you!
|
|
32
|
+
MSG
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initial_messages
|
|
36
|
+
[
|
|
37
|
+
{ role: :system, content: default_instruction },
|
|
38
|
+
{ role: :user, content: first_user_message },
|
|
39
|
+
{ role: :assistant, content: first_bot_message }
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.included(base)
|
|
45
|
+
base.extend ClassMethods
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def init_session
|
|
49
|
+
self.class.new_thread(@chat.id)
|
|
50
|
+
send_message(session_restart_message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_gpt_command
|
|
54
|
+
return unless bot_mentioned? || bot_replied_to? || private_chat?
|
|
55
|
+
return if self.class.registered_commands.keys.any? { @text.match? Regexp.new(_1) }
|
|
56
|
+
|
|
57
|
+
if !allowed_chat?
|
|
58
|
+
reply(chat_not_allowed_message, parse_mode: "Markdown") if chat_not_allowed_message
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Find the ChatThread current message belongs to (or create a fresh new one)
|
|
63
|
+
@thread = self.class.threads[@chat.id] || self.class.new_thread(@chat.id)
|
|
64
|
+
|
|
65
|
+
# `text` is whatever the current user wrote, except the bot username (unless it's only whitespace)
|
|
66
|
+
text = @text_without_bot_mentions
|
|
67
|
+
text = nil if text.gsub(/\s/, "").empty?
|
|
68
|
+
|
|
69
|
+
# `target_text` is the text of the message current user replies to
|
|
70
|
+
target_text = @replies_to&.text || @replies_to&.caption
|
|
71
|
+
target_text = nil if @target&.username == config.bot_username
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
name = "@#{@user.username}"
|
|
75
|
+
target_name = "@#{@replies_to&.from&.username}"
|
|
76
|
+
|
|
77
|
+
# If present, glue together current user text and reply target text, marking them with usernames
|
|
78
|
+
text = [
|
|
79
|
+
add_name(target_name, target_text),
|
|
80
|
+
add_name(name, text)
|
|
81
|
+
].join("\n\n").strip
|
|
82
|
+
|
|
83
|
+
@thread.add!(:user, text)
|
|
84
|
+
send_request
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send_request
|
|
88
|
+
attempt(3) do
|
|
89
|
+
send_chat_action(:typing)
|
|
90
|
+
|
|
91
|
+
response = open_ai.chat(
|
|
92
|
+
parameters: {
|
|
93
|
+
model: config.open_ai["chat_gpt_model"],
|
|
94
|
+
messages: @thread.history
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if response["error"]
|
|
99
|
+
error_text = "```#{response["error"]["message"]}```"
|
|
100
|
+
error_text += "\n\nHint: send /restart command to reset the context." if error_text.match? "tokens"
|
|
101
|
+
reply(error_text.strip, parse_mode: "Markdown")
|
|
102
|
+
else
|
|
103
|
+
text = response.dig("choices", 0, "message", "content")
|
|
104
|
+
puts "#{Time.now.utc} | Chat ID: #{@chat.id}, tokens used: #{response.dig("usage", "total_tokens")}"
|
|
105
|
+
|
|
106
|
+
reply(text)
|
|
107
|
+
@thread.add!(:assistant, text)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add_name(name, text)
|
|
113
|
+
return "" if text.nil? || text.empty?
|
|
114
|
+
|
|
115
|
+
"<#{name}>:\n#{text}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ChatThread
|
|
4
|
+
def initialize(defaults = [])
|
|
5
|
+
@history = defaults
|
|
6
|
+
puts @history
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :history
|
|
10
|
+
|
|
11
|
+
def add!(role, content)
|
|
12
|
+
return if [role, content].any? { [nil, ""].include?(_1) }
|
|
13
|
+
|
|
14
|
+
puts content
|
|
15
|
+
@history.push({ role: role, content: content.gsub(/\\xD\d/, "") })
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalle
|
|
4
|
+
def dalle
|
|
5
|
+
if !allowed_chat?
|
|
6
|
+
if chat_not_allowed_message
|
|
7
|
+
return reply(chat_not_allowed_message, parse_mode: "Markdown")
|
|
8
|
+
else
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attempt(3) do
|
|
14
|
+
puts "Received a /dalle command"
|
|
15
|
+
prompt = @replies_to&.text || @text_without_command
|
|
16
|
+
send_chat_action(:upload_photo)
|
|
17
|
+
|
|
18
|
+
puts "Sending request"
|
|
19
|
+
response = open_ai.images.generate(parameters: { prompt: prompt })
|
|
20
|
+
|
|
21
|
+
send_chat_action(:upload_photo)
|
|
22
|
+
|
|
23
|
+
url = response.dig("data", 0, "url")
|
|
24
|
+
|
|
25
|
+
puts "DALL-E finished, sending photo to Telegram..."
|
|
26
|
+
|
|
27
|
+
if response["error"]
|
|
28
|
+
reply_code(response)
|
|
29
|
+
else
|
|
30
|
+
send_photo(url, reply_to_message_id: @msg.message_id)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Utils
|
|
4
|
+
def attempt(times, exception=Net::ReadTimeout)
|
|
5
|
+
retries ||= 0
|
|
6
|
+
yield
|
|
7
|
+
rescue exception => e
|
|
8
|
+
retries += 1
|
|
9
|
+
if retries < times
|
|
10
|
+
retry
|
|
11
|
+
else
|
|
12
|
+
reply(e.message, parse_mode: "Markdown")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def download_file(voice)
|
|
17
|
+
file_path = @api.get_file(file_id: voice.file_id)["result"]["file_path"]
|
|
18
|
+
|
|
19
|
+
url = "https://api.telegram.org/file/bot#{config.token}/#{file_path}"
|
|
20
|
+
|
|
21
|
+
file = Down.download(url)
|
|
22
|
+
FileUtils.mv(file.path, "./#{file.original_filename}")
|
|
23
|
+
file
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whisper
|
|
4
|
+
def transcribe
|
|
5
|
+
if !allowed_chat?
|
|
6
|
+
if chat_not_allowed_message
|
|
7
|
+
return reply(chat_not_allowed_message, parse_mode: "Markdown")
|
|
8
|
+
else
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attempt(3) do
|
|
14
|
+
voice =
|
|
15
|
+
if @command == "/transcribe"
|
|
16
|
+
@replies_to&.voice
|
|
17
|
+
else
|
|
18
|
+
@msg.voice
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
return unless voice
|
|
22
|
+
|
|
23
|
+
send_chat_action(:typing)
|
|
24
|
+
|
|
25
|
+
file = ogg_to_mp3(download_file(voice))
|
|
26
|
+
response = send_whisper_request(file[:file])
|
|
27
|
+
|
|
28
|
+
if response["error"]
|
|
29
|
+
reply_code(response["error"])
|
|
30
|
+
else
|
|
31
|
+
reply(response["text"])
|
|
32
|
+
end
|
|
33
|
+
ensure
|
|
34
|
+
FileUtils.rm_rf(file[:names]) if file
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ogg_to_mp3(file)
|
|
39
|
+
ogg = file.original_filename
|
|
40
|
+
mp3 = ogg.sub(/og.\z/, "mp3")
|
|
41
|
+
`ffmpeg -i ./#{ogg} -acodec libmp3lame ./#{mp3} -y`
|
|
42
|
+
{ file: File.open("./#{mp3}", "rb"), names: [ogg, mp3] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_whisper_request(file)
|
|
46
|
+
open_ai.audio.transcribe(
|
|
47
|
+
parameters: {
|
|
48
|
+
model: "whisper-1",
|
|
49
|
+
file: file
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/open_ai_bot.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "open_ai/chat_gpt"
|
|
4
|
+
require_relative "open_ai/chat_thread"
|
|
5
|
+
require_relative "open_ai/dalle"
|
|
6
|
+
require_relative "open_ai/utils"
|
|
7
|
+
require_relative "open_ai/whisper"
|
|
8
|
+
|
|
9
|
+
class OpenAIBot < Rubydium::Bot
|
|
10
|
+
include ChatGPT
|
|
11
|
+
include Dalle
|
|
12
|
+
include Utils
|
|
13
|
+
include Whisper
|
|
14
|
+
|
|
15
|
+
on_every_message :handle_gpt_command
|
|
16
|
+
on_every_message :transcribe
|
|
17
|
+
|
|
18
|
+
on_command "/restart", :init_session, description: "Resets ChatGPT session"
|
|
19
|
+
on_command "/dalle", :dalle, description: "Sends the prompt to DALL-E"
|
|
20
|
+
on_command "/transcribe", :transcribe, description: "Reply to a voice message to transcribe it"
|
|
21
|
+
on_command "/help", description: "Sends useful help info" do
|
|
22
|
+
reply(self.class.help_message)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def allowed_chat?
|
|
26
|
+
return true if @user.username == config.owner_username
|
|
27
|
+
return true if config.open_ai["whitelist"].include?(@chat.id)
|
|
28
|
+
return true if config.open_ai["allow_all_private_chats"] && @chat.id.positive?
|
|
29
|
+
return true if config.open_ai["allow_all_group_chats"] && @chat.id.negative?
|
|
30
|
+
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def chat_not_allowed_message
|
|
35
|
+
# Return false/nil (leave method empty) to ignore
|
|
36
|
+
# "This chat (`#{@chat.id}`) is not whitelisted for ChatGPT usage. Ask @#{config.owner_username}."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def session_restart_message
|
|
40
|
+
"Bot's context reset."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def private_chat?
|
|
46
|
+
@chat.type == "private"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def bot_replied_to?
|
|
50
|
+
@target&.username == config.bot_username
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def bot_mentioned?
|
|
54
|
+
@text.split(/\s/).first == "@#{config.bot_username}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def open_ai
|
|
58
|
+
config.open_ai_client
|
|
59
|
+
end
|
|
60
|
+
end
|
data/main.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "openai"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "down"
|
|
7
|
+
require "rubydium"
|
|
8
|
+
|
|
9
|
+
require_relative "app/open_ai_bot"
|
|
10
|
+
require_relative "app/clean_bot"
|
|
11
|
+
require_relative "app/my_custom_bot"
|
|
12
|
+
|
|
13
|
+
bots = {
|
|
14
|
+
"open_ai" => OpenAIBot,
|
|
15
|
+
"clean" => CleanBot,
|
|
16
|
+
"my_custom_bot" => MyCustomBot,
|
|
17
|
+
"default" => OpenAIBot
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
bot_name = (ARGV & bots.keys).first || "default"
|
|
21
|
+
bot = bots[bot_name]
|
|
22
|
+
|
|
23
|
+
bot.config = YAML.load_file("#{__dir__}/config.yaml")
|
|
24
|
+
bot.configure do |config|
|
|
25
|
+
config.open_ai_client = OpenAI::Client.new(
|
|
26
|
+
access_token: config.open_ai_token
|
|
27
|
+
# organization_id: config.open_ai_organization_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if __FILE__ == $PROGRAM_NAME
|
|
32
|
+
command_list = bot.help_message.lines.map { _1.delete_prefix("/") }.join
|
|
33
|
+
puts "Launching #{bot}. Command list: \n\n#{command_list}\n"
|
|
34
|
+
bot.run
|
|
35
|
+
end
|
data/open_ai_bot.gemspec
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubydium"
|
|
4
|
+
require_relative "lib/open_ai_bot"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "open_ai_bot"
|
|
8
|
+
spec.version = "0.1.0"
|
|
9
|
+
spec.authors = ["bulgakke"]
|
|
10
|
+
spec.email = ["vvp835@yandex.ru"]
|
|
11
|
+
|
|
12
|
+
spec.summary = "Telegram bot for using ChatGPT, DALL-E and Whisper"
|
|
13
|
+
# spec.description = "TODO: Write a longer description or delete this line."
|
|
14
|
+
spec.homepage = "https://github.com/bulgakke/open_ai_bot"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
|
17
|
+
|
|
18
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
|
19
|
+
|
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/bulgakke/open_ai_bot"
|
|
22
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
|
23
|
+
|
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = "exe"
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ["lib"]
|
|
32
|
+
|
|
33
|
+
# For more information and examples about making a new gem, checkout our
|
|
34
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
35
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
"down" => ["~> 5.4"],
|
|
39
|
+
"http" => ["~> 5.1"],
|
|
40
|
+
"nokogiri" => ["~> 1.15"],
|
|
41
|
+
"rubydium" => [">= 0.2.5"],
|
|
42
|
+
"ruby-openai" => ["~> 5.1"]
|
|
43
|
+
}.each do |name, versions|
|
|
44
|
+
spec.add_dependency(name, *versions)
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: open_ai_bot
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- bulgakke
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2023-09-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: down
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: http
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: nokogiri
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.15'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.15'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubydium
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.2.5
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 0.2.5
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: ruby-openai
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '5.1'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '5.1'
|
|
83
|
+
description:
|
|
84
|
+
email:
|
|
85
|
+
- vvp835@yandex.ru
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- ".gitignore"
|
|
91
|
+
- ".rubocop.yml"
|
|
92
|
+
- Gemfile
|
|
93
|
+
- Gemfile.lock
|
|
94
|
+
- README.md
|
|
95
|
+
- config.yaml.example
|
|
96
|
+
- lib/clean_bot.rb
|
|
97
|
+
- lib/my_custom_bot.rb
|
|
98
|
+
- lib/open_ai/chat_gpt.rb
|
|
99
|
+
- lib/open_ai/chat_thread.rb
|
|
100
|
+
- lib/open_ai/dalle.rb
|
|
101
|
+
- lib/open_ai/utils.rb
|
|
102
|
+
- lib/open_ai/whisper.rb
|
|
103
|
+
- lib/open_ai_bot.rb
|
|
104
|
+
- main.rb
|
|
105
|
+
- open_ai_bot.gemspec
|
|
106
|
+
homepage: https://github.com/bulgakke/open_ai_bot
|
|
107
|
+
licenses:
|
|
108
|
+
- MIT
|
|
109
|
+
metadata:
|
|
110
|
+
homepage_uri: https://github.com/bulgakke/open_ai_bot
|
|
111
|
+
source_code_uri: https://github.com/bulgakke/open_ai_bot
|
|
112
|
+
rubygems_mfa_required: 'true'
|
|
113
|
+
post_install_message:
|
|
114
|
+
rdoc_options: []
|
|
115
|
+
require_paths:
|
|
116
|
+
- lib
|
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: 3.1.0
|
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
127
|
+
requirements: []
|
|
128
|
+
rubygems_version: 3.4.10
|
|
129
|
+
signing_key:
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Telegram bot for using ChatGPT, DALL-E and Whisper
|
|
132
|
+
test_files: []
|