rubydium 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 538f8c4319a7140a96ed04555a91ddd99aaa9f0d93125c5c9a612cffd8077765
4
- data.tar.gz: e8c0e25a5c1c3de5ff22065d09f40daea053c98856606d615f660e07083ea47a
3
+ metadata.gz: ebfa6d1d46ec5b5d046aae44b857f59b405e5ae79af529d695a552090dc07fc0
4
+ data.tar.gz: 00cd4177e06fb2920a5ff6a1b8c3be332bc57cbf473624020704855df07dec07
5
5
  SHA512:
6
- metadata.gz: 202c749e8eb3a522c06b3af886b409512fa005eee587bd4a6fcb53be4ac895e04727c8ab5471962a6a4a1433dd5bdaddecf2835a110ffed67c90dc66487236c4
7
- data.tar.gz: 7ebbfe846222ff22ca618ef8ee2f7309c0b8529bd9d9571911011473a0bf0d24a564ce63b5bba40262558d4f8e1d533927d6f9972f5d70bd102ff09bba25c893
6
+ metadata.gz: f09704c30374104c0c0cf608ffae1ffad372e2b41317f9a324a6ae1200867393d461fe186ec2f62c5516c38c293ba812ef1016ccf8cab3342a655ca702390358
7
+ data.tar.gz: 478056c327c16135dd0a546fc88c53dd345ca5db7b30047b673c6765c0f22d07a54f8b877eae86a18dceef5b4de5c55db6ecb9a7a10fa65df05f4956e18c1c4e
data/Gemfile CHANGED
@@ -8,5 +8,5 @@ gem "pry"
8
8
  gem "rspec"
9
9
  gem "rubocop"
10
10
  gem "rubocop-rspec"
11
- gem "telegram-bot-ruby"
11
+ gem "telegram-bot-ruby", "~> 1.0.0"
12
12
  gem "async"
data/Gemfile.lock CHANGED
@@ -6,20 +6,33 @@ GEM
6
6
  console (~> 1.10)
7
7
  io-event (~> 1.1)
8
8
  timers (~> 4.1)
9
- axiom-types (0.1.1)
10
- descendants_tracker (~> 0.0.4)
11
- ice_nine (~> 0.11.0)
12
- thread_safe (~> 0.3, >= 0.3.1)
9
+ base64 (0.1.1)
13
10
  coderay (1.1.3)
14
- coercible (1.0.0)
15
- descendants_tracker (~> 0.0.1)
11
+ concurrent-ruby (1.2.2)
16
12
  console (1.16.2)
17
13
  fiber-local
18
- descendants_tracker (0.0.4)
19
- thread_safe (~> 0.3, >= 0.3.1)
20
14
  diff-lcs (1.5.0)
15
+ dry-core (1.0.1)
16
+ concurrent-ruby (~> 1.0)
17
+ zeitwerk (~> 2.6)
21
18
  dry-inflector (1.0.0)
22
- faraday (2.7.2)
19
+ dry-logic (1.5.0)
20
+ concurrent-ruby (~> 1.0)
21
+ dry-core (~> 1.0, < 2)
22
+ zeitwerk (~> 2.6)
23
+ dry-struct (1.6.0)
24
+ dry-core (~> 1.0, < 2)
25
+ dry-types (>= 1.7, < 2)
26
+ ice_nine (~> 0.11)
27
+ zeitwerk (~> 2.6)
28
+ dry-types (1.7.1)
29
+ concurrent-ruby (~> 1.0)
30
+ dry-core (~> 1.0)
31
+ dry-inflector (~> 1.0)
32
+ dry-logic (~> 1.4)
33
+ zeitwerk (~> 2.6)
34
+ faraday (2.7.11)
35
+ base64
23
36
  faraday-net_http (>= 2.0, < 3.1)
24
37
  ruby2_keywords (>= 0.0.4)
25
38
  faraday-multipart (1.0.4)
@@ -30,7 +43,7 @@ GEM
30
43
  io-event (1.1.5)
31
44
  json (2.6.3)
32
45
  method_source (1.0.0)
33
- multipart-post (2.2.3)
46
+ multipart-post (2.3.0)
34
47
  parallel (1.22.1)
