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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb356e64b39be0d26d67b951fb369bfbe73f7ef75dbd1b7713ef34a69c609702
4
- data.tar.gz: 6bd88aac60d93d779722799e96e583d876e34c352c07dca7ede1e0d5cd77dcbe
3
+ metadata.gz: 867a815eac31b0a468c48e4318cdf2d235d575a7f950fb6dda6e084947b5b31f
4
+ data.tar.gz: 1d18f4c71a5725d06325f0e6725328af9acae9d9afa25dd1a91696b8e93d6e3f
5
5
  SHA512:
6
- metadata.gz: 1db16f7ad18c4b4f1418aefd245f40fd125bc3cc12e73b4bd90fbedafdbdc5280fc2ce4591049e0fdb1bd208b83301b2ff60bc25459c9f10181e465a31d3b2c3
7
- data.tar.gz: '097d28aa7788ae0d33a41c8213de556ddf8a01f7ba07e965ae57093bcca134105cdb64546ffa34ab0d5ede6a59602c45a49ddbadb785eaab8371cb0dec552bcb'
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
@@ -0,0 +1,5 @@
1
+ Object.class_eval do
2
+ def in?(collection)
3
+ collection.include?(self)
4
+ end
5
+ end
@@ -1,97 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ChatGPT
4
- module ClassMethods
5
- def threads
6
- @threads ||= {}
7
- end
3
+ module OpenAI
4
+ module ChatGPT
5
+ module ClassMethods
6
+ def threads
7
+ @threads ||= {}
8
+ end
8
9
 
9
- def new_thread(chat_id)
10
- new_thread = ChatThread.new(initial_messages)
11
- threads[chat_id] = new_thread
12
- end
10
+ def new_thread(chat_id)
11
+ new_thread = ChatThread.new(initial_messages)
12
+ threads[chat_id] = new_thread
13
+ end
13
14
 
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
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
- def first_user_message
23
- <<~MSG
24
- <@tyradee>:
25
- I drank some tea today.
26
- MSG
27
- end
22
+ Different languages can be used.
23
+ MSG
28
24
 
29
- def first_bot_message
30
- <<~MSG
31
- Good for you!
32
- MSG
33
- end
25
+ SystemMessage.new(
26
+ body: msg
27
+ )
28
+ end
34
29
 
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
30
+ def first_user_message
31
+ Message.new(
32
+ from: "@tyradee",
33
+ body: "I drank some tea today."
34
+ )
35
+ end
43
36
 
44
- def self.included(base)
45
- base.extend ClassMethods
46
- end
37
+ def first_bot_message
38
+ BotMessage.new(
39
+ body: "Good for you!"
40
+ )
41
+ end
47
42
 
48
- def init_session
49
- self.class.new_thread(@chat.id)
50
- send_message(session_restart_message)
51
- end
43
+ def initial_messages
44
+ [
45
+ default_instruction,
46
+ first_user_message,
47
+ first_bot_message
48
+ ]
49
+ end
50
+ end
52
51
 
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) }
52
+ def self.included(base)
53
+ base.extend ClassMethods
54
+ end
56
55
 
57
- if !allowed_chat?
58
- reply(chat_not_allowed_message, parse_mode: "Markdown") if chat_not_allowed_message
59
- return
56
+ def init_session
57
+ self.class.new_thread(@chat.id)
58
+ send_message(session_restart_message)
60
59
  end
61
60
 
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)
61
+ def current_thread
62
+ self.class.threads[@chat.id] || self.class.new_thread(@chat.id)
63
+ end
64
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?
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
- # `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
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
- name = "@#{@user.username}"
75
- target_name = "@#{@replies_to&.from&.username}"
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
- # 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
+ 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
- @thread.add!(:user, text)
84
- send_request
85
- end
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
- def send_request
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: @thread.history
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
- puts "#{Time.now.utc} | Chat ID: #{@chat.id}, tokens used: #{response.dig("usage", "total_tokens")}"
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
- def send_chat_gpt_error(text)
112
- reply(text, parse_mode: "Markdown")
113
- end
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
- "<#{name}>:\n#{text}"
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
@@ -1,17 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ChatThread
4
- def initialize(defaults = [])
5
- @history = defaults
6
- puts @history
7
- end
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
- attr_reader :history
16
+ @history << message
17
+ puts message
10
18
 
