Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8b3a4b40 |
@@ -1,7 +1,8 @@
|
|||||||
< 3.4.0.beta3-dev: decf1bb49d737ea15308400f22f89d1d1e71d13d
|
|
||||||
< 3.4.0.beta1-dev: 9d887ad4ace8e33c3fe7dbb39237e882c08b4f0b
|
|
||||||
< 3.3.0.beta5-dev: 4d8090002f6dcd8e34d41033606bf131fa221475
|
|
||||||
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
|
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
|
||||||
< 3.3.0.beta1-dev: d94234d89f50733678a5c62809445198a9fda74b
|
< 3.3.0.beta1-dev: a65c4076a63104b61d213534db7f89aa3c8acd9e
|
||||||
3.2.999: d94234d89f50733678a5c62809445198a9fda74b
|
< 3.2.0.beta5-dev: c70f43f130fb98e4a1adb1afd9b621aaa49088e3
|
||||||
|
< 3.2.0.beta4-dev: 493b48477a9288f2f1b6f021954088705a6673d6
|
||||||
|
< 3.2.0.beta2-dev: 3bced1c6f525553fe2b5b79d29bd66c1cf2a47d5
|
||||||
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
|
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
|
||||||
|
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
|
||||||
|
3.1.0.beta3: 057fbe1ce6
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/gems
|
/gems
|
||||||
/auto_generated
|
/auto_generated
|
||||||
.env
|
|
||||||
|
|||||||
+3
-1
@@ -32,7 +32,8 @@ GEM
|
|||||||
rack (3.1.6)
|
rack (3.1.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.9.2)
|
regexp_parser (2.9.2)
|
||||||
rexml (3.3.9)
|
rexml (3.3.1)
|
||||||
|
strscan
|
||||||
rubocop (1.64.1)
|
rubocop (1.64.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
@@ -69,6 +70,7 @@ GEM
|
|||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec (~> 3, >= 3.0.1)
|
rubocop-rspec (~> 3, >= 3.0.1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
strscan (3.1.0)
|
||||||
syntax_tree (6.2.0)
|
syntax_tree (6.2.0)
|
||||||
prettier_print (>= 1.2.0)
|
prettier_print (>= 1.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
export default DiscourseRoute.extend({
|
export default DiscourseRoute.extend({
|
||||||
queryParams: {
|
|
||||||
llmTemplate: { refreshModel: true },
|
|
||||||
},
|
|
||||||
|
|
||||||
async model() {
|
async model() {
|
||||||
const record = this.store.createRecord("ai-llm");
|
const record = this.store.createRecord("ai-llm");
|
||||||
record.provider_params = {};
|
|
||||||
return record;
|
return record;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -17,9 +12,5 @@ export default DiscourseRoute.extend({
|
|||||||
"allLlms",
|
"allLlms",
|
||||||
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
||||||
);
|
);
|
||||||
controller.set(
|
|
||||||
"llmTemplate",
|
|
||||||
this.paramsFor(this.routeName).llmTemplate || null
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-3
@@ -4,9 +4,7 @@ export default DiscourseRoute.extend({
|
|||||||
async model(params) {
|
async model(params) {
|
||||||
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
||||||
const id = parseInt(params.id, 10);
|
const id = parseInt(params.id, 10);
|
||||||
const record = allLlms.findBy("id", id);
|
return allLlms.findBy("id", id);
|
||||||
record.provider_params = record.provider_params || {};
|
|
||||||
return record;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
|
|||||||
-2
@@ -10,8 +10,6 @@ export default DiscourseRoute.extend({
|
|||||||
record.set("rag_chunk_tokens", 374);
|
record.set("rag_chunk_tokens", 374);
|
||||||
record.set("rag_chunk_overlap_tokens", 10);
|
record.set("rag_chunk_overlap_tokens", 10);
|
||||||
record.set("rag_conversation_chunks", 10);
|
record.set("rag_conversation_chunks", 10);
|
||||||
record.set("allow_personal_messages", true);
|
|
||||||
record.set("tool_details", false);
|
|
||||||
return record;
|
return record;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -1,5 +1 @@
|
|||||||
<AiLlmsListEditor
|
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
|
||||||
@llms={{this.allLlms}}
|
|
||||||
@currentLlm={{this.model}}
|
|
||||||
@llmTemplate={{this.llmTemplate}}
|
|
||||||
/>
|
|
||||||
@@ -14,9 +14,6 @@ module DiscourseAi
|
|||||||
llms,
|
llms,
|
||||||
each_serializer: LlmModelSerializer,
|
each_serializer: LlmModelSerializer,
|
||||||
root: false,
|
root: false,
|
||||||
scope: {
|
|
||||||
llm_usage: DiscourseAi::Configuration::LlmEnumerator.global_usage,
|
|
||||||
},
|
|
||||||
).as_json,
|
).as_json,
|
||||||
meta: {
|
meta: {
|
||||||
provider_params: LlmModel.provider_params,
|
provider_params: LlmModel.provider_params,
|
||||||
@@ -39,34 +36,26 @@ module DiscourseAi
|
|||||||
llm_model = LlmModel.new(ai_llm_params)
|
llm_model = LlmModel.new(ai_llm_params)
|
||||||
if llm_model.save
|
if llm_model.save
|
||||||
llm_model.toggle_companion_user
|
llm_model.toggle_companion_user
|
||||||
render json: LlmModelSerializer.new(llm_model), status: :created
|
render json: { ai_persona: LlmModelSerializer.new(llm_model) }, status: :created
|
||||||
else
|
else
|
||||||
render_json_error llm_model
|
render_json_error LlmModelSerializer.new(llm_model)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
llm_model = LlmModel.find(params[:id])
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
|
||||||
if llm_model.seeded?
|
|
||||||
return render_json_error(I18n.t("discourse_ai.llm.cannot_edit_builtin"), status: 403)
|
|
||||||
end
|
|
||||||
|
|
||||||
if llm_model.update(ai_llm_params(updating: llm_model))
|
if llm_model.update(ai_llm_params(updating: llm_model))
|
||||||
llm_model.toggle_companion_user
|
llm_model.toggle_companion_user
|
||||||
render json: LlmModelSerializer.new(llm_model)
|
render json: LlmModelSerializer.new(llm_model)
|
||||||
else
|
else
|
||||||
render_json_error llm_model
|
render_json_error LlmModelSerializer.new(llm_model)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
llm_model = LlmModel.find(params[:id])
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
|
||||||
if llm_model.seeded?
|
|
||||||
return render_json_error(I18n.t("discourse_ai.llm.cannot_delete_builtin"), status: 403)
|
|
||||||
end
|
|
||||||
|
|
||||||
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
|
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
|
||||||
|
|
||||||
if !in_use_by.empty?
|
if !in_use_by.empty?
|
||||||
@@ -108,8 +97,6 @@ module DiscourseAi
|
|||||||
private
|
private
|
||||||
|
|
||||||
def ai_llm_params(updating: nil)
|
def ai_llm_params(updating: nil)
|
||||||
return {} if params[:ai_llm].blank?
|
|
||||||
|
|
||||||
permitted =
|
permitted =
|
||||||
params.require(:ai_llm).permit(
|
params.require(:ai_llm).permit(
|
||||||
:display_name,
|
:display_name,
|
||||||
@@ -119,30 +106,20 @@ module DiscourseAi
|
|||||||
:max_prompt_tokens,
|
:max_prompt_tokens,
|
||||||
:api_key,
|
:api_key,
|
||||||
:enabled_chat_bot,
|
:enabled_chat_bot,
|
||||||
:vision_enabled,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = updating ? updating.provider : permitted[:provider]
|
provider = updating ? updating.provider : permitted[:provider]
|
||||||
permit_url = provider != LlmModel::BEDROCK_PROVIDER_NAME
|
permit_url =
|
||||||
|
(updating && updating.url != LlmModel::RESERVED_VLLM_SRV_URL) ||
|
||||||
|
provider != LlmModel::BEDROCK_PROVIDER_NAME
|
||||||
|
|
||||||
new_url = params.dig(:ai_llm, :url)
|
permitted[:url] = params.dig(:ai_llm, :url) if permit_url
|
||||||
permitted[:url] = new_url if permit_url && new_url
|
|
||||||
|
|
||||||
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym)
|
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym, :fields).to_a
|
||||||
if extra_field_names.present?
|
received_prov_params = params.dig(:ai_llm, :provider_params)
|
||||||
received_prov_params =
|
permitted[:provider_params] = received_prov_params.slice(
|
||||||
params.dig(:ai_llm, :provider_params)&.slice(*extra_field_names.keys)
|
*extra_field_names,
|
||||||
|
).permit! if !extra_field_names.empty? && received_prov_params.present?
|
||||||
if received_prov_params.present?
|
|
||||||
received_prov_params.each do |pname, value|
|
|
||||||
if extra_field_names[pname.to_sym] == :checkbox
|
|
||||||
received_prov_params[pname] = ActiveModel::Type::Boolean.new.cast(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
permitted[:provider_params] = received_prov_params.permit!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
permitted
|
permitted
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ module DiscourseAi
|
|||||||
class AiPersonasController < ::Admin::AdminController
|
class AiPersonasController < ::Admin::AdminController
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :find_ai_persona, only: %i[show update destroy create_user]
|
before_action :find_ai_persona,
|
||||||
|
only: %i[show update destroy create_user indexing_status_check]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
ai_personas =
|
ai_personas =
|
||||||
@@ -40,7 +41,7 @@ module DiscourseAi
|
|||||||
def create
|
def create
|
||||||
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
|
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
|
||||||
if ai_persona.save
|
if ai_persona.save
|
||||||
RagDocumentFragment.link_target_and_uploads(ai_persona, attached_upload_ids)
|
RagDocumentFragment.link_persona_and_uploads(ai_persona, attached_upload_ids)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
|
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
|
||||||
@@ -58,11 +59,11 @@ module DiscourseAi
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
|
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
|
||||||
RagDocumentFragment.update_target_uploads(@ai_persona, attached_upload_ids)
|
RagDocumentFragment.update_persona_uploads(@ai_persona, attached_upload_ids)
|
||||||
|
|
||||||
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
else
|
else
|
||||||
render_json_error @ai_persona
|
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -70,97 +71,43 @@ module DiscourseAi
|
|||||||
if @ai_persona.destroy
|
if @ai_persona.destroy
|
||||||
head :no_content
|
head :no_content
|
||||||
else
|
else
|
||||||
render_json_error @ai_persona
|
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stream_reply
|
def upload_file
|
||||||
persona =
|
file = params[:file] || params[:files].first
|
||||||
AiPersona.find_by(name: params[:persona_name]) ||
|
|
||||||
AiPersona.find_by(id: params[:persona_id])
|
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.persona_not_found")) if persona.nil?
|
|
||||||
|
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.persona_disabled")) if !persona.enabled
|
if !SiteSetting.ai_embeddings_enabled?
|
||||||
|
raise Discourse::InvalidAccess.new("Embeddings not enabled")
|
||||||
if persona.default_llm.blank?
|
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.no_default_llm"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:query].blank?
|
validate_extension!(file.original_filename)
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.no_query_specified"))
|
validate_file_size!(file.tempfile.size)
|
||||||
end
|
|
||||||
|
|
||||||
if !persona.user_id
|
hijack do
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_persona"))
|
upload =
|
||||||
end
|
UploadCreator.new(
|
||||||
|
file.tempfile,
|
||||||
|
file.original_filename,
|
||||||
|
type: "discourse_ai_rag_upload",
|
||||||
|
skip_validations: true,
|
||||||
|
).create_for(current_user.id)
|
||||||
|
|
||||||
if !params[:username] && !params[:user_unique_id]
|
if upload.persisted?
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.no_user_specified"))
|
render json: UploadSerializer.new(upload)
|
||||||
end
|
else
|
||||||
|
render json: failed_json.merge(errors: upload.errors.full_messages), status: 422
|
||||||
user = nil
|
|
||||||
|
|
||||||
if params[:username]
|
|
||||||
user = User.find_by_username(params[:username])
|
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.user_not_found")) if user.nil?
|
|
||||||
elsif params[:user_unique_id]
|
|
||||||
user = stage_user
|
|
||||||
end
|
|
||||||
|
|
||||||
raise Discourse::NotFound if user.nil?
|
|
||||||
|
|
||||||
topic_id = params[:topic_id].to_i
|
|
||||||
topic = nil
|
|
||||||
|
|
||||||
if topic_id > 0
|
|
||||||
topic = Topic.find(topic_id)
|
|
||||||
|
|
||||||
if topic.topic_allowed_users.where(user_id: user.id).empty?
|
|
||||||
return render_json_error(I18n.t("discourse_ai.errors.user_not_allowed"))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
hijack = request.env["rack.hijack"]
|
def indexing_status_check
|
||||||
io = hijack.call
|
render json: RagDocumentFragment.indexing_status(@ai_persona, @ai_persona.uploads)
|
||||||
|
|
||||||
DiscourseAi::AiBot::ResponseHttpStreamer.queue_streamed_reply(
|
|
||||||
io: io,
|
|
||||||
persona: persona,
|
|
||||||
user: user,
|
|
||||||
topic: topic,
|
|
||||||
query: params[:query].to_s,
|
|
||||||
custom_instructions: params[:custom_instructions].to_s,
|
|
||||||
current_user: current_user,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
AI_STREAM_CONVERSATION_UNIQUE_ID = "ai-stream-conversation-unique-id"
|
|
||||||
|
|
||||||
def stage_user
|
|
||||||
unique_id = params[:user_unique_id].to_s
|
|
||||||
field = UserCustomField.find_by(name: AI_STREAM_CONVERSATION_UNIQUE_ID, value: unique_id)
|
|
||||||
|
|
||||||
if field
|
|
||||||
field.user
|
|
||||||
else
|
|
||||||
preferred_username = params[:preferred_username]
|
|
||||||
username = UserNameSuggester.suggest(preferred_username || unique_id)
|
|
||||||
|
|
||||||
user =
|
|
||||||
User.new(
|
|
||||||
username: username,
|
|
||||||
email: "#{SecureRandom.hex}@invalid.com",
|
|
||||||
staged: true,
|
|
||||||
active: false,
|
|
||||||
)
|
|
||||||
user.custom_fields[AI_STREAM_CONVERSATION_UNIQUE_ID] = unique_id
|
|
||||||
user.save!
|
|
||||||
user
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_ai_persona
|
def find_ai_persona
|
||||||
@ai_persona = AiPersona.find(params[:id])
|
@ai_persona = AiPersona.find(params[:id])
|
||||||
end
|
end
|
||||||
@@ -181,6 +128,7 @@ module DiscourseAi
|
|||||||
:temperature,
|
:temperature,
|
||||||
:default_llm,
|
:default_llm,
|
||||||
:user_id,
|
:user_id,
|
||||||
|
:mentionable,
|
||||||
:max_context_posts,
|
:max_context_posts,
|
||||||
:vision_enabled,
|
:vision_enabled,
|
||||||
:vision_max_pixels,
|
:vision_max_pixels,
|
||||||
@@ -188,13 +136,8 @@ module DiscourseAi
|
|||||||
:rag_chunk_overlap_tokens,
|
:rag_chunk_overlap_tokens,
|
||||||
:rag_conversation_chunks,
|
:rag_conversation_chunks,
|
||||||
:question_consolidator_llm,
|
:question_consolidator_llm,
|
||||||
:allow_chat_channel_mentions,
|
:allow_chat,
|
||||||
:allow_chat_direct_messages,
|
|
||||||
:allow_topic_mentions,
|
|
||||||
:allow_personal_messages,
|
|
||||||
:tool_details,
|
:tool_details,
|
||||||
:forced_tool_count,
|
|
||||||
:force_default_llm,
|
|
||||||
allowed_group_ids: [],
|
allowed_group_ids: [],
|
||||||
rag_uploads: [:id],
|
rag_uploads: [:id],
|
||||||
)
|
)
|
||||||
@@ -209,12 +152,40 @@ module DiscourseAi
|
|||||||
def permit_tools(tools)
|
def permit_tools(tools)
|
||||||
return [] if !tools.is_a?(Array)
|
return [] if !tools.is_a?(Array)
|
||||||
|
|
||||||
tools.filter_map do |tool, options, force_tool|
|
tools.filter_map do |tool, options|
|
||||||
break nil if !tool.is_a?(String)
|
break nil if !tool.is_a?(String)
|
||||||
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
||||||
|
|
||||||
# this is simpler from a storage perspective, 1 way to store tools
|
if options
|
||||||
[tool, options, !!force_tool]
|
[tool, options]
|
||||||
|
else
|
||||||
|
tool
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_extension!(filename)
|
||||||
|
extension = File.extname(filename)[1..-1] || ""
|
||||||
|
authorized_extensions = %w[txt md]
|
||||||
|
if !authorized_extensions.include?(extension)
|
||||||
|
raise Discourse::InvalidParameters.new(
|
||||||
|
I18n.t(
|
||||||
|
"upload.unauthorized",
|
||||||
|
authorized_extensions: authorized_extensions.join(" "),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_file_size!(filesize)
|
||||||
|
max_size_bytes = 20.megabytes
|
||||||
|
if filesize > max_size_bytes
|
||||||
|
raise Discourse::InvalidParameters.new(
|
||||||
|
I18n.t(
|
||||||
|
"upload.attachments.too_large_humanized",
|
||||||
|
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes),
|
||||||
|
),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module DiscourseAi
|
|||||||
class AiToolsController < ::Admin::AdminController
|
class AiToolsController < ::Admin::AdminController
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :find_ai_tool, only: %i[test show update destroy]
|
before_action :find_ai_tool, only: %i[show update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
ai_tools = AiTool.all
|
ai_tools = AiTool.all
|
||||||
@@ -21,7 +21,6 @@ module DiscourseAi
|
|||||||
ai_tool.created_by_id = current_user.id
|
ai_tool.created_by_id = current_user.id
|
||||||
|
|
||||||
if ai_tool.save
|
if ai_tool.save
|
||||||
RagDocumentFragment.link_target_and_uploads(ai_tool, attached_upload_ids)
|
|
||||||
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
||||||
else
|
else
|
||||||
render_json_error ai_tool
|
render_json_error ai_tool
|
||||||
@@ -30,7 +29,6 @@ module DiscourseAi
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @ai_tool.update(ai_tool_params)
|
if @ai_tool.update(ai_tool_params)
|
||||||
RagDocumentFragment.update_target_uploads(@ai_tool, attached_upload_ids)
|
|
||||||
render_serialized(@ai_tool, AiCustomToolSerializer)
|
render_serialized(@ai_tool, AiCustomToolSerializer)
|
||||||
else
|
else
|
||||||
render_json_error @ai_tool
|
render_json_error @ai_tool
|
||||||
@@ -46,13 +44,18 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test
|
def test
|
||||||
@ai_tool.assign_attributes(ai_tool_params) if params[:ai_tool]
|
if params[:id].present?
|
||||||
|
ai_tool = AiTool.find(params[:id])
|
||||||
|
else
|
||||||
|
ai_tool = AiTool.new(ai_tool_params)
|
||||||
|
end
|
||||||
|
|
||||||
parameters = params[:parameters].to_unsafe_h
|
parameters = params[:parameters].to_unsafe_h
|
||||||
|
|
||||||
# we need an llm so we have a tokenizer
|
# we need an llm so we have a tokenizer
|
||||||
# but will do without if none is available
|
# but will do without if none is available
|
||||||
llm = LlmModel.first&.to_llm
|
llm = LlmModel.first&.to_llm
|
||||||
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
|
runner = ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
|
||||||
result = runner.invoke
|
result = runner.invoke
|
||||||
|
|
||||||
if result.is_a?(Hash) && result[:error]
|
if result.is_a?(Hash) && result[:error]
|
||||||
@@ -68,28 +71,18 @@ module DiscourseAi
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def attached_upload_ids
|
|
||||||
params[:ai_tool][:rag_uploads].to_a.map { |h| h[:id] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_ai_tool
|
def find_ai_tool
|
||||||
@ai_tool = AiTool.find(params[:id].to_i)
|
@ai_tool = AiTool.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def ai_tool_params
|
def ai_tool_params
|
||||||
params
|
params.require(:ai_tool).permit(
|
||||||
.require(:ai_tool)
|
:name,
|
||||||
.permit(
|
:description,
|
||||||
:name,
|
:script,
|
||||||
:description,
|
:summary,
|
||||||
:script,
|
parameters: [:name, :type, :description, :required, :enum, enum_values: []],
|
||||||
:summary,
|
)
|
||||||
:rag_chunk_tokens,
|
|
||||||
:rag_chunk_overlap_tokens,
|
|
||||||
rag_uploads: [:id],
|
|
||||||
parameters: [:name, :type, :description, :required, enum: []],
|
|
||||||
)
|
|
||||||
.except(:rag_uploads)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseAi
|
|
||||||
module Admin
|
|
||||||
class RagDocumentFragmentsController < ::Admin::AdminController
|
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
|
||||||
|
|
||||||
def indexing_status_check
|
|
||||||
if params[:target_type] == "AiPersona"
|
|
||||||
@target = AiPersona.find(params[:target_id])
|
|
||||||
elsif params[:target_type] == "AiTool"
|
|
||||||
@target = AiTool.find(params[:target_id])
|
|
||||||
else
|
|
||||||
raise Discourse::InvalidParameters.new("Invalid target type")
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: RagDocumentFragment.indexing_status(@target, @target.uploads)
|
|
||||||
end
|
|
||||||
|
|
||||||
def upload_file
|
|
||||||
file = params[:file] || params[:files].first
|
|
||||||
|
|
||||||
if !SiteSetting.ai_embeddings_enabled?
|
|
||||||
raise Discourse::InvalidAccess.new("Embeddings not enabled")
|
|
||||||
end
|
|
||||||
|
|
||||||
validate_extension!(file.original_filename)
|
|
||||||
validate_file_size!(file.tempfile.size)
|
|
||||||
|
|
||||||
hijack do
|
|
||||||
upload =
|
|
||||||
UploadCreator.new(
|
|
||||||
file.tempfile,
|
|
||||||
file.original_filename,
|
|
||||||
type: "discourse_ai_rag_upload",
|
|
||||||
skip_validations: true,
|
|
||||||
).create_for(current_user.id)
|
|
||||||
|
|
||||||
if upload.persisted?
|
|
||||||
render json: UploadSerializer.new(upload)
|
|
||||||
else
|
|
||||||
render json: failed_json.merge(errors: upload.errors.full_messages), status: 422
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validate_extension!(filename)
|
|
||||||
extension = File.extname(filename)[1..-1] || ""
|
|
||||||
authorized_extensions = %w[txt md]
|
|
||||||
if !authorized_extensions.include?(extension)
|
|
||||||
raise Discourse::InvalidParameters.new(
|
|
||||||
I18n.t(
|
|
||||||
"upload.unauthorized",
|
|
||||||
authorized_extensions: authorized_extensions.join(" "),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_file_size!(filesize)
|
|
||||||
max_size_bytes = 20.megabytes
|
|
||||||
if filesize > max_size_bytes
|
|
||||||
raise Discourse::InvalidParameters.new(
|
|
||||||
I18n.t(
|
|
||||||
"upload.attachments.too_large_humanized",
|
|
||||||
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -6,14 +6,6 @@ module DiscourseAi
|
|||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
requires_login
|
requires_login
|
||||||
|
|
||||||
def show_debug_info_by_id
|
|
||||||
log = AiApiAuditLog.find(params[:id])
|
|
||||||
raise Discourse::NotFound if !log.topic
|
|
||||||
|
|
||||||
guardian.ensure_can_debug_ai_bot_conversation!(log.topic)
|
|
||||||
render json: AiApiAuditLogSerializer.new(log, root: false), status: 200
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_debug_info
|
def show_debug_info
|
||||||
post = Post.find(params[:post_id])
|
post = Post.find(params[:post_id])
|
||||||
guardian.ensure_can_debug_ai_bot_conversation!(post)
|
guardian.ensure_can_debug_ai_bot_conversation!(post)
|
||||||
|
|||||||
@@ -6,21 +6,10 @@ module DiscourseAi
|
|||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
requires_login
|
requires_login
|
||||||
before_action :ensure_can_request_suggestions
|
before_action :ensure_can_request_suggestions
|
||||||
before_action :rate_limiter_performed!
|
before_action :rate_limiter_performed!, except: %i[prompts]
|
||||||
|
|
||||||
include SecureUploadEndpointHelpers
|
include SecureUploadEndpointHelpers
|
||||||
|
|
||||||
RATE_LIMITS = {
|
|
||||||
"default" => {
|
|
||||||
amount: 6,
|
|
||||||
interval: 3.minutes,
|
|
||||||
},
|
|
||||||
"caption_image" => {
|
|
||||||
amount: 20,
|
|
||||||
interval: 1.minute,
|
|
||||||
},
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def suggest
|
def suggest
|
||||||
input = get_text_param!
|
input = get_text_param!
|
||||||
force_default_locale = params[:force_default_locale] || false
|
force_default_locale = params[:force_default_locale] || false
|
||||||
@@ -100,26 +89,18 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stream_suggestion
|
def explain
|
||||||
post_id = get_post_param!
|
post_id = get_post_param!
|
||||||
text = get_text_param!
|
term_to_explain = get_text_param!
|
||||||
post = Post.includes(:topic).find_by(id: post_id)
|
post = Post.includes(:topic).find_by(id: post_id)
|
||||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
|
||||||
|
|
||||||
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
|
||||||
raise Discourse::InvalidParameters.new(:post_id) unless post
|
raise Discourse::InvalidParameters.new(:post_id) unless post
|
||||||
|
|
||||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
|
||||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
|
||||||
end
|
|
||||||
|
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
:stream_post_helper,
|
:stream_post_helper,
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
text: text,
|
term_to_explain: term_to_explain,
|
||||||
prompt: prompt.name,
|
|
||||||
custom_prompt: params[:custom_prompt],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: { success: true }, status: 200
|
render json: { success: true }, status: 200
|
||||||
@@ -172,23 +153,13 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rate_limiter_performed!
|
def rate_limiter_performed!
|
||||||
action_rate_limit = RATE_LIMITS[action_name] || RATE_LIMITS["default"]
|
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
|
||||||
RateLimiter.new(
|
|
||||||
current_user,
|
|
||||||
"ai_assistant",
|
|
||||||
action_rate_limit[:amount],
|
|
||||||
action_rate_limit[:interval],
|
|
||||||
).performed!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_can_request_suggestions
|
def ensure_can_request_suggestions
|
||||||
allowed_groups =
|
if !current_user.in_any_groups?(SiteSetting.ai_helper_allowed_groups_map)
|
||||||
(
|
raise Discourse::InvalidAccess
|
||||||
SiteSetting.composer_ai_helper_allowed_groups_map |
|
end
|
||||||
SiteSetting.post_ai_helper_allowed_groups_map
|
|
||||||
)
|
|
||||||
|
|
||||||
raise Discourse::InvalidAccess if !current_user.in_any_groups?(allowed_groups)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseAi
|
|
||||||
module Discord
|
|
||||||
class BotController < ::ApplicationController
|
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
|
||||||
|
|
||||||
def interactions
|
|
||||||
# Request signature verification
|
|
||||||
begin
|
|
||||||
verify_request!
|
|
||||||
rescue Ed25519::VerifyError
|
|
||||||
return head :unauthorized
|
|
||||||
end
|
|
||||||
|
|
||||||
body = request.body.read
|
|
||||||
interaction = JSON.parse(body, object_class: OpenStruct)
|
|
||||||
|
|
||||||
if interaction.type == 1
|
|
||||||
# Respond to Discord PING request
|
|
||||||
render json: { type: 1 }
|
|
||||||
else
|
|
||||||
if !SiteSetting.ai_discord_allowed_guilds_map.include?(interaction.guild_id)
|
|
||||||
return head :forbidden
|
|
||||||
end
|
|
||||||
|
|
||||||
response = { type: 5, data: { content: "Searching..." } }
|
|
||||||
hijack { render json: response }
|
|
||||||
|
|
||||||
# Respond to Discord command
|
|
||||||
Jobs.enqueue(:stream_discord_reply, interaction: body)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def verify_request!
|
|
||||||
signature = request.headers["X-Signature-Ed25519"]
|
|
||||||
timestamp = request.headers["X-Signature-Timestamp"]
|
|
||||||
verify_key.verify([signature].pack("H*"), "#{timestamp}#{request.raw_post}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_key
|
|
||||||
Ed25519::VerifyKey.new([SiteSetting.ai_discord_app_public_key].pack("H*")).freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,12 +7,8 @@ module DiscourseAi
|
|||||||
|
|
||||||
SEMANTIC_SEARCH_TYPE = "semantic_search"
|
SEMANTIC_SEARCH_TYPE = "semantic_search"
|
||||||
|
|
||||||
MAX_HYDE_SEARCHES_PER_MINUTE = 4
|
|
||||||
MAX_SEARCHES_PER_MINUTE = 100
|
|
||||||
|
|
||||||
def search
|
def search
|
||||||
query = params[:q].to_s
|
query = params[:q].to_s
|
||||||
skip_hyde = params[:hyde].to_s.downcase == "false" || params[:hyde].to_s == "0"
|
|
||||||
|
|
||||||
if query.length < SiteSetting.min_search_term_length
|
if query.length < SiteSetting.min_search_term_length
|
||||||
raise Discourse::InvalidParameters.new(:q)
|
raise Discourse::InvalidParameters.new(:q)
|
||||||
@@ -29,25 +25,13 @@ module DiscourseAi
|
|||||||
|
|
||||||
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
||||||
|
|
||||||
if !skip_hyde && !semantic_search.cached_query?(query)
|
if !semantic_search.cached_query?(query)
|
||||||
RateLimiter.new(
|
RateLimiter.new(current_user, "semantic-search", 4, 1.minutes).performed!
|
||||||
current_user,
|
|
||||||
"semantic-search",
|
|
||||||
MAX_HYDE_SEARCHES_PER_MINUTE,
|
|
||||||
1.minutes,
|
|
||||||
).performed!
|
|
||||||
else
|
|
||||||
RateLimiter.new(
|
|
||||||
current_user,
|
|
||||||
"semantic-search-non-hyde",
|
|
||||||
MAX_SEARCHES_PER_MINUTE,
|
|
||||||
1.minutes,
|
|
||||||
).performed!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
semantic_search
|
semantic_search
|
||||||
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
|
.search_for_topics(query)
|
||||||
.each { |topic_post| grouped_results.add(topic_post) }
|
.each { |topic_post| grouped_results.add(topic_post) }
|
||||||
|
|
||||||
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
|
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
|
||||||
@@ -55,9 +39,6 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def quick_search
|
def quick_search
|
||||||
# this search function searches posts (vs: topics)
|
|
||||||
# it requires post embeddings and a reranker
|
|
||||||
# it will not perform a hyde expantion
|
|
||||||
query = params[:q].to_s
|
query = params[:q].to_s
|
||||||
|
|
||||||
if query.length < SiteSetting.min_search_term_length
|
if query.length < SiteSetting.min_search_term_length
|
||||||
|
|||||||
@@ -15,21 +15,29 @@ module DiscourseAi
|
|||||||
channel = ::Chat::Channel.find(params[:channel_id])
|
channel = ::Chat::Channel.find(params[:channel_id])
|
||||||
guardian.ensure_can_join_chat_channel!(channel)
|
guardian.ensure_can_join_chat_channel!(channel)
|
||||||
|
|
||||||
summarizer = DiscourseAi::Summarization.chat_channel_summary(channel, since)
|
strategy = DiscourseAi::Summarization.default_strategy
|
||||||
raise Discourse::NotFound.new unless summarizer
|
raise Discourse::NotFound.new unless strategy
|
||||||
|
|
||||||
guardian.ensure_can_request_summary!
|
guardian.ensure_can_request_summary!
|
||||||
|
|
||||||
RateLimiter.new(current_user, "channel_summary", 6, 5.minutes).performed!
|
RateLimiter.new(current_user, "channel_summary", 6, 5.minutes).performed!
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
strategy = DiscourseAi::Summarization::Strategies::ChatMessages.new(channel, since)
|
content = { content_title: channel.name }
|
||||||
|
|
||||||
|
content[:contents] = channel
|
||||||
|
.chat_messages
|
||||||
|
.where("chat_messages.created_at > ?", since.hours.ago)
|
||||||
|
.includes(:user)
|
||||||
|
.order(created_at: :asc)
|
||||||
|
.pluck(:id, :username_lower, :message)
|
||||||
|
.map { { id: _1, poster: _2, text: _3 } }
|
||||||
|
|
||||||
summarized_text =
|
summarized_text =
|
||||||
if strategy.targets_data.empty?
|
if content[:contents].empty?
|
||||||
I18n.t("discourse_ai.summarization.chat.no_targets")
|
I18n.t("discourse_ai.summarization.chat.no_targets")
|
||||||
else
|
else
|
||||||
summarizer.summarize(current_user)&.summarized_text
|
strategy.summarize(content, current_user).dig(:summary)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: { summary: summarized_text }
|
render json: { summary: summarized_text }
|
||||||
|
|||||||
@@ -14,29 +14,19 @@ module DiscourseAi
|
|||||||
RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user
|
RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user
|
||||||
|
|
||||||
opts = params.permit(:skip_age_check)
|
opts = params.permit(:skip_age_check)
|
||||||
skip_age_check = opts[:skip_age_check] == "true"
|
|
||||||
|
|
||||||
summarization_service = DiscourseAi::TopicSummarization.for(topic, current_user)
|
|
||||||
|
|
||||||
if params[:stream] && current_user
|
if params[:stream] && current_user
|
||||||
cached_summary = summarization_service.cached_summary
|
|
||||||
|
|
||||||
if cached_summary && !skip_age_check
|
|
||||||
render_serialized(cached_summary, AiTopicSummarySerializer)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
:stream_topic_ai_summary,
|
:stream_topic_ai_summary,
|
||||||
topic_id: topic.id,
|
topic_id: topic.id,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
skip_age_check: skip_age_check,
|
opts: opts.as_json,
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: success_json
|
render json: success_json
|
||||||
else
|
else
|
||||||
hijack do
|
hijack do
|
||||||
summary = summarization_service.summarize(skip_age_check: skip_age_check)
|
summary = DiscourseAi::TopicSummarization.summarize(topic, current_user, opts)
|
||||||
render_serialized(summary, AiTopicSummarySerializer)
|
render_serialized(summary, AiTopicSummarySerializer)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,24 +9,17 @@ module ::Jobs
|
|||||||
# TODO(roman): Add a way to automatically recover from errors, resulting in unindexed uploads.
|
# TODO(roman): Add a way to automatically recover from errors, resulting in unindexed uploads.
|
||||||
def execute(args)
|
def execute(args)
|
||||||
return if (upload = Upload.find_by(id: args[:upload_id])).nil?
|
return if (upload = Upload.find_by(id: args[:upload_id])).nil?
|
||||||
|
return if (ai_persona = AiPersona.find_by(id: args[:ai_persona_id])).nil?
|
||||||
target_type = args[:target_type]
|
|
||||||
target_id = args[:target_id]
|
|
||||||
|
|
||||||
return if !target_type || !target_id
|
|
||||||
|
|
||||||
target = target_type.constantize.find_by(id: target_id)
|
|
||||||
return if !target
|
|
||||||
|
|
||||||
truncation = DiscourseAi::Embeddings::Strategies::Truncation.new
|
truncation = DiscourseAi::Embeddings::Strategies::Truncation.new
|
||||||
vector_rep =
|
vector_rep =
|
||||||
DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation(truncation)
|
DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation(truncation)
|
||||||
|
|
||||||
tokenizer = vector_rep.tokenizer
|
tokenizer = vector_rep.tokenizer
|
||||||
chunk_tokens = target.rag_chunk_tokens
|
chunk_tokens = ai_persona.rag_chunk_tokens
|
||||||
overlap_tokens = target.rag_chunk_overlap_tokens
|
overlap_tokens = ai_persona.rag_chunk_overlap_tokens
|
||||||
|
|
||||||
fragment_ids = RagDocumentFragment.where(target: target, upload: upload).pluck(:id)
|
fragment_ids = RagDocumentFragment.where(ai_persona: ai_persona, upload: upload).pluck(:id)
|
||||||
|
|
||||||
# Check if this is the first time we process this upload.
|
# Check if this is the first time we process this upload.
|
||||||
if fragment_ids.empty?
|
if fragment_ids.empty?
|
||||||
@@ -46,7 +39,7 @@ module ::Jobs
|
|||||||
overlap_tokens: overlap_tokens,
|
overlap_tokens: overlap_tokens,
|
||||||
) do |chunk, metadata|
|
) do |chunk, metadata|
|
||||||
fragment_ids << RagDocumentFragment.create!(
|
fragment_ids << RagDocumentFragment.create!(
|
||||||
target: target,
|
ai_persona: ai_persona,
|
||||||
fragment: chunk,
|
fragment: chunk,
|
||||||
fragment_number: idx + 1,
|
fragment_number: idx + 1,
|
||||||
upload: upload,
|
upload: upload,
|
||||||
@@ -126,9 +119,7 @@ module ::Jobs
|
|||||||
|
|
||||||
while overlap_token_ids.present?
|
while overlap_token_ids.present?
|
||||||
begin
|
begin
|
||||||
padding = split_char
|
overlap = tokenizer.decode(overlap_token_ids) + split_char
|
||||||
padding = " " if padding.empty?
|
|
||||||
overlap = tokenizer.decode(overlap_token_ids) + padding
|
|
||||||
break if overlap.encoding == Encoding::UTF_8
|
break if overlap.encoding == Encoding::UTF_8
|
||||||
rescue StandardError
|
rescue StandardError
|
||||||
# it is possible that we truncated mid char
|
# it is possible that we truncated mid char
|
||||||
@@ -137,7 +128,7 @@ module ::Jobs
|
|||||||
end
|
end
|
||||||
|
|
||||||
# remove first word it is probably truncated
|
# remove first word it is probably truncated
|
||||||
overlap = overlap.split(/\s/, 2).last.to_s.lstrip
|
overlap = overlap.split(" ", 2).last
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ module ::Jobs
|
|||||||
fragments.map { |fragment| vector_rep.generate_representation_from(fragment) }
|
fragments.map { |fragment| vector_rep.generate_representation_from(fragment) }
|
||||||
|
|
||||||
last_fragment = fragments.last
|
last_fragment = fragments.last
|
||||||
target = last_fragment.target
|
ai_persona = last_fragment.ai_persona
|
||||||
upload = last_fragment.upload
|
upload = last_fragment.upload
|
||||||
|
|
||||||
indexing_status = RagDocumentFragment.indexing_status(target, [upload])[upload.id]
|
indexing_status = RagDocumentFragment.indexing_status(ai_persona, [upload])[upload.id]
|
||||||
RagDocumentFragment.publish_status(upload, indexing_status)
|
RagDocumentFragment.publish_status(upload, indexing_status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ::Jobs
|
|
||||||
class HotTopicsGistBatch < ::Jobs::Base
|
|
||||||
def execute(_args)
|
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
|
||||||
return if !SiteSetting.ai_summarization_enabled
|
|
||||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
|
||||||
|
|
||||||
Topic
|
|
||||||
.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id")
|
|
||||||
.order("topic_hot_scores.score DESC")
|
|
||||||
.limit(SiteSetting.ai_summarize_max_hot_topics_gists_per_batch)
|
|
||||||
.each do |topic|
|
|
||||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
|
||||||
gist = summarizer.existing_summary
|
|
||||||
|
|
||||||
summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Jobs
|
|
||||||
class StreamDiscordReply < ::Jobs::Base
|
|
||||||
sidekiq_options retry: false
|
|
||||||
|
|
||||||
def execute(args)
|
|
||||||
interaction = args[:interaction]
|
|
||||||
|
|
||||||
if SiteSetting.ai_discord_search_mode == "persona"
|
|
||||||
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
|
|
||||||
else
|
|
||||||
DiscourseAi::Discord::Bot::Search.new(interaction).handle_interaction!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,35 +7,27 @@ module Jobs
|
|||||||
def execute(args)
|
def execute(args)
|
||||||
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
return unless args[:text]
|
return unless args[:term_to_explain]
|
||||||
|
|
||||||
topic = post.topic
|
topic = post.topic
|
||||||
reply_to = post.reply_to_post
|
reply_to = post.reply_to_post
|
||||||
|
|
||||||
return unless user.guardian.can_see?(post)
|
return unless user.guardian.can_see?(post)
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
prompt = CompletionPrompt.enabled_by_name("explain")
|
||||||
|
|
||||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
input = <<~TEXT
|
||||||
prompt.custom_instruction = args[:custom_prompt]
|
<term>#{args[:term_to_explain]}</term>
|
||||||
end
|
|
||||||
|
|
||||||
if prompt.name == "explain"
|
|
||||||
input = <<~TEXT
|
|
||||||
<term>#{args[:text]}</term>
|
|
||||||
<context>#{post.raw}</context>
|
<context>#{post.raw}</context>
|
||||||
<topic>#{topic.title}</topic>
|
<topic>#{topic.title}</topic>
|
||||||
#{reply_to ? "<replyTo>#{reply_to.raw}</replyTo>" : nil}
|
#{reply_to ? "<replyTo>#{reply_to.raw}</replyTo>" : nil}
|
||||||
TEXT
|
TEXT
|
||||||
else
|
|
||||||
input = args[:text]
|
|
||||||
end
|
|
||||||
|
|
||||||
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
||||||
prompt,
|
prompt,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
"/discourse-ai/ai-helper/stream_suggestion/#{post.id}",
|
"/discourse-ai/ai-helper/explain/#{post.id}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,25 +8,25 @@ module Jobs
|
|||||||
return unless topic = Topic.find_by(id: args[:topic_id])
|
return unless topic = Topic.find_by(id: args[:topic_id])
|
||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
|
|
||||||
strategy = DiscourseAi::Summarization.topic_summary(topic)
|
strategy = DiscourseAi::Summarization.default_strategy
|
||||||
return if strategy.nil? || !Guardian.new(user).can_see_summary?(topic)
|
return if strategy.nil? || !Guardian.new(user).can_see_summary?(topic)
|
||||||
|
|
||||||
guardian = Guardian.new(user)
|
guardian = Guardian.new(user)
|
||||||
return unless guardian.can_see?(topic)
|
return unless guardian.can_see?(topic)
|
||||||
|
|
||||||
skip_age_check = !!args[:skip_age_check]
|
opts = args[:opts] || {}
|
||||||
|
|
||||||
streamed_summary = +""
|
streamed_summary = +""
|
||||||
start = Time.now
|
start = Time.now
|
||||||
|
|
||||||
summary =
|
summary =
|
||||||
DiscourseAi::TopicSummarization
|
DiscourseAi::TopicSummarization
|
||||||
.new(strategy, user)
|
.new(strategy)
|
||||||
.summarize(skip_age_check: skip_age_check) do |partial_summary|
|
.summarize(topic, user, opts) do |partial_summary|
|
||||||
streamed_summary << partial_summary
|
streamed_summary << partial_summary
|
||||||
|
|
||||||
# Throttle updates.
|
# Throttle updates.
|
||||||
if (Time.now - start > 0.3) || Rails.env.test?
|
if (Time.now - start > 0.5) || Rails.env.test?
|
||||||
payload = { done: false, ai_topic_summary: { summarized_text: streamed_summary } }
|
payload = { done: false, ai_topic_summary: { summarized_text: streamed_summary } }
|
||||||
|
|
||||||
publish_update(topic, user, payload)
|
publish_update(topic, user, payload)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ::Jobs
|
||||||
|
class UpdateAiBotPmTitle < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return unless bot_user = User.find_by(id: args[:bot_user_id])
|
||||||
|
return unless bot = DiscourseAi::AiBot::Bot.as(bot_user, model: args[:model])
|
||||||
|
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
||||||
|
|
||||||
|
return unless post.topic.custom_fields[DiscourseAi::AiBot::EntryPoint::REQUIRE_TITLE_UPDATE]
|
||||||
|
|
||||||
|
DiscourseAi::AiBot::Playground.new(bot).title_playground(post)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ::Jobs
|
|
||||||
class UpdateHotTopicGist < ::Jobs::Base
|
|
||||||
sidekiq_options retry: false
|
|
||||||
|
|
||||||
def execute(args)
|
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
|
||||||
return if !SiteSetting.ai_summarization_enabled
|
|
||||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
|
||||||
|
|
||||||
topic = Topic.find_by(id: args[:topic_id])
|
|
||||||
return if topic.blank?
|
|
||||||
|
|
||||||
return if !TopicHotScore.where(topic: topic).exists?
|
|
||||||
|
|
||||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
|
||||||
gist = summarizer.existing_summary
|
|
||||||
return if gist.blank?
|
|
||||||
return if !gist.outdated
|
|
||||||
|
|
||||||
summarizer.force_summarize(Discourse.system_user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -35,6 +35,8 @@ module Jobs
|
|||||||
|
|
||||||
rebaked += populate_topic_embeddings(vector_rep, topics)
|
rebaked += populate_topic_embeddings(vector_rep, topics)
|
||||||
|
|
||||||
|
vector_rep.consider_indexing
|
||||||
|
|
||||||
return if rebaked >= limit
|
return if rebaked >= limit
|
||||||
|
|
||||||
# Then, we'll try to backfill embeddings for topics that have outdated
|
# Then, we'll try to backfill embeddings for topics that have outdated
|
||||||
@@ -52,12 +54,9 @@ module Jobs
|
|||||||
# Finally, we'll try to backfill embeddings for topics that have outdated
|
# Finally, we'll try to backfill embeddings for topics that have outdated
|
||||||
# embeddings due to edits or new replies. Here we only do 10% of the limit
|
# embeddings due to edits or new replies. Here we only do 10% of the limit
|
||||||
relation =
|
relation =
|
||||||
topics
|
topics.where("#{table_name}.updated_at < ?", 7.days.ago).limit((limit - rebaked) / 10)
|
||||||
.where("#{table_name}.updated_at < ?", 6.hours.ago)
|
|
||||||
.where("#{table_name}.updated_at < topics.updated_at")
|
|
||||||
.limit((limit - rebaked) / 10)
|
|
||||||
|
|
||||||
populate_topic_embeddings(vector_rep, relation, force: true)
|
populate_topic_embeddings(vector_rep, relation)
|
||||||
|
|
||||||
return if rebaked >= limit
|
return if rebaked >= limit
|
||||||
|
|
||||||
@@ -80,6 +79,8 @@ module Jobs
|
|||||||
rebaked += 1
|
rebaked += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vector_rep.consider_indexing
|
||||||
|
|
||||||
return if rebaked >= limit
|
return if rebaked >= limit
|
||||||
|
|
||||||
# Then, we'll try to backfill embeddings for posts that have outdated
|
# Then, we'll try to backfill embeddings for posts that have outdated
|
||||||
@@ -114,12 +115,9 @@ module Jobs
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def populate_topic_embeddings(vector_rep, topics, force: false)
|
def populate_topic_embeddings(vector_rep, topics)
|
||||||
done = 0
|
done = 0
|
||||||
|
ids = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL").pluck("topics.id")
|
||||||
topics = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL") if !force
|
|
||||||
|
|
||||||
ids = topics.pluck("topics.id")
|
|
||||||
|
|
||||||
ids.each do |id|
|
ids.each do |id|
|
||||||
topic = Topic.find_by(id: id)
|
topic = Topic.find_by(id: id)
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ::Jobs
|
|
||||||
class SummariesBackfill < ::Jobs::Scheduled
|
|
||||||
every 5.minutes
|
|
||||||
cluster_concurrency 1
|
|
||||||
|
|
||||||
def execute(_args)
|
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
|
||||||
return if !SiteSetting.ai_summarization_enabled
|
|
||||||
return if SiteSetting.ai_summary_backfill_maximum_topics_per_hour.zero?
|
|
||||||
|
|
||||||
system_user = Discourse.system_user
|
|
||||||
|
|
||||||
complete_t = AiSummary.summary_types[:complete]
|
|
||||||
backfill_candidates(complete_t)
|
|
||||||
.limit(current_budget(complete_t))
|
|
||||||
.each do |topic|
|
|
||||||
DiscourseAi::Summarization.topic_summary(topic).force_summarize(system_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
gist_t = AiSummary.summary_types[:gist]
|
|
||||||
backfill_candidates(gist_t)
|
|
||||||
.limit(current_budget(gist_t))
|
|
||||||
.each { |topic| DiscourseAi::Summarization.topic_gist(topic).force_summarize(system_user) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def backfill_candidates(summary_type)
|
|
||||||
Topic
|
|
||||||
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
|
|
||||||
.joins(<<~SQL)
|
|
||||||
LEFT OUTER JOIN ai_summaries ais ON
|
|
||||||
topics.id = ais.target_id AND
|
|
||||||
ais.target_type = 'Topic' AND
|
|
||||||
ais.summary_type = '#{summary_type}'
|
|
||||||
SQL
|
|
||||||
.where(
|
|
||||||
"ais.id IS NULL OR UPPER(ais.content_range) < topics.highest_post_number + 1",
|
|
||||||
) # (1..1) gets stored ad (1..2).
|
|
||||||
.order("ais.created_at DESC NULLS FIRST, topics.last_posted_at DESC")
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_budget(type)
|
|
||||||
# Split budget in 12 intervals, but make sure is at least one.
|
|
||||||
base_budget = SiteSetting.ai_summary_backfill_maximum_topics_per_hour
|
|
||||||
limit_per_job = [base_budget, 12].max / 12
|
|
||||||
|
|
||||||
used_budget =
|
|
||||||
AiSummary.system.where("created_at > ?", 1.hour.ago).where(summary_type: type).count
|
|
||||||
|
|
||||||
current_budget = [(base_budget - used_budget), limit_per_job].min
|
|
||||||
return 0 if current_budget < 0
|
|
||||||
|
|
||||||
current_budget
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -12,15 +12,6 @@ class AiApiAuditLog < ActiveRecord::Base
|
|||||||
Vllm = 5
|
Vllm = 5
|
||||||
Cohere = 6
|
Cohere = 6
|
||||||
Ollama = 7
|
Ollama = 7
|
||||||
SambaNova = 8
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_log_id
|
|
||||||
self.class.where("id > ?", id).where(topic_id: topic_id).order(id: :asc).pluck(:id).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def prev_log_id
|
|
||||||
self.class.where("id < ?", id).where(topic_id: topic_id).order(id: :desc).pluck(:id).first
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,4 +32,4 @@ end
|
|||||||
# post_id :integer
|
# post_id :integer
|
||||||
# feature_name :string(255)
|
# feature_name :string(255)
|
||||||
# language_model :string(255)
|
# language_model :string(255)
|
||||||
# feature_context :jsonb
|
#
|
||||||
|
|||||||
+67
-91
@@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AiPersona < ActiveRecord::Base
|
class AiPersona < ActiveRecord::Base
|
||||||
# TODO remove this line 01-1-2025
|
# TODO remove this line 01-11-2024
|
||||||
self.ignored_columns = %i[commands allow_chat mentionable]
|
self.ignored_columns = [:commands]
|
||||||
|
|
||||||
# places a hard limit, so per site we cache a maximum of 500 classes
|
# places a hard limit, so per site we cache a maximum of 500 classes
|
||||||
MAX_PERSONAS_PER_SITE = 500
|
MAX_PERSONAS_PER_SITE = 500
|
||||||
@@ -20,8 +20,6 @@ class AiPersona < ActiveRecord::Base
|
|||||||
validates :rag_chunk_tokens, numericality: { greater_than: 0, maximum: 50_000 }
|
validates :rag_chunk_tokens, numericality: { greater_than: 0, maximum: 50_000 }
|
||||||
validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 }
|
validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 }
|
||||||
validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 }
|
validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 }
|
||||||
validates :forced_tool_count, numericality: { greater_than: -2, maximum: 100_000 }
|
|
||||||
has_many :rag_document_fragments, dependent: :destroy, as: :target
|
|
||||||
|
|
||||||
belongs_to :created_by, class_name: "User"
|
belongs_to :created_by, class_name: "User"
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
@@ -29,7 +27,12 @@ class AiPersona < ActiveRecord::Base
|
|||||||
has_many :upload_references, as: :target, dependent: :destroy
|
has_many :upload_references, as: :target, dependent: :destroy
|
||||||
has_many :uploads, through: :upload_references
|
has_many :uploads, through: :upload_references
|
||||||
|
|
||||||
|
has_many :rag_document_fragment, dependent: :destroy
|
||||||
|
|
||||||
|
has_many :rag_document_fragments, through: :ai_persona_rag_document_fragments
|
||||||
|
|
||||||
before_destroy :ensure_not_system
|
before_destroy :ensure_not_system
|
||||||
|
|
||||||
before_update :regenerate_rag_fragments
|
before_update :regenerate_rag_fragments
|
||||||
|
|
||||||
def self.persona_cache
|
def self.persona_cache
|
||||||
@@ -52,47 +55,30 @@ class AiPersona < ActiveRecord::Base
|
|||||||
persona_cache[:persona_users] ||= AiPersona
|
persona_cache[:persona_users] ||= AiPersona
|
||||||
.where(enabled: true)
|
.where(enabled: true)
|
||||||
.joins(:user)
|
.joins(:user)
|
||||||
.map do |persona|
|
.pluck(
|
||||||
|
"ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable, allow_chat",
|
||||||
|
)
|
||||||
|
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable, allow_chat|
|
||||||
{
|
{
|
||||||
id: persona.id,
|
id: id,
|
||||||
user_id: persona.user_id,
|
user_id: user_id,
|
||||||
username: persona.user.username_lower,
|
username: username,
|
||||||
allowed_group_ids: persona.allowed_group_ids,
|
allowed_group_ids: allowed_group_ids,
|
||||||
default_llm: persona.default_llm,
|
default_llm: default_llm,
|
||||||
force_default_llm: persona.force_default_llm,
|
mentionable: mentionable,
|
||||||
allow_chat_channel_mentions: persona.allow_chat_channel_mentions,
|
allow_chat: allow_chat,
|
||||||
allow_chat_direct_messages: persona.allow_chat_direct_messages,
|
|
||||||
allow_topic_mentions: persona.allow_topic_mentions,
|
|
||||||
allow_personal_messages: persona.allow_personal_messages,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
if user
|
if user
|
||||||
persona_users.select { |persona_user| user.in_any_groups?(persona_user[:allowed_group_ids]) }
|
persona_users.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
|
||||||
else
|
else
|
||||||
persona_users
|
persona_users
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.allowed_modalities(
|
def self.allowed_chat(user: nil)
|
||||||
user: nil,
|
personas = persona_cache[:allowed_chat] ||= persona_users.select { |u| u[:allow_chat] }
|
||||||
allow_chat_channel_mentions: false,
|
|
||||||
allow_chat_direct_messages: false,
|
|
||||||
allow_topic_mentions: false,
|
|
||||||
allow_personal_messages: false
|
|
||||||
)
|
|
||||||
index =
|
|
||||||
"modality-#{allow_chat_channel_mentions}-#{allow_chat_direct_messages}-#{allow_topic_mentions}-#{allow_personal_messages}"
|
|
||||||
|
|
||||||
personas =
|
|
||||||
persona_cache[index.to_sym] ||= persona_users.select do |persona|
|
|
||||||
next true if allow_chat_channel_mentions && persona[:allow_chat_channel_mentions]
|
|
||||||
next true if allow_chat_direct_messages && persona[:allow_chat_direct_messages]
|
|
||||||
next true if allow_topic_mentions && persona[:allow_topic_mentions]
|
|
||||||
next true if allow_personal_messages && persona[:allow_personal_messages]
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
if user
|
if user
|
||||||
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
|
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
|
||||||
else
|
else
|
||||||
@@ -100,6 +86,18 @@ class AiPersona < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.mentionables(user: nil)
|
||||||
|
all_mentionables =
|
||||||
|
persona_cache[:mentionables] ||= persona_users.select do |mentionable|
|
||||||
|
mentionable[:mentionable]
|
||||||
|
end
|
||||||
|
if user
|
||||||
|
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
|
||||||
|
else
|
||||||
|
all_mentionables
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
after_commit :bump_cache
|
after_commit :bump_cache
|
||||||
|
|
||||||
def bump_cache
|
def bump_cache
|
||||||
@@ -118,11 +116,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
vision_max_pixels
|
vision_max_pixels
|
||||||
rag_conversation_chunks
|
rag_conversation_chunks
|
||||||
question_consolidator_llm
|
question_consolidator_llm
|
||||||
allow_chat_channel_mentions
|
allow_chat
|
||||||
allow_chat_direct_messages
|
|
||||||
allow_topic_mentions
|
|
||||||
allow_personal_messages
|
|
||||||
force_default_llm
|
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
allowed_group_ids
|
allowed_group_ids
|
||||||
@@ -137,8 +131,6 @@ class AiPersona < ActiveRecord::Base
|
|||||||
instance_attributes[attr] = value
|
instance_attributes[attr] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
instance_attributes[:username] = user&.username_lower
|
|
||||||
|
|
||||||
if persona_class
|
if persona_class
|
||||||
instance_attributes.each do |key, value|
|
instance_attributes.each do |key, value|
|
||||||
# description/name are localized
|
# description/name are localized
|
||||||
@@ -148,23 +140,17 @@ class AiPersona < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
options = {}
|
options = {}
|
||||||
force_tool_use = []
|
|
||||||
|
|
||||||
tools =
|
tools =
|
||||||
self.tools.filter_map do |element|
|
self.tools.filter_map do |element|
|
||||||
klass = nil
|
klass = nil
|
||||||
|
|
||||||
element = [element] if element.is_a?(String)
|
if element.is_a?(String) && element.start_with?("custom-")
|
||||||
|
custom_tool_id = element.split("-", 2).last.to_i
|
||||||
inner_name, current_options, should_force_tool_use =
|
|
||||||
element.is_a?(Array) ? element : [element, nil]
|
|
||||||
|
|
||||||
if inner_name.start_with?("custom-")
|
|
||||||
custom_tool_id = inner_name.split("-", 2).last.to_i
|
|
||||||
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
||||||
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
|
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
inner_name, current_options = element.is_a?(Array) ? element : [element, nil]
|
||||||
inner_name = inner_name.gsub("Tool", "")
|
inner_name = inner_name.gsub("Tool", "")
|
||||||
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
||||||
|
|
||||||
@@ -173,10 +159,9 @@ class AiPersona < ActiveRecord::Base
|
|||||||
options[klass] = current_options if current_options
|
options[klass] = current_options if current_options
|
||||||
rescue StandardError
|
rescue StandardError
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
force_tool_use << klass if should_force_tool_use
|
klass
|
||||||
klass
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ai_persona_id = self.id
|
ai_persona_id = self.id
|
||||||
@@ -196,8 +181,6 @@ class AiPersona < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
define_method(:tools) { tools }
|
define_method(:tools) { tools }
|
||||||
define_method(:force_tool_use) { force_tool_use }
|
|
||||||
define_method(:forced_tool_count) { @ai_persona&.forced_tool_count }
|
|
||||||
define_method(:options) { options }
|
define_method(:options) { options }
|
||||||
define_method(:temperature) { @ai_persona&.temperature }
|
define_method(:temperature) { @ai_persona&.temperature }
|
||||||
define_method(:top_p) { @ai_persona&.top_p }
|
define_method(:top_p) { @ai_persona&.top_p }
|
||||||
@@ -247,17 +230,14 @@ class AiPersona < ActiveRecord::Base
|
|||||||
|
|
||||||
def regenerate_rag_fragments
|
def regenerate_rag_fragments
|
||||||
if rag_chunk_tokens_changed? || rag_chunk_overlap_tokens_changed?
|
if rag_chunk_tokens_changed? || rag_chunk_overlap_tokens_changed?
|
||||||
RagDocumentFragment.where(target: self).delete_all
|
RagDocumentFragment.where(ai_persona: self).delete_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def chat_preconditions
|
def chat_preconditions
|
||||||
if (
|
if allow_chat && !default_llm
|
||||||
allow_chat_channel_mentions || allow_chat_direct_messages || allow_topic_mentions ||
|
|
||||||
force_default_llm
|
|
||||||
) && !default_llm
|
|
||||||
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
|
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -281,36 +261,32 @@ end
|
|||||||
#
|
#
|
||||||
# Table name: ai_personas
|
# Table name: ai_personas
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# name :string(100) not null
|
# name :string(100) not null
|
||||||
# description :string(2000) not null
|
# description :string(2000) not null
|
||||||
# system_prompt :string(10000000) not null
|
# system_prompt :string(10000000) not null
|
||||||
# allowed_group_ids :integer default([]), not null, is an Array
|
# allowed_group_ids :integer default([]), not null, is an Array
|
||||||
# created_by_id :integer
|
# created_by_id :integer
|
||||||
# enabled :boolean default(TRUE), not null
|
# enabled :boolean default(TRUE), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# system :boolean default(FALSE), not null
|
# system :boolean default(FALSE), not null
|
||||||
# priority :boolean default(FALSE), not null
|
# priority :boolean default(FALSE), not null
|
||||||
# temperature :float
|
# temperature :float
|
||||||
# top_p :float
|
# top_p :float
|
||||||
# user_id :integer
|
# user_id :integer
|
||||||
# default_llm :text
|
# mentionable :boolean default(FALSE), not null
|
||||||
# max_context_posts :integer
|
# default_llm :text
|
||||||
# vision_enabled :boolean default(FALSE), not null
|
# max_context_posts :integer
|
||||||
# vision_max_pixels :integer default(1048576), not null
|
# vision_enabled :boolean default(FALSE), not null
|
||||||
# rag_chunk_tokens :integer default(374), not null
|
# vision_max_pixels :integer default(1048576), not null
|
||||||
# rag_chunk_overlap_tokens :integer default(10), not null
|
# rag_chunk_tokens :integer default(374), not null
|
||||||
# rag_conversation_chunks :integer default(10), not null
|
# rag_chunk_overlap_tokens :integer default(10), not null
|
||||||
# question_consolidator_llm :text
|
# rag_conversation_chunks :integer default(10), not null
|
||||||
# tool_details :boolean default(TRUE), not null
|
# question_consolidator_llm :text
|
||||||
# tools :json not null
|
# allow_chat :boolean default(FALSE), not null
|
||||||
# forced_tool_count :integer default(-1), not null
|
# tool_details :boolean default(TRUE), not null
|
||||||
# allow_chat_channel_mentions :boolean default(FALSE), not null
|
# tools :json not null
|
||||||
# allow_chat_direct_messages :boolean default(FALSE), not null
|
|
||||||
# allow_topic_mentions :boolean default(FALSE), not null
|
|
||||||
# allow_personal_messages :boolean default(TRUE), not null
|
|
||||||
# force_default_llm :boolean default(FALSE), not null
|
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user