chatgpt_assistant 0.1.0

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.
@@ -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