chatgpt_assistant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatgptAssistant
4
+ # This class is responsible for the telegram bot features
5
+ class TelegramBot < ApplicationBot
6
+ def start
7
+ start_log
8
+ bot.listen do |message|
9
+ @msg = message
10
+ next unless text_or_audio?
11
+
12
+ message_received_log
13
+ message_text_cases if msg.text.present?
14
+ message_audio_process if msg.audio.present? || msg.voice.present?
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_accessor :msg
21
+
22
+ def message_text_cases
23
+ logger.log("MESSAGE TEXT: TRUE")
24
+ case msg.text
25
+ when "/start"
26
+ run_start
27
+ when "/help"
28
+ run_help
29
+ when "/list"
30
+ list_chats
31
+ when "/stop"
32
+ run_stop
33
+ when "/hist"
34
+ run_hist
35
+ when nil
36
+ run_nil_error
37
+ else
38
+ operations
39
+ end
40
+ end
41
+
42
+ def message_audio_process
43
+ logger.log("MESSAGE AUDIO: TRUE")
44
+ return bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:no_chat_selected]) if user.current_chat_id.nil? || user.current_chat_id.zero?
45
+ chat = Chat.find(user.current_chat_id)
46
+ message_create(transcribed_text, chat.id, "user")
47
+ text = chatter.chat(transcribed_text, chat.id)
48
+ voice = audio_synthesis.synthesize_text(text)
49
+ bot.api.send_message(chat_id: msg.chat.id, text: text)
50
+ bot.api.send_voice(chat_id: msg.chat.id, voice: Faraday::UploadIO.new(voice, "audio/mp3"))
51
+ delete_all_voice_files
52
+ end
53
+
54
+ def operations
55
+ return chatter_call unless actions?
56
+ return new_chat if msg.text.include?("new_chat/")
57
+ return select_chat if msg.text.include?("sl_chat/")
58
+ return login if msg.text.include?("login/")
59
+ return register if msg.text.include?("register/")
60
+ invalid_command_error_message
61
+ end
62
+
63
+ def login
64
+ user_info = msg.text.split("/").last
65
+ email, password = user_info.split(":")
66
+ case auth_usertelegram(email, password, msg.chat.id)
67
+ when "user not found"
68
+ user_not_found_error_message
69
+ when "wrong password"
70
+ wrong_password_error_message
71
+ when find_usertelegram(email)
72
+ user_logged_in_message
73
+ end
74
+ end
75
+
76
+ def register
77
+ user_info = msg.text.split("/").last
78
+ email, password = user_info.split(":")
79
+ if telegram_user_create(msg.chat.id, email, password, msg.from.username || msg.from.first_name)
80
+ user_created_message
81
+ else
82
+ user_creation_error_message
83
+ end
84
+ end
85
+
86
+ def audio
87
+ msg.audio || msg.voice
88
+ end
89
+
90
+ def audio_info
91
+ bot.api.get_file(file_id: audio.file_id)
92
+ end
93
+
94
+ def audio_url
95
+ "https://api.telegram.org/file/bot#{telegram_token}/#{audio_info["result"]["file_path"]}"
96
+ end
97
+
98
+ def transcribed_text
99
+ audio_recognition.transcribe_audio(audio_url)
100
+ end
101
+
102
+ def run_start
103
+ bot.api.send_message(chat_id: msg.chat.id, text: commom_messages[:start])
104
+ help_message = help_messages.join("\n").to_s
105
+ bot.api.send_message(chat_id: msg.chat.id, text: help_message)
106
+ bot.api.send_message(chat_id: msg.chat.id, text: commom_messages[:start_helper])
107
+ bot.api.send_message(chat_id: msg.chat.id, text: commom_messages[:start_sec_helper])
108
+ end
109
+
110
+ def run_hist
111
+ return not_logged_in_message unless user
112
+
113
+ chat = Chat.find(user.current_chat_id)
114
+ if chat
115
+ if chat.messages.count.zero?
116
+ return bot.api.send_message(chat_id: msg.chat.id,
117
+ text: error_messages[:no_messages_founded])
118
+ end
119
+
120
+ response = chat.messages.last(4).map do |mess|
121
+ "#{mess.role}: #{mess.content}\n at: #{mess.created_at}\n\n"
122
+ end.join
123
+ logger.log("HIST RESPONSE: #{response}")
124
+ bot.api.send_message(chat_id: msg.chat.id, text: response)
125
+ else
126
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:no_chat_selected])
127
+ end
128
+ end
129
+
130
+ def select_chat
131
+ if user.nil?
132
+ not_logged_in_message
133
+ else
134
+ chat = Chat.find_by(user: user, title: msg.text.split("/").last)
135
+ if chat
136
+ user.update(current_chat_id: chat.id)
137
+ bot.api.send_message(chat_id: msg.chat.id, text: success_messages[:chat_selected])
138
+ else
139
+ chat_not_found_message
140
+ end
141
+ end
142
+ end
143
+
144
+ def new_chat
145
+ user.nil? ? not_logged_in_message : create_chat
146
+ end
147
+
148
+ def create_chat
149
+ text = msg.text
150
+ title = text.split("/").last
151
+ chat = Chat.new(user_id: user.id, status: 0, title: title)
152
+ chat.save ? chat_created_message(chat) : chat_creation_failed_message
153
+ end
154
+
155
+ def list_chats
156
+ chats = Chat.where(user_id: User.find_by(telegram_id: msg.chat.id).id)
157
+ return bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:no_chats_founded]) if chats.count.zero?
158
+ bot.api.send_message(chat_id: msg.chat.id,
159
+ text: commom_messages[:chats_list])
160
+ chats_str = ""
161
+ chats.each do |chat|
162
+ chats_str += "Chat #{chat.id} - #{chat.title}\n"
163
+ end
164
+ bot.api.send_message(chat_id: msg.chat.id, text: chats_str)
165
+ end
166
+
167
+ def chatter_call
168
+ user ? chat_if_exists : not_logged_in_message
169
+ end
170
+
171
+ def chat_if_exists
172
+ chat = Chat.find_by(user_id: user.id, id: user.current_chat_id)
173
+ chat ? chat_success(chat.id) : no_chat_selected
174
+ end
175
+
176
+ def chat_success(chat_id)
177
+ Message.create(chat_id: chat_id, content: msg.text, role: "user")
178
+ bot.api.send_message(chat_id: msg.chat.id, text: chatter.chat(msg.text, chat_id))
179
+ end
180
+
181
+ def run_help
182
+ help_messages.each do |message|
183
+ bot.api.send_message(chat_id: msg.chat.id, text: message)
184
+ end
185
+ end
186
+
187
+ def run_stop
188
+ bot.api.send_message(chat_id: msg.chat.id, text: commom_messages[:stop])
189
+ bot.api.leave_chat(chat_id: msg.chat.id)
190
+ end
191
+
192
+ def run_nil_error
193
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:nil])
194
+ end
195
+
196
+ def user_logged_message
197
+ user.update(telegram_id: msg.chat.id)
198
+ bot.api.send_message(chat_id: msg.chat.id, text: success_messages[:user_logged_in])
199
+ end
200
+
201
+ def invalid_command_error_message
202
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:invalid_command])
203
+ end
204
+
205
+ def wrong_password_error_message
206
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:password])
207
+ end
208
+
209
+ def user_not_found_error_message
210
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:user_not_found])
211
+ end
212
+
213
+ def not_logged_in_message
214
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:user_not_logged_in])
215
+ end
216
+
217
+ def user_created_message
218
+ bot.api.send_message(chat_id: msg.chat.id, text: success_messages[:user_created])
219
+ end
220
+
221
+ def user_creation_error_message
222
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:user_creation])
223
+ end
224
+
225
+ def chat_created_message(chat)
226
+ user.update(current_chat_id: chat.id)
227
+ bot.api.send_message(chat_id: msg.chat.id, text: success_messages[:chat_created])
228
+ end
229
+
230
+ def chat_creation_failed_message
231
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:chat_creation_failed])
232
+ end
233
+
234
+ def no_chat_selected
235
+ bot.api.send_message(chat_id: msg.chat.id,
236
+ text: error_messages[:no_chat_selected])
237
+ end
238
+
239
+ def chat_not_found_message
240
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:chat_not_found])
241
+ end
242
+
243
+ def start_log
244
+ logger.log("STARTING BOT AT #{Time.now}")
245
+ logger.log("ENVIRONMENT: #{@config.env_type}")
246
+ end
247
+
248
+ def error_log(err)
249
+ if err.message.to_s.include?("Bad Request: message is too long")
250
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:message_history_too_long])
251
+ else
252
+ bot.api.send_message(chat_id: msg.chat.id, text: error_messages[:something_went_wrong])
253
+ bot.api.send_message(chat_id: msg.chat.id, text: "ERROR: #{err.message}\n #{err.backtrace}")
254
+ end
255
+ logger.log("ERROR: #{err.message}\n #{err.backtrace}")
256
+ end
257
+
258
+ def message_received_log
259
+ logger.log("MESSAGE RECEIVED AT: #{Time.now}")
260
+ logger.log("MESSAGE FROM USER: #{msg.from.first_name} #{msg.from.last_name} - #{msg.from.username}")
261
+ end
262
+
263
+ def user
264
+ @user ||= User.find_by(telegram_id: msg.chat.id)
265
+ end
266
+
267
+ def bot
268
+ @bot ||= Telegram::Bot::Client.new(telegram_token)
269
+ end
270
+
271
+ def text_or_audio?
272
+ msg.respond_to?(:text) || msg.respond_to?(:audio) || msg.respond_to?(:voice)
273
+ end
274
+
275
+ def actions?
276
+ msg.text.include?("new_chat/") || msg.text.include?("sl_chat/") || msg.text.include?("login/") || msg.text.include?("register/")
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatgptAssistant
4
+ # This is the AudioRecognition class
5
+ class AudioRecognition
6
+ def initialize(openai_api_key)
7
+ @conn = Faraday.new(url: "https://api.openai.com/") do |faraday|
8
+ faraday.request :multipart
9
+ faraday.request :url_encoded
10
+ faraday.adapter Faraday.default_adapter
11
+ end
12
+ @logger = ChatterLogger.new
13
+ @openai_api_key = openai_api_key
14
+ end
15
+
16
+ def download_audio(audio_url)
17
+ logger.log("DOWNLOADING AUDIO FROM TELEGRAM")
18
+ @time = Time.now.to_i
19
+ audio_conn = Faraday.new(url: audio_url)
20
+ File.open("voice/audio-#{time}.oga", "wb") do |file|
21
+ file.write(audio_conn.get.body)
22
+ end
23
+
24
+ FFMPEG::Movie.new("voice/audio-#{time}.oga").transcode("voice/audio-#{time}.mp3")
25
+ File.delete("voice/audio-#{time}.oga")
26
+ end
27
+
28
+ def header
29
+ {
30
+ "Content-Type": "multipart/form-data",
31
+ "Authorization": "Bearer #{openai_api_key}"
32
+ }
33
+ end
34
+
35
+ def payload
36
+ {
37
+ "file": Faraday::UploadIO.new("voice/audio-#{time}.mp3", "audio/mp3"),
38
+ "model": "whisper-1"
39
+ }
40
+ end
41
+
42
+ def transcribe_audio(audio_url)
43
+ @audio_url = audio_url
44
+ download_audio(audio_url)
45
+ response = conn.post("v1/audio/transcriptions", payload, header)
46
+ logger.log("RESPONSE FROM OPENAI API AUDIO TRANSCRIPTION")
47
+ JSON.parse(response.body)["text"]
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :conn, :openai_api_key, :logger, :audio_url, :time, :ibm_api_key, :ibm_url
53
+ end
54
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatgptAssistant
4
+ # This class is responsible for synthesize text to speech
5
+ # This class can work with IBM Cloud or AWS Polly for synthesize text into speech
6
+ class AudioSynthesis
7
+ def initialize(config)
8
+ @config = config
9
+ @openai_api_key = config.openai_api_key
10
+ @language = config.language
11
+ @conn = faraday_instance
12
+ @logger = ChatterLogger.new
13
+ classify_mode
14
+ end
15
+
16
+ def synthesize_text(text)
17
+ if ibm_mode?
18
+ synthesize_text_ibm(text)
19
+ elsif aws_mode?
20
+ synthesize_text_aws(text)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :openai_api_key, :ibm_api_key, :ibm_url, :aws_access_key_id, :aws_secret_access_key, :aws_region,
27
+ :config, :logger, :language, :voice
28
+
29
+ def faraday_instance
30
+ Faraday.new(url: "https://api.openai.com/") do |faraday|
31
+ faraday.request :multipart
32
+ faraday.request :url_encoded
33
+ faraday.adapter Faraday.default_adapter
34
+ end
35
+ end
36
+
37
+ def classify_mode
38
+ if ibm_mode?
39
+ @ibm_api_key = config.ibm_api_key
40
+ @ibm_url = config.ibm_url
41
+ @voice = send("#{language}_ibm_voice")
42
+ elsif aws_mode?
43
+ @aws_access_key_id = config.aws_access_key_id
44
+ @aws_secret_access_key = config.aws_secret_access_key
45
+ @aws_region = config.aws_region
46
+ @voice = send("#{language}_aws_voice")
47
+ end
48
+ end
49
+
50
+ def synthesize_text_aws(text)
51
+ time = Time.now.to_i
52
+ logger.log("SYNTHESIZING TEXT WITH AWS POLLY")
53
+ @time = Time.now.to_i
54
+ polly_client = Aws::Polly::Client.new(
55
+ access_key_id: aws_access_key_id,
56
+ secret_access_key: aws_secret_access_key,
57
+ region: aws_region
58
+ )
59
+ response = polly_client.synthesize_speech(
60
+ output_format: "mp3",
61
+ text: text,
62
+ voice_id: voice,
63
+ engine: "neural"
64
+ )
65
+
66
+ File.open("voice/aws-#{time}.mp3", "wb") do |file|
67
+ file.write(response.audio_stream.read)
68
+ end
69
+
70
+ logger.log("SYNTHESIZED TEXT WITH AWS POLLY")
71
+ "voice/aws-#{time}.mp3"
72
+ end
73
+
74
+ def synthesize_text_ibm(text)
75
+ time = Time.now.to_i
76
+ logger.log("SYNTHESIZING TEXT WITH IBM WATSON")
77
+ authenticator = IBMWatson::Authenticators::IamAuthenticator.new(
78
+ apikey: ibm_api_key
79
+ )
80
+
81
+ text_to_speech = IBMWatson::TextToSpeechV1.new(
82
+ authenticator: authenticator
83
+ )
84
+
85
+ text_to_speech.service_url = ibm_url
86
+
87
+ audio_format = "audio/mp3"
88
+ audio = text_to_speech.synthesize(
89
+ text: text,
90
+ accept: audio_format,
91
+ voice: voice
92
+ ).result
93
+
94
+ File.open("voice/ibm-#{time}.mp3", "wb") do |audio_file|
95
+ audio_file.write(audio)
96
+ end
97
+ "voice/ibm-#{time}.mp3"
98
+ end
99
+
100
+ def pt_aws_voice
101
+ "Vitoria"
102
+ end
103
+
104
+ def en_aws_voice
105
+ "Joanna"
106
+ end
107
+
108
+ def pt_ibm_voice
109
+ "pt-BR_IsabelaV3Voice"
110
+ end
111
+
112
+ def en_ibm_voice
113
+ "en-US_AllisonV3Voice"
114
+ end
115
+
116
+ def ibm_mode?
117
+ config.mode == "ibm"
118
+ end
119
+
120
+ def aws_mode?
121
+ config.mode == "aws"
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require_relative "models"
5
+ require_relative "chatter_logger"
6
+
7
+ module ChatgptAssistant
8
+ # This is the Chat Ai class
9
+ class Chatter
10
+ def initialize(openai_api_key)
11
+ @openai_api_key = openai_api_key
12
+ @logger = ChatterLogger.new
13
+ end
14
+
15
+ def chat(message, chat_id)
16
+ @chat_id = chat_id
17
+ @message = message
18
+ init_log
19
+ @response = request(message)
20
+ @json = JSON.parse(response.body)
21
+ logger.log("RESPONSE FROM OPENAI API: OK")
22
+
23
+ return error_log if response.status != 200
24
+
25
+ text = json["choices"][0]["message"]["content"]
26
+
27
+ Message.create(content: text, role: "assistant", chat_id: chat_id)
28
+ logger.log("MESSAGE SAVED IN DATABASE")
29
+ text
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :openai_api_key, :response, :message, :logger
35
+ attr_accessor :chat_id, :json
36
+
37
+ def init_log
38
+ logger.log("REQUESTING OPENAI API COMPLETION")
39
+ end
40
+
41
+ def error_log
42
+ logger.log("RESPONSE FROM OPENAI API ERROR")
43
+ logger.log("RESPONSE STATUS: #{response.status}")
44
+ "Algo deu errado, tente novamente mais tarde."
45
+ end
46
+
47
+ def header
48
+ {
49
+ "Content-Type": "application/json",
50
+ Authorization: "Bearer #{openai_api_key}"
51
+ }
52
+ end
53
+
54
+ def connection
55
+ Faraday.new(url: "https://api.openai.com/") do |faraday|
56
+ faraday.request :url_encoded
57
+ faraday.adapter Faraday.default_adapter
58
+ end
59
+ end
60
+
61
+ def request_params(message)
62
+ messages = Message.where(chat_id: chat_id).order(id: :asc).last(10)
63
+ if messages.empty?
64
+ ids = ["unknown"]
65
+ messages = [{ role: "user", content: message }]
66
+ else
67
+ ids = messages.map(&:id)
68
+ messages = messages.map { |mess| { role: mess.role, content: mess.content } }
69
+ end
70
+ logger.log("MESSAGES LOADED IN CONTEXT: #{messages.count}")
71
+ messages.each_with_index do |mess, index|
72
+ logger.log("MESSAGE ROLE: #{mess[:role]}, ID: #{ids[index]}")
73
+ end
74
+ {
75
+ model: "gpt-3.5-turbo",
76
+ messages: messages
77
+ }.to_json
78
+ end
79
+
80
+ def request(message)
81
+ connection.post("v1/chat/completions", request_params(message), header)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatgptAssistant
4
+ # This class is responsible for logging the messages
5
+ class ChatterLogger
6
+ def initialize
7
+ @file_name = "logs/telegram_chatgpt.log"
8
+ @log_file = File.open("logs/telegram_chatgpt.log", "a")
9
+ end
10
+
11
+ attr_reader :log_file
12
+
13
+ def log(message)
14
+ log_file.puts(message)
15
+ system "echo '#{message}'"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_model"
5
+ require_relative "migrations"
6
+ require "fileutils"
7
+
8
+ module ChatgptAssistant
9
+ # This class is responsible for the configuration of the Chatgpt Assistant
10
+ class Config
11
+ def initialize
12
+ @env_type = ENV["ENV_TYPE"]
13
+ @language = ENV["LANGUAGE"]
14
+ @mode = ENV["MODE"]
15
+ @database_host = ENV["POSTGRES_HOST"]
16
+ @database_name = ENV["POSTGRES_DB"]
17
+ @database_username = ENV["POSTGRES_USER"]
18
+ @database_password = ENV["POSTGRES_PASSWORD"]
19
+ @openai_api_key = ENV["OPENAI_API_KEY"]
20
+ @telegram_token = ENV["TELEGRAM_TOKEN"]
21
+ @discord_token = ENV["DISCORD_TOKEN"]
22
+ @discord_client_id = ENV["DISCORD_CLIENT_ID"]
23
+ @ibm_api_key = ENV["IBM_API_KEY"]
24
+ @ibm_url = ENV["IBM_URL"]
25
+ @aws_access_key_id = ENV["AWS_ACCESS_KEY_ID"]
26
+ @aws_secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
27
+ @aws_region = ENV["AWS_REGION"]
28
+ @discord_prefix = ENV["DISCORD_PREFIX"]
29
+ end
30
+
31
+ attr_reader :openai_api_key, :telegram_token, :discord_token, :ibm_api_key, :ibm_url,
32
+ :aws_access_key_id, :aws_secret_access_key, :aws_region, :mode, :language,
33
+ :discord_client_id, :discord_public_key, :env_type, :discord_prefix
34
+
35
+ def db_connection
36
+ ActiveRecord::Base.establish_connection(
37
+ adapter: "postgresql",
38
+ host: database_host,
39
+ port: 5432,
40
+ database: database_name,
41
+ username: database_username,
42
+ password: database_password
43
+ )
44
+ ActiveRecord::Base.logger = Logger.new($stdout) if ENV["ENV_TYPE"] == "development"
45
+ end
46
+
47
+ def migrate
48
+ db_connection
49
+ ActiveRecord::Base.logger = Logger.new($stdout)
50
+ UserMigration.new.migrate(:up)
51
+ ChatMigration.new.migrate(:up)
52
+ MessageMigration.new.migrate(:up)
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :database_host, :database_name, :database_username, :database_password
58
+ end
59
+ end