35
48
  parser (3.1.3.0)
36
49
  ast (~> 2.4.1)
@@ -69,18 +82,14 @@ GEM
69
82
  rubocop (~> 1.33)
70
83
  ruby-progressbar (1.11.0)
71
84
  ruby2_keywords (0.0.5)
72
- telegram-bot-ruby (0.23.0)
73
- dry-inflector
85
+ telegram-bot-ruby (1.0.0)
86
+ dry-struct (~> 1.6)
74
87
  faraday (~> 2.0)
75
88
  faraday-multipart (~> 1.0)
76
- virtus (~> 2.0)
77
- thread_safe (0.3.6)
89
+ zeitwerk (~> 2.6)
78
90
  timers (4.3.5)
79
91
  unicode-display_width (2.3.0)
80
- virtus (2.0.0)
81
- axiom-types (~> 0.1)
82
- coercible (~> 1.0)
83
- descendants_tracker (~> 0.0, >= 0.0.3)
92
+ zeitwerk (2.6.11)
84
93
 
85
94
  PLATFORMS
86
95
  x86_64-linux
@@ -91,7 +100,7 @@ DEPENDENCIES
91
100
  rspec
92
101
  rubocop
93
102
  rubocop-rspec
94
- telegram-bot-ruby
103
+ telegram-bot-ruby (~> 1.0.0)
95
104
 
96
105
  BUNDLED WITH
97
106
  2.3.26
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubydium"
3
+ # require "rubydium"
4
+ require_relative "../lib/rubydium"
5
+ require "pry"
4
6
 
5
7
  # Your actual logic of handling the updates goes here.
6
8
  class ExampleBot < Rubydium::Bot
7
9
  on_every_message :log_message
8
10
 
9
11
  on_command "/help", description: "Show help message" do
10
- text = help_message
12
+ text = self.class.help_message
11
13
  send_message(text)
12
14
  end
13
15
 
14
16
  on_command "/start", :greet_user, description: "Say hello"
17
+ on_command "/pry" do
18
+ binding.pry
19
+ end
15
20
 
16
21
  def log_message
17
22
  puts "Got message from #{@user.first_name}, text: \n#{@text}"
@@ -24,12 +29,13 @@ class ExampleBot < Rubydium::Bot
24
29
  end
25
30
 
26
31
  ExampleBot.configure do |config|
27
- config.token = "1234567890:long_alphanumeric_string_goes_here"
28
- config.bot_username = "ends_with_bot"
29
- config.owner_username = "thats_you"
30
- config.privileged_usernames = %w[
31
- your_friend your_chat_moderator
32
- ]
32
+ config.token = "1043894792:AAGC_oA2Ztvo5bBZBPxX5_oUxFX-L_obZJI"#"1234567890:long_alphanumeric_string_goes_here"
33
+ config.bot_username = "bulgakkebot"#"ends_with_bot"
34
+ config.owner_username = "bulgakke"#"thats_you"
35
+ config.privileged_usernames = []
36
+ # %w[
37
+ # your_friend your_chat_moderator
38
+ # ]
33
39
  end
34
40
 
