FEATURE: more accurate and faster titles (#791)

Previously we waited 1 minute before automatically titling PMs

The new change introduces adding a title immediately after the the
llm replies

Prompt was also modified to include the LLM reply in title suggestion.

This helps situation like:

user: tell me a joke
llm: a very funy joke about horses

Then the title would be "A Funny Horse Joke"

Specs already covered some auto title logic, amended to also
catch the new message bus message we have been sending.
This commit is contained in:
Sam 2024-09-03 15:52:20 +10:00 committed by GitHub
parent b0ae2138af
commit a48acc894a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 56 additions and 133 deletions

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class UpdateAiBotPmTitle < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
return unless bot_user = User.find_by(id: args[:bot_user_id])
return unless bot = DiscourseAi::AiBot::Bot.as(bot_user, model: args[:model])
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
return unless post.topic.custom_fields[DiscourseAi::AiBot::EntryPoint::REQUIRE_TITLE_UPDATE]
DiscourseAi::AiBot::Playground.new(bot).title_playground(post)
publish_update(post.topic, { title: post.topic.title })
end
def publish_update(topic, payload)
allowed_users = topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish("/discourse-ai/ai-bot/topic/#{topic.id}", payload, user_ids: allowed_users)
end
end
end

View File

@ -24,23 +24,41 @@ module DiscourseAi
def get_updated_title(conversation_context, post) def get_updated_title(conversation_context, post)
system_insts = <<~TEXT.strip system_insts = <<~TEXT.strip
You are titlebot. Given a topic, you will figure out a title. You are titlebot. Given a conversation, you will suggest a title.
You will never respond with anything but 7 word topic title.
- You will never respond with anything but the suggested title.
- You will always match the conversation language in your title suggestion.
- Title will capture the essence of the conversation.
TEXT
# conversation context may contain tool calls, and confusing user names
# clean it up
conversation = +""
conversation_context.each do |context|
if context[:type] == :user
conversation << "User said:\n#{context[:content]}\n\n"
elsif context[:type] == :model
conversation << "Model said:\n#{context[:content]}\n\n"
end
end
instruction = <<~TEXT.strip
Given the following conversation:
{{{
#{conversation}
}}}
Reply only with a title that is 7 words or less.
TEXT TEXT
title_prompt = title_prompt =
DiscourseAi::Completions::Prompt.new( DiscourseAi::Completions::Prompt.new(
system_insts, system_insts,
messages: conversation_context, messages: [type: :user, content: instruction],
topic_id: post.topic_id, topic_id: post.topic_id,
) )
title_prompt.push(
type: :user,
content:
"Based on our previous conversation, suggest a 7 word title without quoting any of it.",
)
DiscourseAi::Completions::Llm DiscourseAi::Completions::Llm
.proxy(model) .proxy(model)
.generate(title_prompt, user: post.user, feature_name: "bot_title") .generate(title_prompt, user: post.user, feature_name: "bot_title")

View File

@ -5,7 +5,6 @@ module DiscourseAi
USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)" USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
class EntryPoint class EntryPoint
REQUIRE_TITLE_UPDATE = "discourse-ai-title-update"
Bot = Struct.new(:id, :name, :llm) Bot = Struct.new(:id, :name, :llm)
def self.all_bot_ids def self.all_bot_ids

View File

