diff --git a/app/controllers/discourse_ai/admin/ai_llms_controller.rb b/app/controllers/discourse_ai/admin/ai_llms_controller.rb index 54230fe0..250dd873 100644 --- a/app/controllers/discourse_ai/admin/ai_llms_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_llms_controller.rb @@ -43,6 +43,7 @@ module DiscourseAi llm_model = LlmModel.find(params[:id]) if llm_model.update(ai_llm_params) + llm_model.toggle_companion_user render json: llm_model else render_json_error llm_model @@ -106,6 +107,7 @@ module DiscourseAi :max_prompt_tokens, :url, :api_key, + :enabled_chat_bot, ) end end diff --git a/app/controllers/discourse_ai/ai_bot/bot_controller.rb b/app/controllers/discourse_ai/ai_bot/bot_controller.rb index 5da8c0c9..1a24215e 100644 --- a/app/controllers/discourse_ai/ai_bot/bot_controller.rb +++ b/app/controllers/discourse_ai/ai_bot/bot_controller.rb @@ -31,12 +31,10 @@ module DiscourseAi end def show_bot_username - bot_user_id = DiscourseAi::AiBot::EntryPoint.map_bot_model_to_user_id(params[:username]) - raise Discourse::InvalidParameters.new(:username) if !bot_user_id + bot_user = DiscourseAi::AiBot::EntryPoint.find_user_from_model(params[:username]) + raise Discourse::InvalidParameters.new(:username) if !bot_user - bot_username_lower = User.find(bot_user_id).username_lower - - render json: { bot_username: bot_username_lower }, status: 200 + render json: { bot_username: bot_user.username_lower }, status: 200 end end end diff --git a/app/models/ai_persona.rb b/app/models/ai_persona.rb index 0d979ed8..a18499be 100644 --- a/app/models/ai_persona.rb +++ b/app/models/ai_persona.rb @@ -252,40 +252,32 @@ end # # Table name: ai_personas # -# id :bigint not null, primary key -# name :string(100) not null -# description :string(2000) not null -# tools :json not null -# system_prompt :string(10000000) not null -# allowed_group_ids :integer default([]), not null, is an Array -# created_by_id :integer -# enabled :boolean default(TRUE), not null -# created_at :datetime not null -# updated_at :datetime not null -# system :boolean default(FALSE), not null -# priority :boolean default(FALSE), not null -# temperature :float -# top_p :float -# user_id :integer -# mentionable :boolean default(FALSE), not null -# default_llm :text -# max_context_posts :integer -# max_post_context_tokens :integer -# max_context_tokens :integer -# vision_enabled :boolean default(FALSE), not null -# vision_max_pixels :integer default(1048576), not null -# rag_chunk_tokens :integer default(374), not null -# rag_chunk_overlap_tokens :integer default(10), not null -# rag_conversation_chunks :integer default(10), not null -# role :enum default("bot"), not null -# role_category_ids :integer default([]), not null, is an Array -# role_tags :string default([]), not null, is an Array -# role_group_ids :integer default([]), not null, is an Array -# role_whispers :boolean default(FALSE), not null -# role_max_responses_per_hour :integer default(50), not null -# question_consolidator_llm :text -# allow_chat :boolean default(FALSE), not null -# tool_details :boolean default(TRUE), not null +# id :bigint not null, primary key +# name :string(100) not null +# description :string(2000) not null +# system_prompt :string(10000000) not null +# allowed_group_ids :integer default([]), not null, is an Array +# created_by_id :integer +# enabled :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# system :boolean default(FALSE), not null +# priority :boolean default(FALSE), not null +# temperature :float +# top_p :float +# user_id :integer +# mentionable :boolean default(FALSE), not null +# default_llm :text +# max_context_posts :integer +# vision_enabled :boolean default(FALSE), not null +# vision_max_pixels :integer default(1048576), not null +# rag_chunk_tokens :integer default(374), not null +# rag_chunk_overlap_tokens :integer default(10), not null +# rag_conversation_chunks :integer default(10), not null +# question_consolidator_llm :text +# allow_chat :boolean default(FALSE), not null +# tool_details :boolean default(TRUE), not null +# tools :json not null # # Indexes # diff --git a/app/models/chat_message_custom_prompt.rb b/app/models/chat_message_custom_prompt.rb index 30b28533..6b36b24b 100644 --- a/app/models/chat_message_custom_prompt.rb +++ b/app/models/chat_message_custom_prompt.rb @@ -6,7 +6,7 @@ end # == Schema Information # -# Table name: message_custom_prompts +# Table name: chat_message_custom_prompts # # id :bigint not null, primary key # message_id :bigint not null @@ -16,5 +16,5 @@ end # # Indexes # -# index_message_custom_prompts_on_message_id (message_id) UNIQUE +# index_chat_message_custom_prompts_on_message_id (message_id) UNIQUE # diff --git a/app/models/llm_model.rb b/app/models/llm_model.rb index 08055802..4ac22fc1 100644 --- a/app/models/llm_model.rb +++ b/app/models/llm_model.rb @@ -1,6 +1,75 @@ # frozen_string_literal: true class LlmModel < ActiveRecord::Base + FIRST_BOT_USER_ID = -1200 + RESERVED_VLLM_SRV_URL = "https://vllm.shadowed-by-srv.invalid" + + belongs_to :user + + validates :url, exclusion: { in: [RESERVED_VLLM_SRV_URL] } + + def self.enable_or_disable_srv_llm! + srv_model = find_by(url: RESERVED_VLLM_SRV_URL) + if SiteSetting.ai_vllm_endpoint_srv.present? && srv_model.blank? + record = + new( + display_name: "vLLM SRV LLM", + name: "mistralai/Mixtral", + provider: "vllm", + tokenizer: "DiscourseAi::Tokenizer::MixtralTokenizer", + url: RESERVED_VLLM_SRV_URL, + vllm_key: "", + user_id: nil, + enabled_chat_bot: false, + ) + + record.save(validate: false) # Ignore reserved URL validation + elsif srv_model.present? + srv_model.destroy! + end + end + + def toggle_companion_user + return if name == "fake" && Rails.env.production? + + enable_check = SiteSetting.ai_bot_enabled && enabled_chat_bot + + if enable_check + if !user + next_id = DB.query_single(<<~SQL).first + SELECT min(id) - 1 FROM users + SQL + + new_user = + User.new( + id: [FIRST_BOT_USER_ID, next_id].min, + email: "no_email_#{name.underscore}", + name: name.titleize, + username: UserNameSuggester.suggest(name), + active: true, + approved: true, + admin: true, + moderator: true, + trust_level: TrustLevel[4], + ) + new_user.save!(validate: false) + self.update!(user: new_user) + else + user.update!(active: true) + end + elsif user + # will include deleted + has_posts = DB.query_single("SELECT 1 FROM posts WHERE user_id = #{user.id} LIMIT 1").present? + + if has_posts + user.update!(active: false) if user.active + else + user.destroy! + self.update!(user: nil) + end + end + end + def tokenizer_class tokenizer.constantize end @@ -20,4 +89,6 @@ end # updated_at :datetime not null # url :string # api_key :string +# user_id :integer +# enabled_chat_bot :boolean default(FALSE), not null # diff --git a/app/models/shared_ai_conversation.rb b/app/models/shared_ai_conversation.rb index a3ba6781..894abf21 100644 --- a/app/models/shared_ai_conversation.rb +++ b/app/models/shared_ai_conversation.rb @@ -133,12 +133,10 @@ class SharedAiConversation < ActiveRecord::Base end def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_usernames: false) - llm_name = nil - topic.topic_allowed_users.each do |tu| - if DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS.include?(tu.user_id) - llm_name = DiscourseAi::AiBot::EntryPoint.find_bot_by_id(tu.user_id)&.llm - end - end + allowed_user_ids = topic.topic_allowed_users.pluck(:user_id) + ai_bot_participant = DiscourseAi::AiBot::EntryPoint.find_participant_in(allowed_user_ids) + + llm_name = ai_bot_participant&.llm llm_name = ActiveSupport::Inflector.humanize(llm_name) if llm_name llm_name ||= I18n.t("discourse_ai.unknown_model") @@ -170,9 +168,7 @@ class SharedAiConversation < ActiveRecord::Base cooked: post.cooked, } - mapped[:persona] = persona if ::DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS.include?( - post.user_id, - ) + mapped[:persona] = persona if ai_bot_participant&.id == post.user_id mapped[:username] = post.user&.username if include_usernames mapped end, diff --git a/app/serializers/llm_model_serializer.rb b/app/serializers/llm_model_serializer.rb index b66a3adb..60e90944 100644 --- a/app/serializers/llm_model_serializer.rb +++ b/app/serializers/llm_model_serializer.rb @@ -3,5 +3,18 @@ class LlmModelSerializer < ApplicationSerializer root "llm" - attributes :id, :display_name, :name, :provider, :max_prompt_tokens, :tokenizer, :api_key, :url + attributes :id, + :display_name, + :name, + :provider, + :max_prompt_tokens, + :tokenizer, + :api_key, + :url, + :enabled_chat_bot, + :url_editable + + def url_editable + object.url != LlmModel::RESERVED_VLLM_SRV_URL + end end diff --git a/assets/javascripts/discourse/admin/models/ai-llm.js b/assets/javascripts/discourse/admin/models/ai-llm.js index e46bf8f2..3c6cb8e9 100644 --- a/assets/javascripts/discourse/admin/models/ai-llm.js +++ b/assets/javascripts/discourse/admin/models/ai-llm.js @@ -11,7 +11,8 @@ export default class AiLlm extends RestModel { "tokenizer", "max_prompt_tokens", "url", - "api_key" + "api_key", + "enabled_chat_bot" ); } diff --git a/assets/javascripts/discourse/components/ai-bot-header-icon.gjs b/assets/javascripts/discourse/components/ai-bot-header-icon.gjs index a95dfe18..6bc04185 100644 --- a/assets/javascripts/discourse/components/ai-bot-header-icon.gjs +++ b/assets/javascripts/discourse/components/ai-bot-header-icon.gjs @@ -1,19 +1,25 @@ import Component from "@glimmer/component"; import { action } from "@ember/object"; import { service } from "@ember/service"; -import { gt } from "truth-helpers"; import DButton from "discourse/components/d-button"; import i18n from "discourse-common/helpers/i18n"; import { composeAiBotMessage } from "../lib/ai-bot-helper"; export default class AiBotHeaderIcon extends Component { + @service currentUser; @service siteSettings; @service composer; get bots() { - return this.siteSettings.ai_bot_add_to_header - ? this.siteSettings.ai_bot_enabled_chat_bots.split("|").filter(Boolean) - : []; + const availableBots = this.currentUser.ai_enabled_chat_bots + .filter((bot) => !bot.is_persosna) + .filter(Boolean); + + return availableBots ? availableBots.map((bot) => bot.model_name) : []; + } + + get showHeaderButton() { + return this.bots.length > 0 && this.siteSettings.ai_bot_add_to_header; } @action @@ -22,7 +28,7 @@ export default class AiBotHeaderIcon extends Component { }