11
- def add!(role, content)
12
- return if [role, content].any? { [nil, ""].include?(_1) }
19
+ true
20
+ end
13
21
 
14
- puts content
15
- @history.push({ role: role, content: content.gsub(/\\xD\d/, "") })
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 Dalle
4
- def dalle
5
- return unless allowed_chat?
3
+ module OpenAI
4
+ module Dalle
5
+ def dalle
6
+ return unless allowed_chat?
6
7
 
7
- attempt(3) do
8
- puts "Received a /dalle command"
9
- prompt = @replies_to&.text || @text_without_command
10
- send_chat_action(:upload_photo)
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
- puts "Sending request"
13
- response = open_ai.images.generate(parameters: { prompt: prompt })
13
+ puts "Sending request"
14
+ response = open_ai.images.generate(parameters: { prompt: prompt })
14
15
 
15
- send_chat_action(:upload_photo)
16
+ send_chat_action(:upload_photo)
16
17
 
17
- url = response.dig("data", 0, "url")
18
+ url = response.dig("data", 0, "url")
18
19
 
19
- puts "DALL-E finished, sending photo to Telegram..."
20
+ puts "DALL-E finished, sending photo to Telegram..."
20
21
 
21
- if response["error"]
22
- reply_code(response)
23
- else
24
- send_photo(url, reply_to_message_id: @msg.message_id)
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 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")
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
- def download_file(voice)
17
- file_path = @api.get_file(file_id: voice.file_id)["result"]["file_path"]
17
+ def download_file(voice)
18
+ file_path = @api.get_file(file_id: voice.file_id)["result"]["file_path"]
18
19
 
19
- url = "https://api.telegram.org/file/bot#{config.token}/#{file_path}"
20
+ url = "https://api.telegram.org/file/bot#{config.token}/#{file_path}"
20
21
 
21
- file = Down.download(url)
22
- FileUtils.mv(file.path, "./#{file.original_filename}")
23
- file
22
+ file = Down.download(url)
23
+ FileUtils.mv(file.path, "./#{file.original_filename}")
24
+ file
25
+ end
24
26
  end
25
27
  end
@@ -1,61 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Whisper
4
- def transcribe
5
- if !allowed_chat?
6
- if @command == "/transcribe"
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
- @replies_to&.voice
17
- else
18
- @msg.voice
8
+ reply(chat_not_allowed_message) if chat_not_allowed_message
19
9
  end
20
10
 
21
- return unless voice
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
- send_chat_action(:typing)
22
+ return unless voice
24
23
 
25
- file = ogg_to_mp3(download_file(voice))
26
- response = send_whisper_request(file[:file])
24
+ send_chat_action(:typing)
27
25
 
28
- if response["error"]
29
- send_whisper_error(response["error"])
30
- else
31
- send_whisper_response(response["text"])
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
- def send_whisper_response(text)
39
- reply(text)
40
- end
39
+ def send_whisper_response(text)
40
+ reply(text)
41
+ end
41
42
 
42
- def send_whisper_error
43
- reply_code(text)
44
- end
43
+ def send_whisper_error
44
+ reply_code(text)
45
+ end
45
46
 
46
- def ogg_to_mp3(file)
47
- ogg = file.original_filename
48
- mp3 = ogg.sub(/og.\z/, "mp3")
49
- `ffmpeg -i ./#{ogg} -acodec libmp3lame ./#{mp3} -y`
50
- { file: File.open("./#{mp3}", "rb"), names: [ogg, mp3] }
51
- end
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
- def send_whisper_request(file)
54
- open_ai.audio.transcribe(
55
- parameters: {
56
- model: "whisper-1",
57
- file: file
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
@@ -6,6 +6,9 @@ require "yaml"
6
6
  require "down"
7
7
  require "rubydium"
8
8
 
9
+ require_relative "lib/ext/blank"
10
+ require_relative "lib/ext/in"
11
+
9
12
  require_relative "lib/open_ai_bot"
10
13
  require_relative "lib/clean_bot"
11
14
  require_relative "lib/my_custom_bot"
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.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.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-18 00:00:00.000000000 Z
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