@ -9,8 +9,6 @@ module DiscourseAi
# The bot will take care of completions while this class updates the topic title # The bot will take care of completions while this class updates the topic title
# and stream replies. # and stream replies.
REQUIRE_TITLE_UPDATE = "discourse-ai-title-update"
def self.find_chat_persona(message, channel, user) def self.find_chat_persona(message, channel, user)
if channel.direct_message_channel? if channel.direct_message_channel?
AiPersona.allowed_chat.find do |p| AiPersona.allowed_chat.find do |p|
@ -126,10 +124,7 @@ module DiscourseAi
end end
def update_playground_with(post) def update_playground_with(post)
if can_attach?(post) schedule_bot_reply(post) if can_attach?(post)
schedule_playground_titling(post)
schedule_bot_reply(post)
end
end end
def conversation_context(post) def conversation_context(post)
@ -217,9 +212,14 @@ module DiscourseAi
bot.bot_user, bot.bot_user,
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""), title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
) )
post.topic.custom_fields.delete(DiscourseAi::AiBot::EntryPoint::REQUIRE_TITLE_UPDATE)
post.topic.save_custom_fields
end end
allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish(
"/discourse-ai/ai-bot/topic/#{post.topic.id}",
{ title: post.topic.title },
user_ids: allowed_users,
)
end end
def chat_context(message, channel, persona_user, context_post_ids) def chat_context(message, channel, persona_user, context_post_ids)
@ -487,6 +487,9 @@ module DiscourseAi
reply_post reply_post
ensure ensure
publish_final_update(reply_post) if stream_reply publish_final_update(reply_post) if stream_reply
if reply_post && post.post_number == 1 && post.topic.private_message?
title_playground(reply_post)
end
end end
def available_bot_usernames def available_bot_usernames
@ -526,21 +529,6 @@ module DiscourseAi
true true
end end
def schedule_playground_titling(post)
if post.post_number == 1 && post.topic.private_message?
post.topic.custom_fields[REQUIRE_TITLE_UPDATE] = true
post.topic.save_custom_fields
::Jobs.enqueue_in(
1.minute,
:update_ai_bot_pm_title,
post_id: post.id,
bot_user_id: bot.bot_user.id,
model: bot.model,
)
end
end
def schedule_bot_reply(post) def schedule_bot_reply(post)
persona_id = persona_id =
DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] || DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] ||

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
RSpec.describe Jobs::UpdateAiBotPmTitle do
let(:user) { Fabricate(:admin) }
fab!(:claude_2) { Fabricate(:llm_model, name: "claude-2") }
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model("claude-2") }
before do
toggle_enabled_bots(bots: [claude_2])
SiteSetting.ai_bot_enabled = true
end
it "will properly update title on bot PMs" do
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:staff]
post =
create_post(
user: user,
raw: "Hello there",
title: "does not matter should be updated",
archetype: Archetype.private_message,
target_usernames: bot_user.username,
)
title_result = "A great title would be:\n\nMy amazing title\n\n"
DiscourseAi::Completions::Llm.with_prepared_responses([title_result]) do
subject.execute(bot_user_id: bot_user.id, post_id: post.id)
expect(post.reload.topic.title).to eq("My amazing title")
end
another_title = "I'm a different title"
DiscourseAi::Completions::Llm.with_prepared_responses([another_title]) do
subject.execute(bot_user_id: bot_user.id, post_id: post.id)
expect(post.reload.topic.title).to eq("My amazing title")
end
end
it "will post an update with the new title to the message bus channel" do
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:staff]
post =
create_post(
user: user,
raw: "Hello there",
title: "does not matter should be updated",
archetype: Archetype.private_message,
target_usernames: bot_user.username,
)
title_result = "A great title would be:\n\nMy amazing title\n\n"
DiscourseAi::Completions::Llm.with_prepared_responses([title_result]) do
messages =
MessageBus.track_publish("/discourse-ai/ai-bot/topic/#{post.topic.id}") do
subject.execute(bot_user_id: bot_user.id, post_id: post.id)
end
final_update = messages.last.data
expect(final_update[:title]).to eq("My amazing title")
end
end
end

View File

@ -517,7 +517,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
post = nil post = nil
DiscourseAi::Completions::Llm.with_prepared_responses( DiscourseAi::Completions::Llm.with_prepared_responses(
["Magic title", "Yes I can"], ["Yes I can", "Magic Title"],
llm: "custom:#{claude_2.id}", llm: "custom:#{claude_2.id}",
) do ) do
post = post =
@ -549,22 +549,29 @@ RSpec.describe DiscourseAi::AiBot::Playground do
post = nil post = nil
gpt3_5_bot_user = gpt_35_turbo.reload.user gpt3_5_bot_user = gpt_35_turbo.reload.user
messages = nil
# title is queued first, ensures it uses the llm targeted via target_usernames not claude
DiscourseAi::Completions::Llm.with_prepared_responses( DiscourseAi::Completions::Llm.with_prepared_responses(
["Magic title", "Yes I can"], ["Yes I can", "Magic Title"],
llm: "custom:#{gpt_35_turbo.id}", llm: "custom:#{gpt_35_turbo.id}",
) do ) do
post = messages =
create_post( MessageBus.track_publish do
title: "I just made a PM", post =
raw: "Hey @#{persona.user.username}, can you help me?", create_post(
target_usernames: "#{user.username},#{gpt3_5_bot_user.username}", title: "I just made a PM",
archetype: Archetype.private_message, raw: "Hey @#{persona.user.username}, can you help me?",
user: admin, target_usernames: "#{user.username},#{gpt3_5_bot_user.username}",
) archetype: Archetype.private_message,
user: admin,
)
end
end end
title_update_message =
messages.find { |m| m.channel == "/discourse-ai/ai-bot/topic/#{post.topic.id}" }
expect(title_update_message.data).to eq({ title: "Magic Title" })
last_post = post.topic.posts.order(:post_number).last last_post = post.topic.posts.order(:post_number).last
expect(last_post.raw).to eq("Yes I can") expect(last_post.raw).to eq("Yes I can")
expect(last_post.user_id).to eq(persona.user_id) expect(last_post.user_id).to eq(persona.user_id)