diff --git a/app/jobs/magellan_rag_query_job.rb b/app/jobs/magellan_rag_query_job.rb new file mode 100644 index 0000000..5223f80 --- /dev/null +++ b/app/jobs/magellan_rag_query_job.rb @@ -0,0 +1,71 @@ +class MagellanRagQueryJob < SlackResponseJob + queue_as :default + + VALID_MODELS = [ + # OpenAI API + "gpt-4o".freeze, + ] + + DEFAULT_MODEL = ENV.fetch("DEFAULT_MODEL", VALID_MODELS[0]) + + class InvalidOptionError < StandardError; end + + Options = Struct.new( + :model, + keyword_init: true + ) do + def validate! + validate_model! unless model.nil? + end + + def validate_model! + MagellanRagQueryJob::VALID_MODELS.each do |valid_model| + case valid_model + when Regexp + return if valid_model.match?(model) + else + return if model == valid_model + end + end + raise InvalidOptionError, "Invalid model is specified: #{model}" + end + end + + def model_for_message(message) + message.conversation.model || DEFAULT_MODEL + end + + def perform(params) + if params["message_id"].blank? + logger.warn "Empty message_id is given" + return + end + + message = Message.find(params["message_id"]) + if message.blank? + logger.warn "Unable to find Message with id=#{message_id}" + return + end + + if message.query.present? + logger.warn "Message with id=#{message.id} already has its query and response" + return + end + + options = Options.new(**params.fetch("options", {})) + + begin + start_response(message) + process_query(message, options) + ensure + finish_response(message) + end + rescue Exception => error + logger.error "ERROR: #{error.message}" + raise unless Rails.env.production? + end + + private def process_query(message, options) + # TODO: MAGELLAN RAG + end +end diff --git a/lib/slack_bot/app.rb b/lib/slack_bot/app.rb index d886765..65cd29c 100644 --- a/lib/slack_bot/app.rb +++ b/lib/slack_bot/app.rb @@ -27,6 +27,8 @@ class Application < Sinatra::Base end ALLOW_CHANNEL_IDS = ENV.fetch("ALLOW_CHANNEL_IDS", "").split(/\s+|,\s*/) + MAGELLAN_RAG_CHANNEL_IDS = ENV.fetch("MAGELLAN_RAG_CHANNEL_IDS", "").split(/\s+|,\s*/) + MAGELLAN_RAG_ENDPOINT = ENV.fetch("MAGELLAN_RAG_ENDPOINT", "localhost:12345") private def allowed_channel?(channel) if ALLOW_CHANNEL_IDS.empty? @@ -36,6 +38,14 @@ class Application < Sinatra::Base end end + private def magellan_rag_channel?(channel) + if MAGELLAN_RAG_CHANNEL_IDS.empty? + false + else + MAGELLAN_RAG_CHANNEL_IDS.include?(channel.slack_id) + end + end + private def thread_allowed_channel?(channel) true # channel.thread_allowed? end @@ -61,6 +71,14 @@ class Application < Sinatra::Base status 400 end + def mention?(event) + event["type"] == "app_mention" + end + + def direct_message?(event) + event["type"] == "message" && event["channel_type"] == "im" + end + post "/events" do request_data = JSON.parse(request.body.read) @@ -112,14 +130,23 @@ class Application < Sinatra::Base # "event_ts"=>"1679644228.326869"} # }}} - if event["type"] == "app_mention" || (event["type"] == "message" && event["channel_type"] == "im") + if mention?(event) || direct_message?(event) channel = ensure_conversation(event["channel"]) user = ensure_user(event["user"], channel) ts = event["ts"] thread_ts = event["thread_ts"] text = event["text"] - if allowed_channel?(channel) + case + when magellan_rag_channel?(channel) + logger.info "Event:\n" + event.pretty_inspect.each_line.map {|l| "> #{l}" }.join("") + logger.info "#{channel.slack_id}: #{text}" + if thread_ts and not thread_allowed_channel?(channel) + notify_do_not_allowed_thread_context(channel, user, ts) + else + process_magellan_rag_message(channel, user, ts, thread_ts, text) + end + when allowed_channel?(channel) logger.info "Event:\n" + event.pretty_inspect.each_line.map {|l| "> #{l}" }.join("") logger.info "#{channel.slack_id}: #{text}" @@ -322,6 +349,7 @@ class Application < Sinatra::Base return unless text =~ /^<@#{bot_id}>\s+/ message_body = Regexp.last_match.post_match + options = process_options(message_body) return if options.nil? @@ -376,6 +404,35 @@ class Application < Sinatra::Base options end + private def process_magellan_rag_message(channel, user, ts, thread_ts, text) + return unless text =~ /^<@#{bot_id}>\s+/ + + message_body = Regexp.last_match.post_match + options = process_magellan_rag_options(message_body) + return if options.nil? + + begin + options.validate! + rescue MagellanRagQeuryJob::InvalidOptionError => error + reply_as_ephemeral(channel, user, ts, error.message) + return + end + + message = Message.create!( + conversation: channel, + user: user, + text: message_body, + slack_ts: ts, + slack_thread_ts: thread_ts || ts + ) + MagellanRagQeuryJob.perform_later("message_id" => message.id, "options" => options.to_h) + end + + private def process_magellan_rag_options(message_body) + # TODO: implement options + MagellanRagQeuryJob::Options.new + end + private def check_command_permission!(channel, user) # TODO end