mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-02-18 01:14:53 +00:00
* FIX: Llm selector / forced tools / search tool This fixes a few issues: 1. When search was not finding any semantic results we would break the tool 2. Gemin / Anthropic models did not implement forced tools previously despite it being an API option 3. Mechanics around displaying llm selector were not right. If you disabled LLM selector server side persona PM did not work correctly. 4. Disabling native tools for anthropic model moved out of a site setting. This deliberately does not migrate cause this feature is really rare to need now, people who had it set probably did not need it. 5. Updates anthropic model names to latest release * linting * fix a couple of tests I missed * clean up conditional
211 lines
7.2 KiB
Ruby
211 lines
7.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module AiBot
|
|
USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
|
|
|
|
class EntryPoint
|
|
Bot = Struct.new(:id, :name, :llm)
|
|
|
|
def self.all_bot_ids
|
|
AiPersona
|
|
.persona_users
|
|
.map { |persona| persona[:user_id] }
|
|
.concat(LlmModel.where(enabled_chat_bot: true).pluck(:user_id))
|
|
end
|
|
|
|
def self.find_participant_in(participant_ids)
|
|
model = LlmModel.includes(:user).where(user_id: participant_ids).last
|
|
return if model.nil?
|
|
|
|
bot_user = model.user
|
|
|
|
Bot.new(bot_user.id, bot_user.username_lower, model.name)
|
|
end
|
|
|
|
def self.find_user_from_model(model_name)
|
|
# Hack(Roman): Added this because Command R Plus had a different in the bot settings.
|
|
# Will eventually ammend it with a data migration.
|
|
name = model_name
|
|
name = "command-r-plus" if name == "cohere-command-r-plus"
|
|
|
|
LlmModel.joins(:user).where(name: name).last&.user
|
|
end
|
|
|
|
def self.enabled_user_ids_and_models_map
|
|
DB.query_hash(<<~SQL)
|
|
SELECT users.username AS username, users.id AS id, llms.name AS model_name, llms.display_name AS display_name
|
|
FROM llm_models llms
|
|
INNER JOIN users ON llms.user_id = users.id
|
|
WHERE llms.enabled_chat_bot
|
|
SQL
|
|
end
|
|
|
|
# Most errors are simply "not_allowed"
|
|
# we do not want to reveal information about this system
|
|
# the 2 exceptions are "other_people_in_pm" and "other_content_in_pm"
|
|
# in both cases you have access to the PM so we are not revealing anything
|
|
def self.ai_share_error(topic, guardian)
|
|
return nil if guardian.can_share_ai_bot_conversation?(topic)
|
|
|
|
return :not_allowed if !guardian.can_see?(topic)
|
|
|
|
# other people in PM
|
|
if topic.topic_allowed_users.where("user_id > 0 and user_id <> ?", guardian.user.id).exists?
|
|
return :other_people_in_pm
|
|
end
|
|
|
|
# other content in PM
|
|
if topic.posts.where("user_id > 0 and user_id <> ?", guardian.user.id).exists?
|
|
return :other_content_in_pm
|
|
end
|
|
|
|
:not_allowed
|
|
end
|
|
|
|
def inject_into(plugin)
|
|
plugin.register_modifier(:chat_allowed_bot_user_ids) do |user_ids, guardian|
|
|
if guardian.user
|
|
allowed_chat =
|
|
AiPersona.allowed_modalities(
|
|
user: guardian.user,
|
|
allow_chat_direct_messages: true,
|
|
allow_chat_channel_mentions: true,
|
|
)
|
|
allowed_bot_ids = allowed_chat.map { |info| info[:user_id] }
|
|
user_ids.concat(allowed_bot_ids)
|
|
end
|
|
user_ids
|
|
end
|
|
|
|
plugin.on(:site_setting_changed) do |name, _old_value, _new_value|
|
|
if name == :ai_bot_enabled || name == :discourse_ai_enabled
|
|
DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots
|
|
end
|
|
end
|
|
|
|
Oneboxer.register_local_handler(
|
|
"discourse_ai/ai_bot/shared_ai_conversations",
|
|
) do |url, route|
|
|
if route[:action] == "show" && share_key = route[:share_key]
|
|
if conversation = SharedAiConversation.find_by(share_key: share_key)
|
|
conversation.onebox
|
|
end
|
|
end
|
|
end
|
|
|
|
plugin.on(:reduce_excerpt) do |doc, options|
|
|
doc.css("details").remove if options && options[:strip_details]
|
|
end
|
|
|
|
plugin.register_seedfu_fixtures(
|
|
Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"),
|
|
)
|
|
|
|
plugin.add_to_serializer(
|
|
:current_user,
|
|
:ai_enabled_personas,
|
|
include_condition: -> do
|
|
SiteSetting.ai_bot_enabled && scope.authenticated? &&
|
|
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
|
|
end,
|
|
) do
|
|
DiscourseAi::AiBot::Personas::Persona
|
|
.all(user: scope.user)
|
|
.map do |persona|
|
|
{
|
|
id: persona.id,
|
|
name: persona.name,
|
|
description: persona.description,
|
|
force_default_llm: persona.force_default_llm,
|
|
username: persona.username,
|
|
}
|
|
end
|
|
end
|
|
|
|
plugin.add_to_serializer(
|
|
:current_user,
|
|
:can_debug_ai_bot_conversations,
|
|
include_condition: -> do
|
|
SiteSetting.ai_bot_enabled && scope.authenticated? &&
|
|
SiteSetting.ai_bot_debugging_allowed_groups.present? &&
|
|
scope.user.in_any_groups?(SiteSetting.ai_bot_debugging_allowed_groups_map)
|
|
end,
|
|
) { true }
|
|
|
|
plugin.add_to_serializer(
|
|
:current_user,
|
|
:ai_enabled_chat_bots,
|
|
include_condition: -> do
|
|
SiteSetting.ai_bot_enabled && scope.authenticated? &&
|
|
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
|
|
end,
|
|
) do
|
|
bots_map = ::DiscourseAi::AiBot::EntryPoint.enabled_user_ids_and_models_map
|
|
|
|
persona_users = AiPersona.persona_users(user: scope.user)
|
|
if persona_users.present?
|
|
persona_users.filter! { |persona_user| persona_user[:username].present? }
|
|
|
|
bots_map.concat(
|
|
persona_users.map do |persona_user|
|
|
{
|
|
"id" => persona_user[:user_id],
|
|
"username" => persona_user[:username],
|
|
"force_default_llm" => persona_user[:force_default_llm],
|
|
"is_persona" => true,
|
|
}
|
|
end,
|
|
)
|
|
end
|
|
|
|
bots_map
|
|
end
|
|
|
|
plugin.add_to_serializer(:current_user, :can_share_ai_bot_conversations) do
|
|
scope.user.in_any_groups?(SiteSetting.ai_bot_public_sharing_allowed_groups_map)
|
|
end
|
|
|
|
plugin.register_svg_icon("robot")
|
|
|
|
plugin.add_to_serializer(
|
|
:topic_view,
|
|
:ai_persona_name,
|
|
include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
|
|
) do
|
|
id = topic.custom_fields["ai_persona_id"]
|
|
name =
|
|
DiscourseAi::AiBot::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
|
|
name || topic.custom_fields["ai_persona"]
|
|
end
|
|
|
|
plugin.on(:post_created) { |post| DiscourseAi::AiBot::Playground.schedule_reply(post) }
|
|
|
|
plugin.on(:chat_message_created) do |chat_message, channel, user, context|
|
|
DiscourseAi::AiBot::Playground.schedule_chat_reply(chat_message, channel, user, context)
|
|
end
|
|
|
|
if plugin.respond_to?(:register_editable_topic_custom_field)
|
|
plugin.register_editable_topic_custom_field(:ai_persona_id)
|
|
end
|
|
|
|
plugin.on(:site_setting_changed) do |name, old_value, new_value|
|
|
if name == :ai_embeddings_model && SiteSetting.ai_embeddings_enabled? &&
|
|
new_value != old_value
|
|
RagDocumentFragment.delete_all
|
|
UploadReference
|
|
.where(target: AiPersona.all)
|
|
.each do |ref|
|
|
Jobs.enqueue(
|
|
:digest_rag_upload,
|
|
ai_persona_id: ref.target_id,
|
|
upload_id: ref.upload_id,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|