35
41
  ExampleBot.run
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "down", "~> 5.4"
6
+ gem "http", "~> 5.1"
7
+ gem "pry", "~> 0.14.2"
8
+ gem "rubydium", ">= 0.2.3"
9
+ gem "ruby-openai", "~> 5.1"
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chat_thread"
4
+
5
+ module ChatGPT
6
+ module ClassMethods
7
+ def threads
8
+ @threads ||= {}
9
+ end
10
+
11
+ def new_thread(chat_id)
12
+ new_thread = ChatThread.new(chat_id)
13
+ threads[chat_id] = new_thread
14
+ new_thread
15
+ end
16
+ end
17
+
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ def init_session
23
+ self.class.new_thread(@chat.id)
24
+ reply("Bot's context reset.")
25
+ end
26
+
27
+ def allowed_chat?
28
+ return true if @user.username == config.owner_username
29
+ return true if config.chat_gpt_allow_all_private_chats && @chat.id.positive?
30
+ return true if config.chat_gpt_allow_all_group_chats && @chat.id.negative?
31
+ return true if config.chat_gpt_whitelist.include?(@chat.id)
32
+
33
+ false
34
+ end
35
+
36
+ def handle_gpt_command
37
+ return if self.class.registered_commands.keys.any? { @text.match? Regexp.new(_1) }
38
+ return unless bot_mentioned? || bot_replied_to? || private_chat?
39
+
40
+ if !allowed_chat?
41
+ msg = "This chat (`#{@chat.id}`) is not whitelisted for ChatGPT usage. Ask @#{config.owner_username}."
42
+ return reply(msg, parse_mode: "Markdown")
43
+ end
44
+
45
+ text = @text_without_bot_mentions
46
+ text = nil if text.gsub(/\s/, "").empty?
47
+
48
+ target_text = @replies_to&.text || @replies_to&.caption
49
+ target_text = nil if @target&.username == config.bot_username
50
+
51
+ thread = self.class.threads[@chat.id] || self.class.new_thread(@chat.id)
52
+
53
+ name = "@#{@user.username}"
54
+
55
+ if target_text && bot_mentioned?
56
+ target_name = "@#{@replies_to.from.username}"
57
+ text = [add_name(target_name, target_text), add_name(name, text)].join("\n\n")
58
+ ask_gpt(name, text, thread)
59
+ elsif text
60
+ text = add_name(name, text)
61
+ ask_gpt(name, text, thread)
62
+ end
63
+ end
64
+
65
+ def ask_gpt(_name, prompt, thread)
66
+ thread.add!(:user, prompt)
67
+
68
+ send_request(thread)
69
+ end
70
+
71
+ def send_request(thread)
72
+ Async do |task|
73
+ request = Async do
74
+ attempt(3) do
75
+ response = open_ai.chat(
76
+ parameters: {
77
+ model: "gpt-3.5-turbo",
78
+ messages: thread.history
79
+ }
80
+ )
81
+
82
+ if response["error"]
83
+ error_text = response["error"]["message"]
84
+ error_text += "\n\nHint: press /start to reset the context." if error_text.match? "tokens"
85
+ raise Net::ReadTimeout, response["error"]["message"]
86
+ else
87
+ text = response.dig("choices", 0, "message", "content")
88
+ puts "#{Time.now.to_i} | Chat ID: #{@chat.id}, tokens used: #{response.dig("usage", "total_tokens")}"
89
+
90
+ reply(text)
91
+ thread.add!(:assistant, text)
92
+ end
93
+ end
94
+ end
95
+
96
+ status = task.async do
97
+ loop do
98
+ send_chat_action(:typing)
99
+ sleep 4.5
100
+ end
101
+ end
102
+ request.wait
103
+
104
+ status.stop
105
+ end
106
+ end
107
+
108
+ def add_name(name, text)
109
+ "<#{name}>:\n#{text}"
110
+ end
111
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChatThread
4
+ def initialize(_chat)
5
+ @history = [
6
+ {
7
+ role: :system,
8
+ content: default_instruction
9
+ },
10
+ {
11
+ role: :user,
12
+ content: first_user_message
13
+ },
14
+ {
15
+ role: :assistant,
16
+ content: first_bot_message
17
+ }
18
+ ]
19
+ end
20
+
21
+ attr_reader :history
22
+
23
+ def add!(role, content)
24
+ return if [role, content].any? { [nil, ""].include?(_1) }
25
+
26
+ @history.push({
27
+ role: role, content: content.gsub(/\\xD\d/, "")
28
+ })
29
+ end
30
+
31
+ def default_instruction
32
+ <<~MSG
33
+ Ты находишься в групповом чате. Здесь могут использоваться разные языки, так что отвечай на вопросы на том языке, на котором они заданы.
34
+
35
+ Помимо текста сообщений, первой строчкой ты будешь получать имя пользователя, который отправил это сообщение.
36
+ Пользователи могут просить обращаться к ним иначе, чем подписано сообщение.
37
+
38
+ Тебе не нужно подписывать свои сообщения и без необходимости вставлять имена других пользователей.
39
+
40
+ Если не до конца понимаешь, о чём вопрос - задавай уточняющий вопрос в ответ. Также изредка задавай общие вопросы для продвижения диалога.
41
+ MSG
42
+ end
43
+
44
+ def first_user_message
45
+ <<~MSG
46
+ <@tyradee>:
47
+
48
+ I drank some tea today.
49
+ MSG
50
+ end
51
+
52
+ def first_bot_message
53
+ <<~MSG
54
+ Good for you!
55
+ MSG
56
+ end
57
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalle
4
+ def dalle
5
+ attempt(3) do
6
+ prompt = @replies_to&.text || @text_without_command
7
+ send_chat_action(:upload_photo)
8
+
9
+ response = open_ai.images.generate(parameters: { prompt: prompt })
10
+
11
+ send_chat_action(:upload_photo)
12
+
13
+ url = response.dig("data", 0, "url")
14
+
15
+ if response["error"]
16
+ reply_code(response)
17
+ else
18
+ send_photo(url, reply_to_message_id: @msg.message_id)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubydium"
4
+ require "openai"
5
+ require "down"
6
+ require "pry"
7
+ require_relative "chat_gpt"
8
+ require_relative "dalle"
9
+ require_relative "utils"
10
+ require_relative "whisper"
11
+
12
+ # I wrote the code "until it works" as I went without much refactoring,
13
+ # so yes, it's not great, especially the ChatGPT functionality.
14
+ class ChatGPTBot < Rubydium::Bot
15
+ include ChatGPT
16
+ include Dalle
17
+ include Utils
18
+ include Whisper
19
+
20
+ # Whenever mentioned or replied to, finds or creates a conversation thread
21
+ # for this chat, add your message to it and returns ChatGPT's response.
22
+ on_every_message :handle_gpt_command
23
+ # Whenever a voice message appears in the chat, uses Whisper to transcribe
24
+ # it and sends the result.
25
+ # Set `ignore_forwarded` to false to automatically react to forwarded voice messages
26
+ on_every_message :transcribe, ignore_forwarded: true
27
+
28
+ on_command "/start", :init_session, description: "Resets ChatGPT session"
29
+ on_command "/dalle", :dalle, description: "Sends the prompt to DALL-E"
30
+ on_command "/transcribe", :transcribe, description: "Reply to a voice message to transcribe it"
31
+ on_command "/pry", :pry, description: "Open a debug session in the terminal with the context of this message"
32
+
33
+ def pry
34
+ binding.pry
35
+ end
36
+
37
+ private
38
+
39
+ def private_chat?
40
+ @chat.type == "private"
41
+ end
42
+
43
+ def bot_replied_to?
44
+ @target&.username == config.bot_username
45
+ end
46
+
47
+ def bot_mentioned?
48
+ @text.split(/\s/).first == "@#{config.bot_username}"
49
+ end
50
+
51
+ def open_ai
52
+ config.open_ai_client
53
+ end
54
+ end
55
+
56
+ ChatGPTBot.configure do |config|
57
+ config.token = "1234567890:long_alphanumeric_string_goes_here"
58
+ config.bot_username = "ends_with_bot"
59
+ config.owner_username = "thats_you"
60
+ config.chat_gpt_allow_all_group_chats = true
61
+ config.chat_gpt_allow_all_private_chats = true
62
+ config.open_ai_token = "sk-WWEt2TQQwRwT5Q6R11erE3TerrY65E2w4q5y1t4T2TwqYYyQ"
63
+ config.open_ai_client = OpenAI::Client.new(
64
+ access_token: config.open_ai_token
65
+ )
66
+ end
67
+
68
+ ChatGPTBot.run
@@ -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_code(e.message)
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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whisper
4
+ def transcribe
5
+ attempt(3) do
6
+ voice =
7
+ if @command == "/transcribe"
8
+ @replies_to&.voice
9
+ else
10
+ @msg.voice
11
+ end
12
+
13
+ return unless voice
14
+
15
+ send_chat_action(:typing)
16
+
17
+ file = ogg_to_mp3(download_file(voice))
18
+ response = send_whisper_request(file[:file])
19
+
20
+ if response["error"]
21
+ reply_code(response)
22
+ else
23
+ reply(response["text"])
24
+ end
25
+ ensure
26
+ FileUtils.rm_rf(file[:names]) if file
27
+ end
28
+ end
29
+
30
+ def ogg_to_mp3(file)
31
+ ogg = file.original_filename
32
+ mp3 = ogg.sub(/og.\z/, "mp3")
33
+ `ffmpeg -i ./#{ogg} -acodec libmp3lame ./#{mp3} -y`
34
+ { file: File.open("./#{mp3}", "rb"), names: [ogg, mp3] }
35
+ end
36
+
37
+ def send_whisper_request(file)
38
+ open_ai.audio.transcribe(
39
+ parameters: {
40
+ model: "whisper-1",
41
+ file: file
42
+ }
43
+ )
44
+ end
45
+ end
@@ -7,6 +7,7 @@ module Rubydium
7
7
  def send_message(text)
