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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb356e64b39be0d26d67b951fb369bfbe73f7ef75dbd1b7713ef34a69c609702
4
- data.tar.gz: 6bd88aac60d93d779722799e96e583d876e34c352c07dca7ede1e0d5cd77dcbe
3
+ metadata.gz: 269af8f1a7f00af420ae1dfc7abc7f819dd145c9b3d63c757c0b40422c5391a1
4
+ data.tar.gz: 3befe567139afa676bdda80bfe153a9d56b6125d8bf284da496b376b08ecebbd
5
5
  SHA512:
6
- metadata.gz: 1db16f7ad18c4b4f1418aefd245f40fd125bc3cc12e73b4bd90fbedafdbdc5280fc2ce4591049e0fdb1bd208b83301b2ff60bc25459c9f10181e465a31d3b2c3
7
- data.tar.gz: '097d28aa7788ae0d33a41c8213de556ddf8a01f7ba07e965ae57093bcca134105cdb64546ffa34ab0d5ede6a59602c45a49ddbadb785eaab8371cb0dec552bcb'
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
@@ -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
@@ -5,7 +5,7 @@ require_relative "lib/open_ai_bot"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "open_ai_bot"
8
- spec.version = "0.2.2"
8
+ spec.version = "0.2.3"
9
9
  spec.authors = ["bulgakke"]
10
10
  spec.email = ["vvp835@yandex.ru"]
11
11
 
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.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-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