discourse-ai/lib/ai_bot/entry_point.rb

216 lines
7.4 KiB
Ruby
Raw Normal View History

# 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)
FEATURE: UI to update ai personas on admin page (#290) Introduces a UI to manage customizable personas (admin only feature) Part of the change was some extensive internal refactoring: - AIBot now has a persona set in the constructor, once set it never changes - Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly - Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work - Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure - name uniqueness, and only allow certain properties to be touched for system personas. - (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona. - (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta - This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis - Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length - Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things - Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up. - Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer - Migrates the persona selector to gjs --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
.map do |persona|
{
id: persona.id,
name: persona.name,
description: persona.description,
force_default_llm: persona.force_default_llm,
username: persona.username,
}
FEATURE: UI to update ai personas on admin page (#290) Introduces a UI to manage customizable personas (admin only feature) Part of the change was some extensive internal refactoring: - AIBot now has a persona set in the constructor, once set it never changes - Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly - Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work - Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure - name uniqueness, and only allow certain properties to be touched for system personas. - (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona. - (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta - This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis - Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length - Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things - Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up. - Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer - Migrates the persona selector to gjs --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
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? },
FEATURE: UI to update ai personas on admin page (#290) Introduces a UI to manage customizable personas (admin only feature) Part of the change was some extensive internal refactoring: - AIBot now has a persona set in the constructor, once set it never changes - Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly - Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work - Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure - name uniqueness, and only allow certain properties to be touched for system personas. - (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona. - (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta - This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis - Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length - Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things - Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up. - Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer - Migrates the persona selector to gjs --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
) 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
FEATURE: UI to update ai personas on admin page (#290) Introduces a UI to manage customizable personas (admin only feature) Part of the change was some extensive internal refactoring: - AIBot now has a persona set in the constructor, once set it never changes - Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly - Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work - Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure - name uniqueness, and only allow certain properties to be touched for system personas. - (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona. - (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta - This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis - Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length - Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things - Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up. - Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer - Migrates the persona selector to gjs --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
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)
FEATURE: UI to update ai personas on admin page (#290) Introduces a UI to manage customizable personas (admin only feature) Part of the change was some extensive internal refactoring: - AIBot now has a persona set in the constructor, once set it never changes - Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly - Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work - Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure - name uniqueness, and only allow certain properties to be touched for system personas. - (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona. - (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta - This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis - Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length - Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things - Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up. - Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer - Migrates the persona selector to gjs --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
plugin.register_editable_topic_custom_field(:ai_persona_id)
end
plugin.add_api_key_scope(
:discourse_ai,
{ stream_completion: { actions: %w[discourse_ai/admin/ai_personas#stream_reply] } },
)
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