discourse-ai/lib/ai_bot/entry_point.rb
Sam 7f16d3ad43
FEATURE: Cohere Command R support (#558)
- Added Cohere Command models (Command, Command Light, Command R, Command R Plus) to the available model list
- Added a new site setting `ai_cohere_api_key` for configuring the Cohere API key
- Implemented a new `DiscourseAi::Completions::Endpoints::Cohere` class to handle interactions with the Cohere API, including:
   - Translating request parameters to the Cohere API format
   - Parsing Cohere API responses 
   - Supporting streaming and non-streaming completions
   - Supporting "tools" which allow the model to call back to discourse to lookup additional information
- Implemented a new `DiscourseAi::Completions::Dialects::Command` class to translate between the generic Discourse AI prompt format and the Cohere Command format
- Added specs covering the new Cohere endpoint and dialect classes
- Updated `DiscourseAi::AiBot::Bot.guess_model` to map the new Cohere model to the appropriate bot user

In summary, this PR adds support for using the Cohere Command family of models with the Discourse AI plugin. It handles configuring API keys, making requests to the Cohere API, and translating between Discourse's generic prompt format and Cohere's specific format. Thorough test coverage was added for the new functionality.
2024-04-11 07:24:17 +10:00

226 lines
7.6 KiB
Ruby

# frozen_string_literal: true
module DiscourseAi
module AiBot
USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
class EntryPoint
REQUIRE_TITLE_UPDATE = "discourse-ai-title-update"
GPT4_ID = -110
GPT3_5_TURBO_ID = -111
CLAUDE_V2_ID = -112
GPT4_TURBO_ID = -113
MIXTRAL_ID = -114
GEMINI_ID = -115
FAKE_ID = -116 # only used for dev and test
CLAUDE_3_OPUS_ID = -117
CLAUDE_3_SONNET_ID = -118
CLAUDE_3_HAIKU_ID = -119
COHERE_COMMAND_R_PLUS = -120
BOTS = [
[GPT4_ID, "gpt4_bot", "gpt-4"],
[GPT3_5_TURBO_ID, "gpt3.5_bot", "gpt-3.5-turbo"],
[CLAUDE_V2_ID, "claude_bot", "claude-2"],
[GPT4_TURBO_ID, "gpt4t_bot", "gpt-4-turbo"],
[MIXTRAL_ID, "mixtral_bot", "mixtral-8x7B-Instruct-V0.1"],
[GEMINI_ID, "gemini_bot", "gemini-pro"],
[FAKE_ID, "fake_bot", "fake"],
[CLAUDE_3_OPUS_ID, "claude_3_opus_bot", "claude-3-opus"],
[CLAUDE_3_SONNET_ID, "claude_3_sonnet_bot", "claude-3-sonnet"],
[CLAUDE_3_HAIKU_ID, "claude_3_haiku_bot", "claude-3-haiku"],
[COHERE_COMMAND_R_PLUS, "cohere_command_bot", "cohere-command-r-plus"],
]
BOT_USER_IDS = BOTS.map(&:first)
Bot = Struct.new(:id, :name, :llm)
def self.all_bot_ids
BOT_USER_IDS.concat(AiPersona.mentionables.map { |mentionable| mentionable[:user_id] })
end
def self.find_bot_by_id(id)
found = DiscourseAi::AiBot::EntryPoint::BOTS.find { |bot| bot[0] == id }
return if !found
Bot.new(found[0], found[1], found[2])
end
def self.map_bot_model_to_user_id(model_name)
case model_name
in "gpt-4-turbo"
GPT4_TURBO_ID
in "gpt-3.5-turbo"
GPT3_5_TURBO_ID
in "gpt-4"
GPT4_ID
in "claude-2"
CLAUDE_V2_ID
in "mixtral-8x7B-Instruct-V0.1"
MIXTRAL_ID
in "gemini-pro"
GEMINI_ID
in "fake"
FAKE_ID
in "claude-3-opus"
CLAUDE_3_OPUS_ID
in "claude-3-sonnet"
CLAUDE_3_SONNET_ID
in "claude-3-haiku"
CLAUDE_3_HAIKU_ID
in "cohere-command-r-plus"
COHERE_COMMAND_R_PLUS
else
nil
end
end
# Most errors are simply "not_allowed"
# we do not want to reveal information about this sytem
# 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.on(:site_setting_changed) do |name, _old_value, _new_value|
if name == :ai_bot_enabled_chat_bots || 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 }
end
end
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
model_map = {}
SiteSetting
.ai_bot_enabled_chat_bots
.split("|")
.each do |bot_name|
model_map[
::DiscourseAi::AiBot::EntryPoint.map_bot_model_to_user_id(bot_name)
] = bot_name
end
# not 100% ideal, cause it is one extra query, but we need it
bots = DB.query_hash(<<~SQL, user_ids: model_map.keys)
SELECT username, id FROM users WHERE id IN (:user_ids)
SQL
bots.each { |hash| hash["model_name"] = model_map[hash["id"]] }
mentionables = AiPersona.mentionables(user: scope.user)
if mentionables.present?
bots.concat(
mentionables.map do |mentionable|
{ "id" => mentionable[:user_id], "username" => mentionable[:username] }
end,
)
end
bots
end
plugin.add_to_serializer(:current_user, :can_use_assistant) do
scope.user.in_any_groups?(SiteSetting.ai_helper_allowed_groups_map)
end
plugin.add_to_serializer(:current_user, :can_use_assistant_in_post) do
scope.user.in_any_groups?(SiteSetting.post_ai_helper_allowed_groups_map)
end
plugin.add_to_serializer(:current_user, :can_use_custom_prompts) do
scope.user.in_any_groups?(SiteSetting.ai_helper_custom_prompts_allowed_groups_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) }
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.find_in_batches do |batch|
batch.each_slice(100) do |fragments|
Jobs.enqueue(:generate_rag_embeddings, fragment_ids: fragments.map(&:id))
end
end
end
end
end
end
end
end