8
8
  @api.send_message(
9
9
  chat_id: @chat.id,
10
+ message_thread_id: @msg.message_thread_id,
10
11
  text: text
11
12
  )
12
13
  end
@@ -16,6 +17,7 @@ module Rubydium
16
17
 
17
18
  @api.send_sticker(
18
19
  chat_id: @chat.id,
20
+ message_thread_id: @msg.message_thread_id,
19
21
  sticker: sticker,
20
22
  **kwargs
21
23
  )
@@ -32,6 +34,7 @@ module Rubydium
32
34
  @api.send_chat_action(
33
35
  chat_id: @chat.id,
34
36
  action: action,
37
+ message_thread_id: @msg.message_thread_id,
35
38
  **kwargs
36
39
  )
37
40
  end
@@ -41,6 +44,7 @@ module Rubydium
41
44
 
42
45
  @api.send_video(
43
46
  chat_id: @chat.id,
47
+ message_thread_id: @msg.message_thread_id,
44
48
  video: video,
45
49
  **kwargs
46
50
  )
@@ -51,6 +55,7 @@ module Rubydium
51
55
 
52
56
  @api.send_photo(
53
57
  chat_id: @chat.id,
58
+ message_thread_id: @msg.message_thread_id,
54
59
  photo: photo,
55
60
  **kwargs
56
61
  )
