2024-01-04 08:44:07 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module DiscourseAi
|
|
|
|
module AiBot
|
|
|
|
class Playground
|
2024-10-30 05:24:39 -04:00
|
|
|
BYPASS_AI_REPLY_CUSTOM_FIELD = "discourse_ai_bypass_ai_reply"
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
attr_reader :bot
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
# An abstraction to manage the bot and topic interactions.
|
|
|
|
# The bot will take care of completions while this class updates the topic title
|
|
|
|
# and stream replies.
|
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
def self.find_chat_persona(message, channel, user)
|
2024-05-05 19:49:02 -04:00
|
|
|
if channel.direct_message_channel?
|
2024-10-15 16:20:31 -04:00
|
|
|
AiPersona
|
|
|
|
.allowed_modalities(allow_chat_direct_messages: true)
|
|
|
|
.find do |p|
|
|
|
|
p[:user_id].in?(channel.allowed_user_ids) && (user.group_ids & p[:allowed_group_ids])
|
|
|
|
end
|
2024-05-06 20:30:39 -04:00
|
|
|
else
|
|
|
|
# let's defer on the parse if there is no @ in the message
|
|
|
|
if message.message.include?("@")
|
|
|
|
mentions = message.parsed_mentions.parsed_direct_mentions
|
|
|
|
if mentions.present?
|
2024-10-15 16:20:31 -04:00
|
|
|
AiPersona
|
|
|
|
.allowed_modalities(allow_chat_channel_mentions: true)
|
|
|
|
.find { |p| p[:username].in?(mentions) && (user.group_ids & p[:allowed_group_ids]) }
|
2024-05-05 19:49:02 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
def self.schedule_chat_reply(message, channel, user, context)
|
|
|
|
return if !SiteSetting.ai_bot_enabled
|
2024-10-15 16:20:31 -04:00
|
|
|
|
|
|
|
all_chat =
|
|
|
|
AiPersona.allowed_modalities(
|
|
|
|
allow_chat_channel_mentions: true,
|
|
|
|
allow_chat_direct_messages: true,
|
|
|
|
)
|
|
|
|
return if all_chat.blank?
|
|
|
|
return if all_chat.any? { |m| m[:user_id] == user.id }
|
2024-05-06 20:30:39 -04:00
|
|
|
|
|
|
|
persona = find_chat_persona(message, channel, user)
|
|
|
|
return if !persona
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
post_ids = nil
|
|
|
|
post_ids = context.dig(:context, :post_ids) if context.is_a?(Hash)
|
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
::Jobs.enqueue(
|
|
|
|
:create_ai_chat_reply,
|
|
|
|
channel_id: channel.id,
|
|
|
|
message_id: message.id,
|
|
|
|
persona_id: persona[:id],
|
2024-05-21 03:17:02 -04:00
|
|
|
context_post_ids: post_ids,
|
2024-05-06 20:30:39 -04:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2024-02-28 00:46:32 -05:00
|
|
|
def self.is_bot_user_id?(user_id)
|
2024-03-04 18:02:49 -05:00
|
|
|
# this will catch everything and avoid any feedback loops
|
|
|
|
# we could get feedback loops between say discobot and ai-bot or third party plugins
|
|
|
|
# and bots
|
|
|
|
user_id.to_i <= 0
|
2024-02-28 00:46:32 -05:00
|
|
|
end
|
2024-02-15 00:37:59 -05:00
|
|
|
|
2024-02-28 00:46:32 -05:00
|
|
|
def self.schedule_reply(post)
|
|
|
|
return if is_bot_user_id?(post.user_id)
|
2024-10-15 16:20:31 -04:00
|
|
|
mentionables = nil
|
2024-02-28 00:46:32 -05:00
|
|
|
|
2024-10-15 16:20:31 -04:00
|
|
|
if post.topic.private_message?
|
|
|
|
mentionables =
|
|
|
|
AiPersona.allowed_modalities(user: post.user, allow_personal_messages: true)
|
|
|
|
else
|
|
|
|
mentionables = AiPersona.allowed_modalities(user: post.user, allow_topic_mentions: true)
|
|
|
|
end
|
2024-02-15 00:37:59 -05:00
|
|
|
|
|
|
|
bot_user = nil
|
|
|
|
mentioned = nil
|
|
|
|
|
2024-11-25 15:19:56 -05:00
|
|
|
all_llm_users =
|
|
|
|
LlmModel
|
|
|
|
.where(enabled_chat_bot: true)
|
|
|
|
.joins(:user)
|
|
|
|
.pluck("users.id", "users.username_lower")
|
2024-10-15 16:20:31 -04:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
if post.topic.private_message?
|
2024-10-15 16:20:31 -04:00
|
|
|
# this is an edge case, you started a PM with a different bot
|
2024-11-25 15:19:56 -05:00
|
|
|
bot_user =
|
|
|
|
post.topic.topic_allowed_users.where(user_id: all_llm_users.map(&:first)).first&.user
|
2024-02-28 00:46:32 -05:00
|
|
|
bot_user ||=
|
|
|
|
post
|
|
|
|
.topic
|
|
|
|
.topic_allowed_users
|
|
|
|
.where(user_id: mentionables.map { |m| m[:user_id] })
|
|
|
|
.first
|
|
|
|
&.user
|
2024-02-15 00:37:59 -05:00
|
|
|
end
|
|
|
|
|
2024-11-25 15:19:56 -05:00
|
|
|
mentions = nil
|
|
|
|
if mentionables.present? || (bot_user && post.topic.private_message?)
|
2024-02-15 00:37:59 -05:00
|
|
|
mentions = post.mentions.map(&:downcase)
|
2024-03-19 05:15:12 -04:00
|
|
|
|
|
|
|
# in case we are replying to a post by a bot
|
2024-08-07 13:22:32 -04:00
|
|
|
if post.reply_to_post_number && post.reply_to_post&.user
|
2024-03-19 05:15:12 -04:00
|
|
|
mentions << post.reply_to_post.user.username_lower
|
|
|
|
end
|
2024-11-25 15:19:56 -05:00
|
|
|
end
|
2024-03-19 05:15:12 -04:00
|
|
|
|
2024-11-25 15:19:56 -05:00
|
|
|
if mentionables.present?
|
2024-02-28 00:46:32 -05:00
|
|
|
mentioned = mentionables.find { |mentionable| mentions.include?(mentionable[:username]) }
|
2024-02-15 00:37:59 -05:00
|
|
|
|
2024-02-28 00:46:32 -05:00
|
|
|
# direct PM to mentionable
|
|
|
|
if !mentioned && bot_user
|
|
|
|
mentioned = mentionables.find { |mentionable| bot_user.id == mentionable[:user_id] }
|
2024-02-15 00:37:59 -05:00
|
|
|
end
|
2024-02-28 00:46:32 -05:00
|
|
|
|
|
|
|
# public topic so we need to use the persona user
|
|
|
|
bot_user ||= User.find_by(id: mentioned[:user_id]) if mentioned
|
2024-02-15 00:37:59 -05:00
|
|
|
end
|
|
|
|
|
2024-03-19 05:15:12 -04:00
|
|
|
if bot_user && post.reply_to_post_number && !post.reply_to_post.user&.bot?
|
|
|
|
# replying to a non-bot user
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
if bot_user
|
2024-11-25 15:19:56 -05:00
|
|
|
topic_persona_id = post.topic.custom_fields["ai_persona_id"]
|
|
|
|
persona_id = mentioned&.dig(:id) || topic_persona_id
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
persona = nil
|
|
|
|
|
|
|
|
if persona_id
|
|
|
|
persona =
|
|
|
|
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
|
|
|
|
end
|
|
|
|
|
|
|
|
if !persona && persona_name = post.topic.custom_fields["ai_persona"]
|
|
|
|
persona =
|
|
|
|
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
|
|
|
|
end
|
|
|
|
|
2024-11-25 15:19:56 -05:00
|
|
|
# edge case, llm was mentioned in an ai persona conversation
|
|
|
|
if persona_id == topic_persona_id.to_i && post.topic.private_message? && persona &&
|
|
|
|
all_llm_users.present?
|
|
|
|
if !persona.force_default_llm && mentions.present?
|
|
|
|
mentioned_llm_user_id, _ =
|
|
|
|
all_llm_users.find { |id, username| mentions.include?(username) }
|
|
|
|
|
|
|
|
if mentioned_llm_user_id
|
|
|
|
bot_user = User.find_by(id: mentioned_llm_user_id) || bot_user
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
persona ||= DiscourseAi::AiBot::Personas::General
|
|
|
|
|
2024-10-15 16:20:31 -04:00
|
|
|
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
|
|
|
|
new(bot).update_playground_with(post)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def initialize(bot)
|
|
|
|
@bot = bot
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_playground_with(post)
|
2024-09-03 01:52:20 -04:00
|
|
|
schedule_bot_reply(post) if can_attach?(post)
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def conversation_context(post)
|
|
|
|
# Pay attention to the `post_number <= ?` here.
|
|
|
|
# We want to inject the last post as context because they are translated differently.
|
2024-02-15 00:37:59 -05:00
|
|
|
|
|
|
|
# also setting default to 40, allowing huge contexts costs lots of tokens
|
|
|
|
max_posts = 40
|
|
|
|
if bot.persona.class.respond_to?(:max_context_posts)
|
|
|
|
max_posts = bot.persona.class.max_context_posts || 40
|
|
|
|
end
|
|
|
|
|
|
|
|
post_types = [Post.types[:regular]]
|
|
|
|
post_types << Post.types[:whisper] if post.post_type == Post.types[:whisper]
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
context =
|
|
|
|
post
|
|
|
|
.topic
|
|
|
|
.posts
|
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-26 23:30:11 -04:00
|
|
|
.joins(:user)
|
2024-01-04 08:44:07 -05:00
|
|
|
.joins("LEFT JOIN post_custom_prompts ON post_custom_prompts.post_id = posts.id")
|
|
|
|
.where("post_number <= ?", post.post_number)
|
|
|
|
.order("post_number desc")
|
2024-02-15 00:37:59 -05:00
|
|
|
.where("post_type in (?)", post_types)
|
|
|
|
.limit(max_posts)
|
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-26 23:30:11 -04:00
|
|
|
.pluck(
|
|
|
|
"posts.raw",
|
|
|
|
"users.username",
|
|
|
|
"post_custom_prompts.custom_prompt",
|
|
|
|
"(
|
|
|
|
SELECT array_agg(ref.upload_id)
|
|
|
|
FROM upload_references ref
|
|
|
|
WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
|
|
|
|
) as upload_ids",
|
|
|
|
)
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
builder = DiscourseAi::Completions::PromptMessagesBuilder.new
|
2024-01-04 08:44:07 -05:00
|
|
|
|
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-26 23:30:11 -04:00
|
|
|
context.reverse_each do |raw, username, custom_prompt, upload_ids|
|
2024-01-04 08:44:07 -05:00
|
|
|
custom_prompt_translation =
|
|
|
|
Proc.new do |message|
|
|
|
|
# We can't keep backwards-compatibility for stored functions.
|
|
|
|
# Tool syntax requires a tool_call_id which we don't have.
|
|
|
|
if message[2] != "function"
|
|
|
|
custom_context = {
|
|
|
|
content: message[0],
|
2024-01-12 12:36:44 -05:00
|
|
|
type: message[2].present? ? message[2].to_sym : :model,
|
2024-01-04 08:44:07 -05:00
|
|
|
}
|
|
|
|
|
2024-01-12 12:36:44 -05:00
|
|
|
custom_context[:id] = message[1] if custom_context[:type] != :model
|
2024-03-08 16:46:40 -05:00
|
|
|
custom_context[:name] = message[3] if message[3]
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
builder.push(**custom_context)
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if custom_prompt.present?
|
2024-03-05 14:04:37 -05:00
|
|
|
custom_prompt.each(&custom_prompt_translation)
|
2024-01-04 08:44:07 -05:00
|
|
|
else
|
|
|
|
context = {
|
|
|
|
content: raw,
|
2024-01-12 12:36:44 -05:00
|
|
|
type: (available_bot_usernames.include?(username) ? :model : :user),
|
2024-01-04 08:44:07 -05:00
|
|
|
}
|
|
|
|
|
2024-01-12 12:36:44 -05:00
|
|
|
context[:id] = username if context[:type] == :user
|
2024-01-04 08:44:07 -05:00
|
|
|
|
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-26 23:30:11 -04:00
|
|
|
if upload_ids.present? && context[:type] == :user && bot.persona.class.vision_enabled
|
|
|
|
context[:upload_ids] = upload_ids.compact
|
|
|
|
end
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
builder.push(**context)
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
builder.to_a
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def title_playground(post)
|
|
|
|
context = conversation_context(post)
|
|
|
|
|
|
|
|
bot
|
2024-03-01 15:53:21 -05:00
|
|
|
.get_updated_title(context, post)
|
2024-01-04 08:44:07 -05:00
|
|
|
.tap do |new_title|
|
|
|
|
PostRevisor.new(post.topic.first_post, post.topic).revise!(
|
|
|
|
bot.bot_user,
|
|
|
|
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
|
|
|
|
)
|
|
|
|
end
|
2024-09-03 01:52:20 -04:00
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
def chat_context(message, channel, persona_user, context_post_ids)
|
2024-05-05 19:49:02 -04:00
|
|
|
has_vision = bot.persona.class.vision_enabled
|
2024-05-08 04:44:04 -04:00
|
|
|
include_thread_titles = !channel.direct_message_channel? && !message.thread_id
|
|
|
|
|
|
|
|
current_id = message.id
|
|
|
|
if !channel.direct_message_channel?
|
|
|
|
# we are interacting via mentions ... strip mention
|
|
|
|
instruction_message = message.message.gsub(/@#{bot.bot_user.username}/i, "").strip
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
messages = nil
|
2024-02-15 00:37:59 -05:00
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
max_messages = 40
|
|
|
|
if bot.persona.class.respond_to?(:max_context_posts)
|
|
|
|
max_messages = bot.persona.class.max_context_posts || 40
|
|
|
|
end
|
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
if !message.thread_id && channel.direct_message_channel?
|
|
|
|
messages = [message]
|
|
|
|
elsif !channel.direct_message_channel? && !message.thread_id
|
|
|
|
messages =
|
|
|
|
Chat::Message
|
2024-05-08 04:44:04 -04:00
|
|
|
.joins("left join chat_threads on chat_threads.id = chat_messages.thread_id")
|
|
|
|
.where(chat_channel_id: channel.id)
|
|
|
|
.where(
|
|
|
|
"chat_messages.thread_id IS NULL OR chat_threads.original_message_id = chat_messages.id",
|
|
|
|
)
|
2024-05-06 20:30:39 -04:00
|
|
|
.order(id: :desc)
|
|
|
|
.limit(max_messages)
|
|
|
|
.to_a
|
|
|
|
.reverse
|
|
|
|
end
|
|
|
|
|
|
|
|
messages ||=
|
2024-05-05 19:49:02 -04:00
|
|
|
ChatSDK::Thread.last_messages(
|
|
|
|
thread_id: message.thread_id,
|
|
|
|
guardian: Discourse.system_user.guardian,
|
|
|
|
page_size: max_messages,
|
|
|
|
)
|
|
|
|
|
|
|
|
builder = DiscourseAi::Completions::PromptMessagesBuilder.new
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
guardian = Guardian.new(message.user)
|
|
|
|
if context_post_ids
|
|
|
|
builder.set_chat_context_posts(context_post_ids, guardian, include_uploads: has_vision)
|
|
|
|
end
|
|
|
|
|
2024-05-06 20:30:39 -04:00
|
|
|
messages.each do |m|
|
2024-05-08 04:44:04 -04:00
|
|
|
# restore stripped message
|
|
|
|
m.message = instruction_message if m.id == current_id && instruction_message
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
if available_bot_user_ids.include?(m.user_id)
|
|
|
|
builder.push(type: :model, content: m.message)
|
|
|
|
else
|
|
|
|
upload_ids = nil
|
|
|
|
upload_ids = m.uploads.map(&:id) if has_vision && m.uploads.present?
|
2024-05-08 04:44:04 -04:00
|
|
|
mapped_message = m.message
|
|
|
|
|
|
|
|
thread_title = nil
|
|
|
|
thread_title = m.thread&.title if include_thread_titles && m.thread_id
|
|
|
|
mapped_message = "(#{thread_title})\n#{m.message}" if thread_title
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
builder.push(
|
|
|
|
type: :user,
|
2024-05-08 04:44:04 -04:00
|
|
|
content: mapped_message,
|
2024-05-05 19:49:02 -04:00
|
|
|
name: m.user.username,
|
|
|
|
upload_ids: upload_ids,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
builder.to_a(
|
|
|
|
limit: max_messages,
|
|
|
|
style: channel.direct_message_channel? ? :chat_with_context : :chat,
|
|
|
|
)
|
2024-05-05 19:49:02 -04:00
|
|
|
end
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
def reply_to_chat_message(message, channel, context_post_ids)
|
2024-05-05 19:49:02 -04:00
|
|
|
persona_user = User.find(bot.persona.class.user_id)
|
|
|
|
|
|
|
|
participants = channel.user_chat_channel_memberships.map { |m| m.user.username }
|
|
|
|
|
2024-05-21 03:17:02 -04:00
|
|
|
context_post_ids = nil if !channel.direct_message_channel?
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
context =
|
|
|
|
get_context(
|
|
|
|
participants: participants.join(", "),
|
2024-05-21 03:17:02 -04:00
|
|
|
conversation_context: chat_context(message, channel, persona_user, context_post_ids),
|
2024-05-05 19:49:02 -04:00
|
|
|
user: message.user,
|
|
|
|
skip_tool_details: true,
|
|
|
|
)
|
|
|
|
|
|
|
|
reply = nil
|
|
|
|
guardian = Guardian.new(persona_user)
|
|
|
|
|
2024-05-08 04:44:04 -04:00
|
|
|
force_thread = message.thread_id.nil? && channel.direct_message_channel?
|
|
|
|
in_reply_to_id = channel.direct_message_channel? ? message.id : nil
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
new_prompts =
|
|
|
|
bot.reply(context) do |partial, cancel, placeholder|
|
|
|
|
if !reply
|
|
|
|
# just eat all leading spaces we can not create the message
|
|
|
|
next if partial.blank?
|
|
|
|
reply =
|
|
|
|
ChatSDK::Message.create(
|
|
|
|
raw: partial,
|
|
|
|
thread_id: message.thread_id,
|
|
|
|
channel_id: channel.id,
|
|
|
|
guardian: guardian,
|
2024-05-08 04:44:04 -04:00
|
|
|
in_reply_to_id: in_reply_to_id,
|
|
|
|
force_thread: force_thread,
|
2024-05-06 20:30:39 -04:00
|
|
|
enforce_membership: !channel.direct_message_channel?,
|
2024-05-05 19:49:02 -04:00
|
|
|
)
|
|
|
|
ChatSDK::Message.start_stream(message_id: reply.id, guardian: guardian)
|
|
|
|
else
|
|
|
|
streaming =
|
|
|
|
ChatSDK::Message.stream(message_id: reply.id, raw: partial, guardian: guardian)
|
|
|
|
|
|
|
|
if !streaming
|
|
|
|
cancel&.call
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if new_prompts.length > 1 && reply.id
|
|
|
|
ChatMessageCustomPrompt.create!(message_id: reply.id, custom_prompt: new_prompts)
|
|
|
|
end
|
|
|
|
|
|
|
|
ChatSDK::Message.stop_stream(message_id: reply.id, guardian: guardian) if reply
|
|
|
|
|
|
|
|
reply
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_context(participants:, conversation_context:, user:, skip_tool_details: nil)
|
|
|
|
result = {
|
2024-01-04 08:44:07 -05:00
|
|
|
site_url: Discourse.base_url,
|
|
|
|
site_title: SiteSetting.title,
|
|
|
|
site_description: SiteSetting.site_description,
|
|
|
|
time: Time.zone.now,
|
2024-05-05 19:49:02 -04:00
|
|
|
participants: participants,
|
|
|
|
conversation_context: conversation_context,
|
|
|
|
user: user,
|
2024-01-04 08:44:07 -05:00
|
|
|
}
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
result[:skip_tool_details] = true if skip_tool_details
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
2024-11-04 15:43:26 -05:00
|
|
|
def reply_to(post, custom_instructions: nil, &blk)
|
2024-10-29 19:28:20 -04:00
|
|
|
# this is a multithreading issue
|
|
|
|
# post custom prompt is needed and it may not
|
|
|
|
# be properly loaded, ensure it is loaded
|
|
|
|
PostCustomPrompt.none
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
reply = +""
|
2024-11-18 17:22:39 -05:00
|
|
|
post_streamer = nil
|
2024-05-05 19:49:02 -04:00
|
|
|
|
|
|
|
post_type =
|
|
|
|
post.post_type == Post.types[:whisper] ? Post.types[:whisper] : Post.types[:regular]
|
|
|
|
|
|
|
|
context =
|
|
|
|
get_context(
|
|
|
|
participants: post.topic.allowed_users.map(&:username).join(", "),
|
|
|
|
conversation_context: conversation_context(post),
|
|
|
|
user: post.user,
|
|
|
|
)
|
|
|
|
context[:post_id] = post.id
|
|
|
|
context[:topic_id] = post.topic_id
|
2024-05-07 07:55:46 -04:00
|
|
|
context[:private_message] = post.topic.private_message?
|
2024-11-04 15:43:26 -05:00
|
|
|
context[:custom_instructions] = custom_instructions
|
2024-05-05 19:49:02 -04:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
reply_user = bot.bot_user
|
|
|
|
if bot.persona.class.respond_to?(:user_id)
|
|
|
|
reply_user = User.find_by(id: bot.persona.class.user_id) || reply_user
|
|
|
|
end
|
|
|
|
|
|
|
|
stream_reply = post.topic.private_message?
|
|
|
|
|
|
|
|
# we need to ensure persona user is allowed to reply to the pm
|
|
|
|
if post.topic.private_message?
|
|
|
|
if !post.topic.topic_allowed_users.exists?(user_id: reply_user.id)
|
|
|
|
post.topic.topic_allowed_users.create!(user_id: reply_user.id)
|
|
|
|
end
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
if stream_reply
|
|
|
|
reply_post =
|
|
|
|
PostCreator.create!(
|
|
|
|
reply_user,
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
raw: "",
|
|
|
|
skip_validations: true,
|
|
|
|
skip_jobs: true,
|
|
|
|
post_type: post_type,
|
|
|
|
)
|
2024-01-09 07:20:28 -05:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
publish_update(reply_post, { raw: reply_post.cooked })
|
|
|
|
|
|
|
|
redis_stream_key = "gpt_cancel:#{reply_post.id}"
|
|
|
|
Discourse.redis.setex(redis_stream_key, 60, 1)
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-06-11 04:14:14 -04:00
|
|
|
context[:skip_tool_details] ||= !bot.persona.class.tool_details
|
|
|
|
|
2024-11-18 17:22:39 -05:00
|
|
|
post_streamer = PostStreamer.new(delay: Rails.env.test? ? 0 : 0.5) if stream_reply
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
new_custom_prompts =
|
2024-10-29 19:28:20 -04:00
|
|
|
bot.reply(context) do |partial, cancel, placeholder, type|
|
2024-01-04 08:44:07 -05:00
|
|
|
reply << partial
|
|
|
|
raw = reply.dup
|
2024-11-13 14:58:24 -05:00
|
|
|
raw << "\n\n" << placeholder if placeholder.present?
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-11-18 17:22:39 -05:00
|
|
|
blk.call(partial) if blk && type != :tool_details && type != :partial_tool
|
2024-10-29 19:28:20 -04:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
if stream_reply && !Discourse.redis.get(redis_stream_key)
|
2024-01-04 08:44:07 -05:00
|
|
|
cancel&.call
|
|
|
|
reply_post.update!(raw: reply, cooked: PrettyText.cook(reply))
|
2024-11-21 01:51:45 -05:00
|
|
|
# we do not break out, cause if we do
|
|
|
|
# we will not get results from bot
|
|
|
|
# leading to broken context
|
|
|
|
# we need to trust it to cancel at the endpoint
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
2024-11-18 17:22:39 -05:00
|
|
|
if post_streamer
|
|
|
|
post_streamer.run_later do
|
|
|
|
Discourse.redis.expire(redis_stream_key, 60)
|
|
|
|
publish_update(reply_post, { raw: raw })
|
2024-02-15 00:37:59 -05:00
|
|
|
end
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
return if reply.blank?
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
if stream_reply
|
2024-11-18 17:22:39 -05:00
|
|
|
post_streamer.finish
|
|
|
|
post_streamer = nil
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
# land the final message prior to saving so we don't clash
|
|
|
|
reply_post.cooked = PrettyText.cook(reply)
|
|
|
|
publish_final_update(reply_post)
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
reply_post.revise(
|
|
|
|
bot.bot_user,
|
|
|
|
{ raw: reply },
|
|
|
|
skip_validations: true,
|
|
|
|
skip_revision: true,
|
|
|
|
)
|
|
|
|
else
|
|
|
|
reply_post =
|
|
|
|
PostCreator.create!(
|
|
|
|
reply_user,
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
raw: reply,
|
|
|
|
skip_validations: true,
|
|
|
|
post_type: post_type,
|
|
|
|
)
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
# we do not need to add a custom prompt for a single reply
|
2024-01-05 13:21:14 -05:00
|
|
|
if new_custom_prompts.length > 1
|
|
|
|
reply_post.post_custom_prompt ||= reply_post.build_post_custom_prompt(custom_prompt: [])
|
|
|
|
prompt = reply_post.post_custom_prompt.custom_prompt || []
|
2024-01-04 08:44:07 -05:00
|
|
|
prompt.concat(new_custom_prompts)
|
2024-01-05 13:21:14 -05:00
|
|
|
reply_post.post_custom_prompt.update!(custom_prompt: prompt)
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
2024-01-05 13:21:14 -05:00
|
|
|
|
2024-03-03 17:56:59 -05:00
|
|
|
# since we are skipping validations and jobs we
|
|
|
|
# may need to fix participant count
|
|
|
|
if reply_post.topic.private_message? && reply_post.topic.participant_count < 2
|
|
|
|
reply_post.topic.update!(participant_count: 2)
|
|
|
|
end
|
|
|
|
|
2024-01-05 13:21:14 -05:00
|
|
|
reply_post
|
2024-01-15 02:51:14 -05:00
|
|
|
ensure
|
2024-11-18 17:22:39 -05:00
|
|
|
post_streamer&.finish(skip_callback: true)
|
2024-02-15 00:37:59 -05:00
|
|
|
publish_final_update(reply_post) if stream_reply
|
2024-09-03 01:52:20 -04:00
|
|
|
if reply_post && post.post_number == 1 && post.topic.private_message?
|
|
|
|
title_playground(reply_post)
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:37:23 -05:00
|
|
|
def available_bot_usernames
|
|
|
|
@bot_usernames ||=
|
2024-06-18 13:32:14 -04:00
|
|
|
AiPersona.joins(:user).pluck(:username).concat(available_bot_users.map(&:username))
|
2024-03-07 14:37:23 -05:00
|
|
|
end
|
|
|
|
|
2024-05-05 19:49:02 -04:00
|
|
|
def available_bot_user_ids
|
2024-06-18 13:32:14 -04:00
|
|
|
@bot_ids ||= AiPersona.joins(:user).pluck("users.id").concat(available_bot_users.map(&:id))
|
2024-05-05 19:49:02 -04:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
private
|
|
|
|
|
2024-06-18 13:32:14 -04:00
|
|
|
def available_bot_users
|
|
|
|
@available_bots ||=
|
|
|
|
User.joins("INNER JOIN llm_models llm ON llm.user_id = users.id").where(active: true)
|
|
|
|
end
|
|
|
|
|
2024-01-15 02:51:14 -05:00
|
|
|
def publish_final_update(reply_post)
|
|
|
|
return if @published_final_update
|
|
|
|
if reply_post
|
|
|
|
publish_update(reply_post, { cooked: reply_post.cooked, done: true })
|
|
|
|
# we subscribe at position -2 so we will always get this message
|
|
|
|
# moving all cooked on every page load is wasteful ... this means
|
|
|
|
# we have a benign message at the end, 2 is set to ensure last message
|
|
|
|
# is delivered
|
|
|
|
publish_update(reply_post, { noop: true })
|
|
|
|
@published_final_update = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def can_attach?(post)
|
|
|
|
return false if bot.bot_user.nil?
|
2024-02-15 00:37:59 -05:00
|
|
|
return false if post.topic.private_message? && post.post_type != Post.types[:regular]
|
2024-01-04 08:44:07 -05:00
|
|
|
return false if (SiteSetting.ai_bot_allowed_groups_map & post.user.group_ids).blank?
|
2024-10-30 05:24:39 -04:00
|
|
|
return false if post.custom_fields[BYPASS_AI_REPLY_CUSTOM_FIELD].present?
|
2024-01-04 08:44:07 -05:00
|
|
|
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2024-02-15 00:37:59 -05:00
|
|
|
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,
|
2024-01-04 08:44:07 -05:00
|
|
|
post_id: post.id,
|
2024-02-15 00:37:59 -05:00
|
|
|
bot_user_id: bot.bot_user.id,
|
|
|
|
persona_id: persona_id,
|
2024-01-04 08:44:07 -05:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def context(topic)
|
|
|
|
{
|
|
|
|
site_url: Discourse.base_url,
|
|
|
|
site_title: SiteSetting.title,
|
|
|
|
site_description: SiteSetting.site_description,
|
|
|
|
time: Time.zone.now,
|
|
|
|
participants: topic.allowed_users.map(&:username).join(", "),
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def publish_update(bot_reply_post, payload)
|
2024-01-15 02:51:14 -05:00
|
|
|
payload = { post_id: bot_reply_post.id, post_number: bot_reply_post.post_number }.merge(
|
|
|
|
payload,
|
|
|
|
)
|
2024-01-04 08:44:07 -05:00
|
|
|
MessageBus.publish(
|
|
|
|
"discourse-ai/ai-bot/topic/#{bot_reply_post.topic_id}",
|
2024-01-15 02:51:14 -05:00
|
|
|
payload,
|
2024-01-04 08:44:07 -05:00
|
|
|
user_ids: bot_reply_post.topic.allowed_user_ids,
|
2024-01-15 02:51:14 -05:00
|
|
|
max_backlog_size: 2,
|
|
|
|
max_backlog_age: 60,
|
2024-01-04 08:44:07 -05:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|