rubydium 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 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