message responder backend now works

This commit is contained in:
Sam Saffron 2024-04-24 16:18:23 +10:00
parent 4fcf12287f
commit cd7790f39a
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
5 changed files with 138 additions and 18 deletions

View File

@ -10,7 +10,13 @@ module ::Jobs
persona_id = args[:persona_id] persona_id = args[:persona_id]
begin 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? raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new) bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)

View File

@ -132,6 +132,7 @@ class AiPersona < ActiveRecord::Base
vision_max_pixels = self.vision_max_pixels vision_max_pixels = self.vision_max_pixels
rag_conversation_chunks = self.rag_conversation_chunks rag_conversation_chunks = self.rag_conversation_chunks
question_consolidator_llm = self.question_consolidator_llm question_consolidator_llm = self.question_consolidator_llm
role = self.role
role_whispers = self.role_whispers role_whispers = self.role_whispers
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id] persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
@ -184,6 +185,10 @@ class AiPersona < ActiveRecord::Base
role_whispers role_whispers
end end
persona_class.define_singleton_method :role do
role
end
return persona_class return persona_class
end end
@ -275,6 +280,10 @@ class AiPersona < ActiveRecord::Base
question_consolidator_llm question_consolidator_llm
end end
define_singleton_method :role do
role
end
define_singleton_method :role_whispers do define_singleton_method :role_whispers do
role_whispers role_whispers
end end

View File

@ -5,6 +5,23 @@ module DiscourseAi
module Personas module Personas
class Persona class Persona
class << self 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 def rag_conversation_chunks
10 10
end end

View File

@ -18,9 +18,30 @@ module DiscourseAi
user_id.to_i <= 0 user_id.to_i <= 0
end 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) def self.schedule_reply(post)
return if is_bot_user_id?(post.user_id) 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 bot_ids = DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS
mentionables = AiPersona.mentionables(user: post.user) mentionables = AiPersona.mentionables(user: post.user)
@ -126,11 +147,12 @@ module DiscourseAi
FROM upload_references ref FROM upload_references ref
WHERE ref.target_type = 'Post' AND ref.target_id = posts.id WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
) as upload_ids", ) as upload_ids",
"post_number",
) )
result = [] 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 = custom_prompt_translation =
Proc.new do |message| Proc.new do |message|
# We can't keep backwards-compatibility for stored functions. # We can't keep backwards-compatibility for stored functions.
@ -151,8 +173,14 @@ module DiscourseAi
if custom_prompt.present? if custom_prompt.present?
custom_prompt.each(&custom_prompt_translation) custom_prompt.each(&custom_prompt_translation)
else 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 = { context = {
content: raw, content: content,
type: (available_bot_usernames.include?(username) ? :model : :user), type: (available_bot_usernames.include?(username) ? :model : :user),
} }
@ -188,8 +216,11 @@ module DiscourseAi
reply = +"" reply = +""
start = Time.now start = Time.now
post_type = post_type = Post.types[:regular]
post.post_type == Post.types[:whisper] ? Post.types[:whisper] : Post.types[:regular]
if post.post_type == Post.types[:whisper] || bot.persona.class.role_whispers
post_type = Post.types[:whisper]
end
context = { context = {
site_url: Discourse.base_url, site_url: Discourse.base_url,
@ -208,7 +239,7 @@ module DiscourseAi
reply_user = User.find_by(id: bot.persona.class.user_id) || reply_user reply_user = User.find_by(id: bot.persona.class.user_id) || reply_user
end 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 # we need to ensure persona user is allowed to reply to the pm
if post.topic.private_message? if post.topic.private_message?
@ -309,6 +340,19 @@ module DiscourseAi
.concat(DiscourseAi::AiBot::EntryPoint::BOTS.map(&:second)) .concat(DiscourseAi::AiBot::EntryPoint::BOTS.map(&:second))
end 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 private
def publish_final_update(reply_post) def publish_final_update(reply_post)
@ -347,18 +391,6 @@ module DiscourseAi
end end
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) def context(topic)
{ {
site_url: Discourse.base_url, site_url: Discourse.base_url,

View File

@ -530,4 +530,60 @@ RSpec.describe DiscourseAi::AiBot::Playground do
end end
end 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 end