mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-03-09 02:40:50 +00:00
- 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.
226 lines
7.6 KiB
Ruby
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
|