FEATURE: Add support for contextualizing a DM to a bot (#627)

This brings the context of the current topic on screen into chat
This commit is contained in:
Sam 2024-05-21 17:17:02 +10:00 committed by GitHub
parent 232f12eba6
commit d4116ecfac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 94 additions and 12 deletions

View File

@ -18,7 +18,11 @@ module ::Jobs
user = User.find_by(id: personaClass.user_id) user = User.find_by(id: personaClass.user_id)
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new) bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(message, channel) DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
message,
channel,
args[:context_post_ids],
)
end end
end end
end end

View File

@ -37,11 +37,15 @@ module DiscourseAi
persona = find_chat_persona(message, channel, user) persona = find_chat_persona(message, channel, user)
return if !persona return if !persona
post_ids = nil
post_ids = context.dig(:context, :post_ids) if context.is_a?(Hash)
::Jobs.enqueue( ::Jobs.enqueue(
:create_ai_chat_reply, :create_ai_chat_reply,
channel_id: channel.id, channel_id: channel.id,
message_id: message.id, message_id: message.id,
persona_id: persona[:id], persona_id: persona[:id],
context_post_ids: post_ids,
) )
end end
@ -218,7 +222,7 @@ module DiscourseAi
end end
end end
def chat_context(message, channel, persona_user) def chat_context(message, channel, persona_user, context_post_ids)
has_vision = bot.persona.class.vision_enabled has_vision = bot.persona.class.vision_enabled
include_thread_titles = !channel.direct_message_channel? && !message.thread_id include_thread_titles = !channel.direct_message_channel? && !message.thread_id
@ -260,6 +264,11 @@ module DiscourseAi
builder = DiscourseAi::Completions::PromptMessagesBuilder.new builder = DiscourseAi::Completions::PromptMessagesBuilder.new
guardian = Guardian.new(message.user)
if context_post_ids
builder.set_chat_context_posts(context_post_ids, guardian, include_uploads: has_vision)
end
messages.each do |m| messages.each do |m|
# restore stripped message # restore stripped message
m.message = instruction_message if m.id == current_id && instruction_message m.message = instruction_message if m.id == current_id && instruction_message
@ -284,18 +293,23 @@ module DiscourseAi
end end
end end
builder.to_a(limit: max_messages, style: channel.direct_message_channel? ? :default : :chat) builder.to_a(
limit: max_messages,
style: channel.direct_message_channel? ? :chat_with_context : :chat,
)
end end
def reply_to_chat_message(message, channel) def reply_to_chat_message(message, channel, context_post_ids)
persona_user = User.find(bot.persona.class.user_id) persona_user = User.find(bot.persona.class.user_id)
participants = channel.user_chat_channel_memberships.map { |m| m.user.username } participants = channel.user_chat_channel_memberships.map { |m| m.user.username }
context_post_ids = nil if !channel.direct_message_channel?
context = context =
get_context( get_context(
participants: participants.join(", "), participants: participants.join(", "),
conversation_context: chat_context(message, channel, persona_user), conversation_context: chat_context(message, channel, persona_user, context_post_ids),
user: message.user, user: message.user,
skip_tool_details: true, skip_tool_details: true,
) )

View File

@ -4,11 +4,41 @@ module DiscourseAi
module Completions module Completions
class PromptMessagesBuilder class PromptMessagesBuilder
MAX_CHAT_UPLOADS = 5 MAX_CHAT_UPLOADS = 5
attr_reader :chat_context_posts
attr_reader :chat_context_post_upload_ids
def initialize def initialize
@raw_messages = [] @raw_messages = []
end end
def set_chat_context_posts(post_ids, guardian, include_uploads:)
posts = []
Post
.where(id: post_ids)
.order("id asc")
.each do |post|
next if !guardian.can_see?(post)
posts << post
end
if posts.present?
posts_context =
+"\nThis chat is in the context of the Discourse topic '#{posts[0].topic.title}':\n\n"
posts_context = +"{{{\n"
posts.each do |post|
posts_context << "url: #{post.url}\n"
posts_context << "#{post.username}: #{post.raw}\n\n"
end
posts_context << "}}}"
@chat_context_posts = posts_context
if include_uploads
uploads = []
posts.each { |post| uploads.concat(post.uploads.pluck(:id)) }
uploads.uniq!
@chat_context_post_upload_ids = uploads.take(MAX_CHAT_UPLOADS)
end
end
end
def to_a(limit: nil, style: nil) def to_a(limit: nil, style: nil)
return chat_array(limit: limit) if style == :chat return chat_array(limit: limit) if style == :chat
result = [] result = []
@ -51,6 +81,20 @@ module DiscourseAi
last_type = message[:type] last_type = message[:type]
end end
if style == :chat_with_context && @chat_context_posts
buffer = +"You are replying inside a Discourse chat."
buffer << "\n"
buffer << @chat_context_posts
buffer << "\n"
buffer << "Your instructions are:\n"
result[0][:content] = "#{buffer}#{result[0][:content]}"
if @chat_context_post_upload_ids.present?
result[0][:upload_ids] = (result[0][:upload_ids] || []).concat(
@chat_context_post_upload_ids,
)
end
end
if limit if limit
result[0..limit] result[0..limit]
else else
@ -75,13 +119,9 @@ module DiscourseAi
private private
def chat_array(limit:) def chat_array(limit:)
buffer = +""
if @raw_messages.length > 1 if @raw_messages.length > 1
buffer << (<<~TEXT).strip buffer =
You are replying inside a Discourse chat. Here is a summary of the conversation so far: +"You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:\n{{{"
{{{
TEXT
upload_ids = [] upload_ids = []

View File

@ -204,7 +204,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
content = prompt.messages[1][:content] content = prompt.messages[1][:content]
# this is fragile by design, mainly so the example can be ultra clear # this is fragile by design, mainly so the example can be ultra clear
expected = (<<~TEXT).strip expected = (<<~TEXT).strip
You are replying inside a Discourse chat. Here is a summary of the conversation so far: You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:
{{{ {{{
#{user.username}: (a magic thread) #{user.username}: (a magic thread)
thread 1 message 1 thread 1 message 1
@ -265,6 +265,30 @@ RSpec.describe DiscourseAi::AiBot::Playground do
let(:guardian) { Guardian.new(user) } let(:guardian) { Guardian.new(user) }
it "can supply context" do
post = Fabricate(:post, raw: "this is post content")
prompts = nil
message =
DiscourseAi::Completions::Llm.with_prepared_responses(["World"]) do |_, _, _prompts|
prompts = _prompts
::Chat::CreateMessage.call!(
chat_channel_id: dm_channel.id,
message: "Hello",
guardian: guardian,
context_post_ids: [post.id],
).message_instance
end
expect(prompts[0].messages[1][:content]).to include("this is post content")
message.reload
reply = ChatSDK::Thread.messages(thread_id: message.thread_id, guardian: guardian).last
expect(reply.message).to eq("World")
expect(message.thread_id).to be_present
end
it "can run tools" do it "can run tools" do
persona.update!(commands: ["TimeCommand"]) persona.update!(commands: ["TimeCommand"])