@@ -59,6 +64,7 @@ module Rubydium
59
64
  def reply(text, **args)
60
65
  @api.send_message(
61
66
  chat_id: @chat.id,
67
+ message_thread_id: @msg.message_thread_id,
62
68
  reply_to_message_id: @message_id,
63
69
  text: text,
64
70
  **args
@@ -72,6 +78,7 @@ module Rubydium
72
78
  def reply_to_target(text)
73
79
  @api.send_message(
74
80
  chat_id: @chat.id,
81
+ message_thread_id: @msg.message_thread_id,
75
82
  reply_to_message_id: @replies_to.message_id,
76
83
  text: text
77
84
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubydium
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
data/rubydium.gemspec CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.metadata["rubygems_mfa_required"] = "true"
35
35
 
36
36
  {
37
- "telegram-bot-ruby" => ['~> 0.23.0'],
37
+ "telegram-bot-ruby" => ['~> 1.0.0'],
38
38
  "async" => ['~> 2.3']
39
39
  }.each do |name, versions|
40
40
  spec.add_dependency(name, *versions)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
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-14 00:00:00.000000000 Z
11
+ date: 2023-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: telegram-bot-ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.23.0
19
+ version: 1.0.0
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.23.0
26
+ version: 1.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: async
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -54,6 +54,13 @@ files:
54
54
  - LICENSE
55
55
  - README.md
56
56
  - example/example_bot.rb
57
+ - example/open_ai_bot/Gemfile
58
+ - example/open_ai_bot/chat_gpt.rb
59
+ - example/open_ai_bot/chat_thread.rb
60
+ - example/open_ai_bot/dalle.rb
61
+ - example/open_ai_bot/open_ai_bot.rb
62
+ - example/open_ai_bot/utils.rb
63
+ - example/open_ai_bot/whisper.rb
57
64
  - lib/rubydium.rb
58
65
  - lib/rubydium/bot.rb
59
66
  - lib/rubydium/config.rb