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:
parent
232f12eba6
commit
d4116ecfac
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue