From cd7790f39a304ed6306174b1f2fbc60385fe42ec Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 24 Apr 2024 16:18:23 +1000 Subject: [PATCH] message responder backend now works --- app/jobs/regular/create_ai_reply.rb | 8 ++- app/models/ai_persona.rb | 9 +++ lib/ai_bot/personas/persona.rb | 17 ++++++ lib/ai_bot/playground.rb | 66 ++++++++++++++++------ spec/lib/modules/ai_bot/playground_spec.rb | 56 ++++++++++++++++++ 5 files changed, 138 insertions(+), 18 deletions(-) diff --git a/app/jobs/regular/create_ai_reply.rb b/app/jobs/regular/create_ai_reply.rb index 16f24b2e..e2d5ada4 100644 --- a/app/jobs/regular/create_ai_reply.rb +++ b/app/jobs/regular/create_ai_reply.rb @@ -10,7 +10,13 @@ module ::Jobs persona_id = args[:persona_id] begin - persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id) + persona = + if args[:skip_persona_security_check] + persona = AiPersona.all_personas.find { |persona| persona.id == persona_id } + else + persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id) + end + raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil? bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new) diff --git a/app/models/ai_persona.rb b/app/models/ai_persona.rb index c2e97313..6383c599 100644 --- a/app/models/ai_persona.rb +++ b/app/models/ai_persona.rb @@ -132,6 +132,7 @@ class AiPersona < ActiveRecord::Base vision_max_pixels = self.vision_max_pixels rag_conversation_chunks = self.rag_conversation_chunks question_consolidator_llm = self.question_consolidator_llm + role = self.role role_whispers = self.role_whispers persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id] @@ -184,6 +185,10 @@ class AiPersona < ActiveRecord::Base role_whispers end + persona_class.define_singleton_method :role do + role + end + return persona_class end @@ -275,6 +280,10 @@ class AiPersona < ActiveRecord::Base question_consolidator_llm end + define_singleton_method :role do + role + end + define_singleton_method :role_whispers do role_whispers end diff --git a/lib/ai_bot/personas/persona.rb b/lib/ai_bot/personas/persona.rb index 52c67132..37c41b7f 100644 --- a/lib/ai_bot/personas/persona.rb +++ b/lib/ai_bot/personas/persona.rb @@ -5,6 +5,23 @@ module DiscourseAi module Personas class Persona class << self + def as_bot + if self.respond_to?(:user_id) && self.respond_to?(:default_llm) + if self.default_llm + user = User.find_by(id: user_id) + DiscourseAi::AiBot::Bot.new(user, self.new, self.default_llm) if user + end + end + end + + def role + "bot" + end + + def role_whispers + false + end + def rag_conversation_chunks 10 end diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb index d84bd82f..68ae27ae 100644 --- a/lib/ai_bot/playground.rb +++ b/lib/ai_bot/playground.rb @@ -18,9 +18,30 @@ module DiscourseAi user_id.to_i <= 0 end + def self.find_responder_persona(post) + if post.post_number == 1 && post.topic && post.topic.archetype == Archetype.private_message + # only supported responder for PMs is based on role_group_ids + group_ids = post.topic.allowed_groups.pluck(:id) + + info = + group_ids + .lazy + .map { |group_id| AiPersona.message_responder_for(group_id: group_id) } + .find { |found| !found.nil? } + + AiPersona.all_personas.find { |persona| persona.id == info[:id] } if info && info[:id] + end + end + def self.schedule_reply(post) return if is_bot_user_id?(post.user_id) + if responder_persona_class = find_responder_persona(post) + bot = responder_persona_class.as_bot + new(bot).schedule_bot_reply(post, skip_persona_security_check: true) if bot + return + end + bot_ids = DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS mentionables = AiPersona.mentionables(user: post.user) @@ -126,11 +147,12 @@ module DiscourseAi FROM upload_references ref WHERE ref.target_type = 'Post' AND ref.target_id = posts.id ) as upload_ids", + "post_number", ) result = [] - context.reverse_each do |raw, username, custom_prompt, upload_ids| + context.reverse_each do |raw, username, custom_prompt, upload_ids, post_number| custom_prompt_translation = Proc.new do |message| # We can't keep backwards-compatibility for stored functions. @@ -151,8 +173,14 @@ module DiscourseAi if custom_prompt.present? custom_prompt.each(&custom_prompt_translation) else + content = raw + if post_number == 1 && bot.persona.class.role.include?("responder") + title = Topic.where("id = ?", post.topic_id).pluck(:title).first + content = "# #{title}\n\n#{content}" + end + context = { - content: raw, + content: content, type: (available_bot_usernames.include?(username) ? :model : :user), } @@ -188,8 +216,11 @@ module DiscourseAi reply = +"" start = Time.now - post_type = - post.post_type == Post.types[:whisper] ? Post.types[:whisper] : Post.types[:regular] + post_type = Post.types[:regular] + + if post.post_type == Post.types[:whisper] || bot.persona.class.role_whispers + post_type = Post.types[:whisper] + end context = { site_url: Discourse.base_url, @@ -208,7 +239,7 @@ module DiscourseAi reply_user = User.find_by(id: bot.persona.class.user_id) || reply_user end - stream_reply = post.topic.private_message? + stream_reply = post.topic.private_message? && !bot.persona.role.include?("responder") # we need to ensure persona user is allowed to reply to the pm if post.topic.private_message? @@ -309,6 +340,19 @@ module DiscourseAi .concat(DiscourseAi::AiBot::EntryPoint::BOTS.map(&:second)) end + def schedule_bot_reply(post, skip_persona_security_check: false) + persona_id = + DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] || + bot.persona.class.id + ::Jobs.enqueue( + :create_ai_reply, + post_id: post.id, + bot_user_id: bot.bot_user.id, + persona_id: persona_id, + skip_persona_security_check: skip_persona_security_check, + ) + end + private def publish_final_update(reply_post) @@ -347,18 +391,6 @@ module DiscourseAi end end - def schedule_bot_reply(post) - persona_id = - DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] || - bot.persona.class.id - ::Jobs.enqueue( - :create_ai_reply, - post_id: post.id, - bot_user_id: bot.bot_user.id, - persona_id: persona_id, - ) - end - def context(topic) { site_url: Discourse.base_url, diff --git a/spec/lib/modules/ai_bot/playground_spec.rb b/spec/lib/modules/ai_bot/playground_spec.rb index 29ffc968..68c0544e 100644 --- a/spec/lib/modules/ai_bot/playground_spec.rb +++ b/spec/lib/modules/ai_bot/playground_spec.rb @@ -530,4 +530,60 @@ RSpec.describe DiscourseAi::AiBot::Playground do end end end + + describe "message_responder role" do + fab!(:group) do + Fabricate(:group, name: "test-group", messageable_level: Group::ALIAS_LEVELS[:everyone]) + end + + it "can reply with a whisper to group PM" do + Jobs.run_immediately! + + persona = + AiPersona.create!( + name: "Responder Persona", + description: "A responder", + system_prompt: "You are a responder", + enabled: true, + role: "message_responder", + role_group_ids: [group.id], + role_whispers: true, + default_llm: "anthropic:claude-2", + ) + + persona.create_user! + + post = nil + + prompts = nil + DiscourseAi::Completions::Llm.with_prepared_responses( + ["An amazing post just for you"], + ) do |_, _, _prompts| + post = + create_post( + title: "an amazing title", + raw: "Howdy I need help!!", + archetype: Archetype.private_message, + target_group_names: [group.name], + ) + prompts = _prompts + end + + expect(prompts.length).to eq(1) + expect(prompts[0].messages[0][:content]).to eq("You are a responder") + expect(prompts[0].messages[1][:content]).to eq("# An amazing title\n\nHowdy I need help!!") + + reply = post.topic.posts.find_by(post_number: 2) + + expect(reply.post_type).to eq(Post.types[:whisper]) + expect(reply.raw).to eq("An amazing post just for you") + + # should be done responding at this point so llm will not be called + create_post( + raw: "should ignore posts that are not first on message", + topic: post.topic, + user: post.user, + ) + end + end end