Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba091aa8b |
@@ -1,5 +1,3 @@
|
||||
< 3.5.0.beta7-dev: cd14b0c0bee0cf63c59b64b6f7213e31a37f11a7
|
||||
< 3.5.0.beta6-dev: 3e74eea1e5e3143888d67a8d8a11206df214dc24
|
||||
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
||||
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
||||
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
|
||||
|
||||
@@ -6,4 +6,5 @@ evals/log
|
||||
evals/cases
|
||||
config/eval-llms.local.yml
|
||||
# this gets rid of search results from ag, ripgrep, etc
|
||||
tokenizers/
|
||||
public/ai-share/highlight.min.js
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ GEM
|
||||
racc
|
||||
prettier_print (1.2.1)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack (3.1.14)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.74.0)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# **Discourse AI** Plugin
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
|
||||
|
||||
**Plugin Summary**
|
||||
|
||||
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiAgentsEdit extends DiscourseRoute {
|
||||
async model(params) {
|
||||
const allAgents = this.modelFor(
|
||||
"adminPlugins.show.discourse-ai-agents"
|
||||
);
|
||||
const id = parseInt(params.id, 10);
|
||||
return allAgents.findBy("id", id);
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
controller.set(
|
||||
"allAgents",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-agents")
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
import { AUTO_GROUPS } from "discourse/lib/constants";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute {
|
||||
export default class AdminPluginsShowDiscourseAiAgentsNew extends DiscourseRoute {
|
||||
async model() {
|
||||
const record = this.store.createRecord("ai-persona");
|
||||
const record = this.store.createRecord("ai-agent");
|
||||
record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
|
||||
record.set("rag_uploads", []);
|
||||
// these match the defaults on the table
|
||||
record.set("rag_chunk_tokens", 374);
|
||||
record.set("rag_chunk_overlap_tokens", 10);
|
||||
record.set("rag_conversation_chunks", 10);
|
||||
record.set("allow_personal_messages", true);
|
||||
record.set("allow_agentl_messages", true);
|
||||
record.set("tool_details", false);
|
||||
return record;
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRou
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
controller.set(
|
||||
"allPersonas",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-personas")
|
||||
"allAgents",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-agents")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class DiscourseAiAiAgentsRoute extends DiscourseRoute {
|
||||
model() {
|
||||
return this.store.findAll("ai-agent");
|
||||
}
|
||||
}
|
||||
+1
-9
@@ -1,4 +1,3 @@
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
@@ -13,7 +12,7 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo
|
||||
|
||||
const { site_settings } = await ajax("/admin/config/site_settings.json", {
|
||||
data: {
|
||||
filter_area: `ai-features/${currentFeature.module_name}`,
|
||||
filter_area: `ai-features/${currentFeature.ref}`,
|
||||
plugin: "discourse-ai",
|
||||
category: "discourse_ai",
|
||||
},
|
||||
@@ -25,11 +24,4 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo
|
||||
|
||||
return currentFeature;
|
||||
}
|
||||
|
||||
@action
|
||||
willTransition() {
|
||||
// site settings may amend if a feature is enabled or disabled, so refresh the model
|
||||
// even on back button
|
||||
this.router.refresh("adminPlugins.show.discourse-ai-features");
|
||||
}
|
||||
}
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiPersonasEdit extends DiscourseRoute {
|
||||
async model(params) {
|
||||
const allPersonas = this.modelFor(
|
||||
"adminPlugins.show.discourse-ai-personas"
|
||||
);
|
||||
const id = parseInt(params.id, 10);
|
||||
return allPersonas.findBy("id", id);
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
controller.set(
|
||||
"allPersonas",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-personas")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class DiscourseAiAiPersonasRoute extends DiscourseRoute {
|
||||
model() {
|
||||
return this.store.findAll("ai-persona");
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<AiAgentListEditor
|
||||
@agents={{this.allAgents}}
|
||||
@currentAgent={{this.model}}
|
||||
/>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<AiAgentListEditor @agents={{this.model}} />
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<AiAgentListEditor
|
||||
@agents={{this.allAgents}}
|
||||
@currentAgent={{this.model}}
|
||||
/>
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import { gt } from "truth-helpers";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
class extends Component {
|
||||
@service adminPluginNavManager;
|
||||
|
||||
get tableHeaders() {
|
||||
const prefix = "discourse_ai.features.list.header";
|
||||
return [
|
||||
i18n(`${prefix}.name`),
|
||||
i18n(`${prefix}.agent`),
|
||||
i18n(`${prefix}.groups`),
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
get configuredFeatures() {
|
||||
return this.args.model.filter(
|
||||
(feature) => feature.enable_setting.value === true
|
||||
);
|
||||
}
|
||||
|
||||
get unconfiguredFeatures() {
|
||||
return this.args.model.filter(
|
||||
(feature) => feature.enable_setting.value === false
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
|
||||
@label={{i18n "discourse_ai.features.short_title"}}
|
||||
/>
|
||||
<section class="ai-feature-list admin-detail">
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n "discourse_ai.features.short_title"}}
|
||||
@descriptionLabel={{i18n "discourse_ai.features.description"}}
|
||||
@learnMoreUrl="todo"
|
||||
/>
|
||||
|
||||
{{#if (gt this.configuredFeatures.length 0)}}
|
||||
<div class="ai-feature-list__configured-features">
|
||||
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>
|
||||
|
||||
<table class="d-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{#each this.tableHeaders as |header|}}
|
||||
<th>{{header}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.configuredFeatures as |feature|}}
|
||||
<tr
|
||||
class="ai-feature-list__row d-admin-row__content"
|
||||
data-feature-name={{feature.name}}
|
||||
>
|
||||
<td class="d-admin-row__overview ai-feature-list__row-item">
|
||||
<span class="ai-feature-list__row-item-name">
|
||||
<strong>
|
||||
{{feature.name}}
|
||||
</strong>
|
||||
</span>
|
||||
<span class="ai-feature-list__row-item-description">
|
||||
{{feature.description}}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__agent"
|
||||
>
|
||||
<DButton
|
||||
class="btn-flat btn-small ai-feature-list__row-item-agent"
|
||||
@translatedLabel={{feature.agent.name}}
|
||||
@route="adminPlugins.show.discourse-ai-agents.edit"
|
||||
@routeModels={{feature.agent.id}}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
|
||||
>
|
||||
{{#if (gt feature.agent.allowed_groups.length 0)}}
|
||||
<ul class="ai-feature-list__row-item-groups">
|
||||
{{#each feature.agent.allowed_groups as |group|}}
|
||||
<li>{{group.name}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row_controls">
|
||||
<DButton
|
||||
class="btn-small edit"
|
||||
@label="discourse_ai.features.list.edit"
|
||||
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||
@routeModels={{feature.id}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.unconfiguredFeatures.length 0)}}
|
||||
<div class="ai-feature-list__unconfigured-features">
|
||||
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>
|
||||
|
||||
<table class="d-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.unconfiguredFeatures as |feature|}}
|
||||
<tr class="ai-feature-list__row d-admin-row__content">
|
||||
<td class="d-admin-row__overview ai-feature-list__row-item">
|
||||
<span class="ai-feature-list__row-item-name">
|
||||
<strong>
|
||||
{{feature.name}}
|
||||
</strong>
|
||||
</span>
|
||||
<span class="ai-feature-list__row-item-description">
|
||||
{{feature.description}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="d-admin-row_controls">
|
||||
<DButton
|
||||
class="btn-small"
|
||||
@label="discourse_ai.features.list.set_up"
|
||||
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||
@routeModels={{feature.id}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
}
|
||||
);
|
||||
-1
@@ -1 +0,0 @@
|
||||
<AiFeatures @features={{this.model}} />
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
<AiPersonaListEditor
|
||||
@personas={{this.allPersonas}}
|
||||
@currentPersona={{this.model}}
|
||||
/>
|
||||
-1
@@ -1 +0,0 @@
|
||||
<AiPersonaListEditor @personas={{this.model}} />
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
<AiPersonaListEditor
|
||||
@personas={{this.allPersonas}}
|
||||
@currentPersona={{this.model}}
|
||||
/>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<section class="ai-agent-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<section class="ai-agent-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module Admin
|
||||
class AiAgentsController < ::Admin::AdminController
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
before_action :find_ai_agent, only: %i[edit update destroy create_user]
|
||||
|
||||
def index
|
||||
ai_agents =
|
||||
AiAgent.ordered.map do |agent|
|
||||
# we use a special serializer here cause names and descriptions are
|
||||
# localized for system agents
|
||||
LocalizedAiAgentSerializer.new(agent, root: false)
|
||||
end
|
||||
tools =
|
||||
DiscourseAi::Agents::Agent.all_available_tools.map do |tool|
|
||||
AiToolSerializer.new(tool, root: false)
|
||||
end
|
||||
AiTool
|
||||
.where(enabled: true)
|
||||
.each do |tool|
|
||||
tools << {
|
||||
id: "custom-#{tool.id}",
|
||||
name:
|
||||
I18n.t(
|
||||
"discourse_ai.tools.custom_name",
|
||||
name: tool.name.capitalize,
|
||||
tool_name: tool.tool_name,
|
||||
),
|
||||
}
|
||||
end
|
||||
llms =
|
||||
DiscourseAi::Configuration::LlmEnumerator.values_for_serialization(
|
||||
allowed_seeded_llm_ids: SiteSetting.ai_bot_allowed_seeded_models_map,
|
||||
)
|
||||
render json: {
|
||||
ai_agents: ai_agents,
|
||||
meta: {
|
||||
tools: tools,
|
||||
llms: llms,
|
||||
settings: {
|
||||
rag_images_enabled: SiteSetting.ai_rag_images_enabled,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def edit
|
||||
render json: LocalizedAiAgentSerializer.new(@ai_agent)
|
||||
end
|
||||
|
||||
def create
|
||||
ai_agent = AiAgent.new(ai_agent_params.except(:rag_uploads))
|
||||
if ai_agent.save
|
||||
RagDocumentFragment.link_target_and_uploads(ai_agent, attached_upload_ids)
|
||||
|
||||
render json: {
|
||||
ai_agent: LocalizedAiAgentSerializer.new(ai_agent, root: false),
|
||||
},
|
||||
status: :created
|
||||
else
|
||||
render_json_error ai_agent
|
||||
end
|
||||
end
|
||||
|
||||
def create_user
|
||||
user = @ai_agent.create_user!
|
||||
render json: BasicUserSerializer.new(user, root: "user")
|
||||
end
|
||||
|
||||
def update
|
||||
if @ai_agent.update(ai_agent_params.except(:rag_uploads))
|
||||
RagDocumentFragment.update_target_uploads(@ai_agent, attached_upload_ids)
|
||||
|
||||
render json: LocalizedAiAgentSerializer.new(@ai_agent, root: false)
|
||||
else
|
||||
render_json_error @ai_agent
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @ai_agent.destroy
|
||||
head :no_content
|
||||
else
|
||||
render_json_error @ai_agent
|
||||
end
|
||||
end
|
||||
|
||||
def stream_reply
|
||||
agent =
|
||||
AiAgent.find_by(name: params[:agent_name]) ||
|
||||
AiAgent.find_by(id: params[:agent_id])
|
||||
return render_json_error(I18n.t("discourse_ai.errors.agent_not_found")) if agent.nil?
|
||||
|
||||
return render_json_error(I18n.t("discourse_ai.errors.agent_disabled")) if !agent.enabled
|
||||
|
||||
if agent.default_llm.blank?
|
||||
return render_json_error(I18n.t("discourse_ai.errors.no_default_llm"))
|
||||
end
|
||||
|
||||
if params[:query].blank?
|
||||
return render_json_error(I18n.t("discourse_ai.errors.no_query_specified"))
|
||||
end
|
||||
|
||||
if !agent.user_id
|
||||
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_agent"))
|
||||
end
|
||||
|
||||
if !params[:username] && !params[:user_unique_id]
|
||||
return render_json_error(I18n.t("discourse_ai.errors.no_user_specified"))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
hijack = request.env["rack.hijack"]
|
||||
io = hijack.call
|
||||
|
||||
DiscourseAi::AiBot::ResponseHttpStreamer.queue_streamed_reply(
|
||||
io: io,
|
||||
agent: agent,
|
||||
user: user,
|
||||
topic: topic,
|
||||
query: params[:query].to_s,
|
||||
custom_instructions: params[:custom_instructions].to_s,
|
||||
current_user: current_user,
|
||||
)
|
||||
end
|
||||
|
||||
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_agent
|
||||
@ai_agent = AiAgent.find(params[:id])
|
||||
end
|
||||
|
||||
def attached_upload_ids
|
||||
ai_agent_params[:rag_uploads].to_a.map { |h| h[:id] }
|
||||
end
|
||||
|
||||
def ai_agent_params
|
||||
permitted =
|
||||
params.require(:ai_agent).permit(
|
||||
:name,
|
||||
:description,
|
||||
:enabled,
|
||||
:system_prompt,
|
||||
:priority,
|
||||
:top_p,
|
||||
:temperature,
|
||||
:default_llm_id,
|
||||
:user_id,
|
||||
:max_context_posts,
|
||||
:vision_enabled,
|
||||
:vision_max_pixels,
|
||||
:rag_chunk_tokens,
|
||||
:rag_chunk_overlap_tokens,
|
||||
:rag_conversation_chunks,
|
||||
:rag_llm_model_id,
|
||||
:question_consolidator_llm_id,
|
||||
:allow_chat_channel_mentions,
|
||||
:allow_chat_direct_messages,
|
||||
:allow_topic_mentions,
|
||||
:allow_agentl_messages,
|
||||
:tool_details,
|
||||
:forced_tool_count,
|
||||
:force_default_llm,
|
||||
allowed_group_ids: [],
|
||||
rag_uploads: [:id],
|
||||
)
|
||||
|
||||
if tools = params.dig(:ai_agent, :tools)
|
||||
permitted[:tools] = permit_tools(tools)
|
||||
end
|
||||
|
||||
if response_format = params.dig(:ai_agent, :response_format)
|
||||
permitted[:response_format] = permit_response_format(response_format)
|
||||
end
|
||||
|
||||
if examples = params.dig(:ai_agent, :examples)
|
||||
permitted[:examples] = permit_examples(examples)
|
||||
end
|
||||
|
||||
permitted
|
||||
end
|
||||
|
||||
def permit_tools(tools)
|
||||
return [] if !tools.is_a?(Array)
|
||||
|
||||
tools.filter_map do |tool, options, force_tool|
|
||||
break nil if !tool.is_a?(String)
|
||||
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
||||
|
||||
# this is simpler from a storage perspective, 1 way to store tools
|
||||
[tool, options, !!force_tool]
|
||||
end
|
||||
end
|
||||
|
||||
def permit_response_format(response_format)
|
||||
return [] if !response_format.is_a?(Array)
|
||||
|
||||
response_format.map do |element|
|
||||
if element && element.is_a?(ActionController::Parameters)
|
||||
element.permit!
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def permit_examples(examples)
|
||||
return [] if !examples.is_a?(Array)
|
||||
|
||||
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -40,7 +40,6 @@ module DiscourseAi
|
||||
embedding_def = EmbeddingDefinition.new(ai_embeddings_params)
|
||||
|
||||
if embedding_def.save
|
||||
log_ai_embedding_creation(embedding_def)
|
||||
render json: AiEmbeddingDefinitionSerializer.new(embedding_def), status: :created
|
||||
else
|
||||
render_json_error embedding_def
|
||||
@@ -56,10 +55,7 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
|
||||
initial_attributes = embedding_def.attributes.dup
|
||||
|
||||
if embedding_def.update(ai_embeddings_params.except(:dimensions))
|
||||
log_ai_embedding_update(embedding_def, initial_attributes)
|
||||
render json: AiEmbeddingDefinitionSerializer.new(embedding_def)
|
||||
else
|
||||
render_json_error embedding_def
|
||||
@@ -79,16 +75,7 @@ module DiscourseAi
|
||||
return render_json_error(I18n.t("discourse_ai.embeddings.delete_failed"), status: 409)
|
||||
end
|
||||
|
||||
embedding_details = {
|
||||
embedding_id: embedding_def.id,
|
||||
display_name: embedding_def.display_name,
|
||||
provider: embedding_def.provider,
|
||||
dimensions: embedding_def.dimensions,
|
||||
subject: embedding_def.display_name,
|
||||
}
|
||||
|
||||
if embedding_def.destroy
|
||||
log_ai_embedding_deletion(embedding_details)
|
||||
head :no_content
|
||||
else
|
||||
render_json_error embedding_def
|
||||
@@ -141,60 +128,6 @@ module DiscourseAi
|
||||
|
||||
permitted
|
||||
end
|
||||
|
||||
def ai_embeddings_logger_fields
|
||||
{
|
||||
display_name: {
|
||||
},
|
||||
provider: {
|
||||
},
|
||||
dimensions: {
|
||||
},
|
||||
url: {
|
||||
},
|
||||
tokenizer_class: {
|
||||
},
|
||||
max_sequence_length: {
|
||||
},
|
||||
embed_prompt: {
|
||||
type: :large_text,
|
||||
},
|
||||
search_prompt: {
|
||||
type: :large_text,
|
||||
},
|
||||
matryoshka_dimensions: {
|
||||
},
|
||||
api_key: {
|
||||
type: :sensitive,
|
||||
},
|
||||
# JSON fields should be tracked as simple changes
|
||||
json_fields: [:provider_params],
|
||||
}
|
||||
end
|
||||
|
||||
def log_ai_embedding_creation(embedding_def)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
entity_details = { embedding_id: embedding_def.id, subject: embedding_def.display_name }
|
||||
logger.log_creation("embedding", embedding_def, ai_embeddings_logger_fields, entity_details)
|
||||
end
|
||||
|
||||
def log_ai_embedding_update(embedding_def, initial_attributes)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
entity_details = { embedding_id: embedding_def.id, subject: embedding_def.display_name }
|
||||
|
||||
logger.log_update(
|
||||
"embedding",
|
||||
embedding_def,
|
||||
initial_attributes,
|
||||
ai_embeddings_logger_fields,
|
||||
entity_details,
|
||||
)
|
||||
end
|
||||
|
||||
def log_ai_embedding_deletion(embedding_details)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
logger.log_deletion("embedding", embedding_details)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,60 +6,30 @@ module DiscourseAi
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
def index
|
||||
render json: serialize_modules(DiscourseAi::Configuration::Module.all)
|
||||
render json: serialize_features(DiscourseAi::Features.features)
|
||||
end
|
||||
|
||||
def edit
|
||||
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
|
||||
|
||||
a_module = DiscourseAi::Configuration::Module.find_by(id: params[:id].to_i)
|
||||
|
||||
render json: serialize_module(a_module)
|
||||
render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize_modules(modules)
|
||||
modules.map { |a_module| serialize_module(a_module) }
|
||||
end
|
||||
|
||||
def serialize_module(a_module)
|
||||
return nil if a_module.blank?
|
||||
|
||||
{
|
||||
id: a_module.id,
|
||||
module_name: a_module.name,
|
||||
module_enabled: a_module.enabled?,
|
||||
features: a_module.features.map { |f| serialize_feature(f) },
|
||||
}
|
||||
def serialize_features(features)
|
||||
features.map { |feature| feature.merge(agent: serialize_agent(feature[:agent])) }
|
||||
end
|
||||
|
||||
def serialize_feature(feature)
|
||||
{
|
||||
name: feature.name,
|
||||
personas: feature.persona_ids.map { |id| serialize_persona(persona_id_obj_hash[id]) },
|
||||
llm_models:
|
||||
feature.llm_models.map do |llm_model|
|
||||
{ id: llm_model.id, name: llm_model.display_name }
|
||||
end,
|
||||
enabled: feature.enabled?,
|
||||
}
|
||||
return nil if feature.blank?
|
||||
|
||||
feature.merge(agent: serialize_agent(feature[:agent]))
|
||||
end
|
||||
|
||||
def serialize_persona(persona)
|
||||
return nil if persona.blank?
|
||||
def serialize_agent(agent)
|
||||
return nil if agent.blank?
|
||||
|
||||
serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def persona_id_obj_hash
|
||||
@persona_id_obj_hash ||=
|
||||
begin
|
||||
ids = DiscourseAi::Configuration::Feature.all.map(&:persona_ids).flatten.uniq
|
||||
AiPersona.where(id: ids).index_by(&:id)
|
||||
end
|
||||
serialize_data(agent, AiFeaturesAgentSerializer, root: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,6 @@ module DiscourseAi
|
||||
|
||||
if llm_model.save
|
||||
llm_model.toggle_companion_user
|
||||
log_llm_model_creation(llm_model)
|
||||
render json: LlmModelSerializer.new(llm_model), status: :created
|
||||
else
|
||||
render_json_error llm_model
|
||||
@@ -57,10 +56,6 @@ module DiscourseAi
|
||||
def update
|
||||
llm_model = LlmModel.find(params[:id])
|
||||
|
||||
# Capture initial state for logging
|
||||
initial_attributes = llm_model.attributes.dup
|
||||
initial_quotas = llm_model.llm_quotas.map(&:attributes)
|
||||
|
||||
if params[:ai_llm].key?(:llm_quotas)
|
||||
if quota_params
|
||||
existing_quota_group_ids = llm_model.llm_quotas.pluck(:group_id)
|
||||
@@ -86,7 +81,6 @@ module DiscourseAi
|
||||
|
||||
if llm_model.update(ai_llm_params(updating: llm_model))
|
||||
llm_model.toggle_companion_user
|
||||
log_llm_model_update(llm_model, initial_attributes, initial_quotas)
|
||||
render json: LlmModelSerializer.new(llm_model)
|
||||
else
|
||||
render_json_error llm_model
|
||||
@@ -115,20 +109,11 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
|
||||
# Capture model details for logging before destruction
|
||||
model_details = {
|
||||
model_id: llm_model.id,
|
||||
display_name: llm_model.display_name,
|
||||
name: llm_model.name,
|
||||
provider: llm_model.provider,
|
||||
}
|
||||
|
||||
# Clean up companion users
|
||||
llm_model.enabled_chat_bot = false
|
||||
llm_model.toggle_companion_user
|
||||
|
||||
if llm_model.destroy
|
||||
log_llm_model_deletion(model_details)
|
||||
head :no_content
|
||||
else
|
||||
render_json_error llm_model
|
||||
@@ -205,89 +190,6 @@ module DiscourseAi
|
||||
|
||||
permitted
|
||||
end
|
||||
|
||||
def ai_llm_logger_fields
|
||||
{
|
||||
display_name: {
|
||||
},
|
||||
name: {
|
||||
},
|
||||
provider: {
|
||||
},
|
||||
tokenizer: {
|
||||
},
|
||||
url: {
|
||||
},
|
||||
max_prompt_tokens: {
|
||||
},
|
||||
max_output_tokens: {
|
||||
},
|
||||
enabled_chat_bot: {
|
||||
},
|
||||
vision_enabled: {
|
||||
},
|
||||
api_key: {
|
||||
type: :sensitive,
|
||||
},
|
||||
input_cost: {
|
||||
},
|
||||
output_cost: {
|
||||
},
|
||||
# JSON fields should be tracked as simple changes
|
||||
json_fields: [:provider_params],
|
||||
}
|
||||
end
|
||||
|
||||
def log_llm_model_creation(llm_model)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
entity_details = { model_id: llm_model.id, subject: llm_model.display_name }
|
||||
|
||||
# Add quota information as a special case
|
||||
if llm_model.llm_quotas.any?
|
||||
entity_details[:quotas] = llm_model
|
||||
.llm_quotas
|
||||
.map do |quota|
|
||||
"Group #{quota.group_id}: #{quota.max_tokens} tokens, #{quota.max_usages} usages, #{quota.duration_seconds}s"
|
||||
end
|
||||
.join("; ")
|
||||
end
|
||||
|
||||
logger.log_creation("llm_model", llm_model, ai_llm_logger_fields, entity_details)
|
||||
end
|
||||
|
||||
def log_llm_model_update(llm_model, initial_attributes, initial_quotas)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
entity_details = { model_id: llm_model.id, subject: llm_model.display_name }
|
||||
|
||||
# Track quota changes separately as they're a special case
|
||||
current_quotas = llm_model.llm_quotas.reload.map(&:attributes)
|
||||
if initial_quotas != current_quotas
|
||||
initial_quota_summary =
|
||||
initial_quotas
|
||||
.map { |q| "Group #{q["group_id"]}: #{q["max_tokens"]} tokens" }
|
||||
.join("; ")
|
||||
current_quota_summary =
|
||||
current_quotas
|
||||
.map { |q| "Group #{q["group_id"]}: #{q["max_tokens"]} tokens" }
|
||||
.join("; ")
|
||||
entity_details[:quotas_changed] = true
|
||||
entity_details[:quotas] = "#{initial_quota_summary} → #{current_quota_summary}"
|
||||
end
|
||||
|
||||
logger.log_update(
|
||||
"llm_model",
|
||||
llm_model,
|
||||
initial_attributes,
|
||||
ai_llm_logger_fields,
|
||||
entity_details,
|
||||
)
|
||||
end
|
||||
|
||||
def log_llm_model_deletion(model_details)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
model_details[:subject] = model_details[:display_name]
|
||||
logger.log_deletion("llm_model", model_details)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,17 +10,6 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def update
|
||||
initial_settings = AiModerationSetting.spam
|
||||
|
||||
initial_data = {
|
||||
custom_instructions: initial_settings&.data&.dig("custom_instructions"),
|
||||
llm_model_id: initial_settings&.llm_model_id,
|
||||
ai_persona_id: initial_settings&.ai_persona_id,
|
||||
}
|
||||
|
||||
initial_custom_instructions = initial_settings&.data&.dig("custom_instructions")
|
||||
initial_llm_model_id = initial_settings&.llm_model_id
|
||||
|
||||
updated_params = {}
|
||||
if allowed_params.key?(:llm_model_id)
|
||||
llm_model_id = updated_params[:llm_model_id] = allowed_params[:llm_model_id]
|
||||
@@ -36,22 +25,6 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if allowed_params.key?(:ai_persona_id)
|
||||
updated_params[:ai_persona_id] = allowed_params[:ai_persona_id]
|
||||
persona = AiPersona.find_by(id: allowed_params[:ai_persona_id])
|
||||
if persona.nil? ||
|
||||
persona.response_format.to_a.none? { |rf|
|
||||
rf["key"] == "spam" && rf["type"] == "boolean"
|
||||
}
|
||||
return(
|
||||
render_json_error(
|
||||
I18n.t("discourse_ai.llm.configuration.invalid_persona_response_format"),
|
||||
status: 422,
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
updated_params[:data] = {
|
||||
custom_instructions: allowed_params[:custom_instructions],
|
||||
} if allowed_params.key?(:custom_instructions)
|
||||
@@ -63,8 +36,6 @@ module DiscourseAi
|
||||
else
|
||||
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
|
||||
end
|
||||
|
||||
log_ai_spam_update(initial_data, allowed_params)
|
||||
end
|
||||
|
||||
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
|
||||
@@ -142,44 +113,8 @@ module DiscourseAi
|
||||
|
||||
private
|
||||
|
||||
def log_ai_spam_update(initial_data, params)
|
||||
changes_to_log = {}
|
||||
|
||||
initial_llm_model_id = initial_data[:llm_model_id]
|
||||
if params.key?(:llm_model_id) && initial_llm_model_id.to_s != params[:llm_model_id].to_s
|
||||
old_model_name =
|
||||
LlmModel.find_by(id: initial_llm_model_id)&.display_name || initial_llm_model_id
|
||||
new_model_name =
|
||||
LlmModel.find_by(id: params[:llm_model_id])&.display_name || params[:llm_model_id]
|
||||
|
||||
changes_to_log[:llm_model_id] = "#{old_model_name} → #{new_model_name}"
|
||||
end
|
||||
|
||||
initial_custom_instructions = initial_data[:custom_instructions]
|
||||
if params.key?(:custom_instructions) &&
|
||||
initial_custom_instructions != params[:custom_instructions]
|
||||
changes_to_log[:custom_instructions] = params[:custom_instructions]
|
||||
end
|
||||
|
||||
initial_ai_persona_id = initial_data[:ai_persona_id]
|
||||
if params.key?(:ai_persona_id) && initial_ai_persona_id.to_s != params[:ai_persona_id].to_s
|
||||
old_persona_name =
|
||||
AiPersona.find_by(id: initial_ai_persona_id)&.name || initial_ai_persona_id
|
||||
new_persona_name =
|
||||
AiPersona.find_by(id: params[:ai_persona_id])&.name || params[:ai_persona_id]
|
||||
|
||||
changes_to_log[:ai_persona_id] = "#{old_persona_name} → #{new_persona_name}"
|
||||
end
|
||||
|
||||
if changes_to_log.present?
|
||||
changes_to_log[:subject] = I18n.t("discourse_ai.spam_detection.logging_subject")
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
logger.log_custom("update_ai_spam_settings", changes_to_log)
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_params
|
||||
params.permit(:is_enabled, :llm_model_id, :custom_instructions, :ai_persona_id)
|
||||
params.permit(:is_enabled, :llm_model_id, :custom_instructions)
|
||||
end
|
||||
|
||||
def spam_config
|
||||
|
||||
@@ -5,7 +5,7 @@ module DiscourseAi
|
||||
class AiToolsController < ::Admin::AdminController
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
before_action :find_ai_tool, only: %i[test edit update destroy export]
|
||||
before_action :find_ai_tool, only: %i[test edit update destroy]
|
||||
|
||||
def index
|
||||
ai_tools = AiTool.all
|
||||
@@ -25,58 +25,15 @@ module DiscourseAi
|
||||
|
||||
if ai_tool.save
|
||||
RagDocumentFragment.link_target_and_uploads(ai_tool, attached_upload_ids)
|
||||
log_ai_tool_creation(ai_tool)
|
||||
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
||||
else
|
||||
render_json_error ai_tool
|
||||
end
|
||||
end
|
||||
|
||||
def export
|
||||
response.headers[
|
||||
"Content-Disposition"
|
||||
] = "attachment; filename=\"#{@ai_tool.tool_name}.json\""
|
||||
render_serialized(@ai_tool, AiCustomToolSerializer)
|
||||
end
|
||||
|
||||
def import
|
||||
existing_tool = AiTool.find_by(tool_name: ai_tool_params[:tool_name])
|
||||
force_update = params[:force].present? && params[:force].to_s.downcase == "true"
|
||||
|
||||
if existing_tool && !force_update
|
||||
return(
|
||||
render_json_error "Tool with tool_name '#{ai_tool_params[:tool_name]}' already exists. Use force=true to overwrite.",
|
||||
status: :conflict
|
||||
)
|
||||
end
|
||||
|
||||
if existing_tool && force_update
|
||||
initial_attributes = existing_tool.attributes.dup
|
||||
if existing_tool.update(ai_tool_params)
|
||||
log_ai_tool_update(existing_tool, initial_attributes)
|
||||
render_serialized(existing_tool, AiCustomToolSerializer)
|
||||
else
|
||||
render_json_error existing_tool
|
||||
end
|
||||
else
|
||||
ai_tool = AiTool.new(ai_tool_params)
|
||||
ai_tool.created_by_id = current_user.id
|
||||
|
||||
if ai_tool.save
|
||||
log_ai_tool_creation(ai_tool)
|
||||
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
||||
else
|
||||
render_json_error ai_tool
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
initial_attributes = @ai_tool.attributes.dup
|
||||
|
||||
if @ai_tool.update(ai_tool_params)
|
||||
RagDocumentFragment.update_target_uploads(@ai_tool, attached_upload_ids)
|
||||
log_ai_tool_update(@ai_tool, initial_attributes)
|
||||
render_serialized(@ai_tool, AiCustomToolSerializer)
|
||||
else
|
||||
render_json_error @ai_tool
|
||||
@@ -84,15 +41,7 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def destroy
|
||||
tool_logger_details = {
|
||||
tool_id: @ai_tool.id,
|
||||
name: @ai_tool.name,
|
||||
tool_name: @ai_tool.tool_name,
|
||||
subject: @ai_tool.name,
|
||||
}
|
||||
|
||||
if @ai_tool.destroy
|
||||
log_ai_tool_deletion(tool_logger_details)
|
||||
head :no_content
|
||||
else
|
||||
render_json_error @ai_tool
|
||||
@@ -147,60 +96,6 @@ module DiscourseAi
|
||||
)
|
||||
.except(:rag_uploads)
|
||||
end
|
||||
|
||||
def ai_tool_logger_fields
|
||||
{
|
||||
name: {
|
||||
},
|
||||
tool_name: {
|
||||
},
|
||||
description: {
|
||||
},
|
||||
summary: {
|
||||
},
|
||||
enabled: {
|
||||
},
|
||||
rag_chunk_tokens: {
|
||||
},
|
||||
rag_chunk_overlap_tokens: {
|
||||
},
|
||||
rag_llm_model_id: {
|
||||
},
|
||||
script: {
|
||||
type: :large_text,
|
||||
},
|
||||
parameters: {
|
||||
type: :large_text,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def log_ai_tool_creation(ai_tool)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
|
||||
entity_details = { tool_id: ai_tool.id, subject: ai_tool.name }
|
||||
entity_details[:parameter_count] = ai_tool.parameters.size if ai_tool.parameters.present?
|
||||
|
||||
logger.log_creation("tool", ai_tool, ai_tool_logger_fields, entity_details)
|
||||
end
|
||||
|
||||
def log_ai_tool_update(ai_tool, initial_attributes)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
entity_details = { tool_id: ai_tool.id, subject: ai_tool.name }
|
||||
|
||||
logger.log_update(
|
||||
"tool",
|
||||
ai_tool,
|
||||
initial_attributes,
|
||||
ai_tool_logger_fields,
|
||||
entity_details,
|
||||
)
|
||||
end
|
||||
|
||||
def log_ai_tool_deletion(tool_details)
|
||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||
logger.log_deletion("tool", tool_details)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,8 @@ module DiscourseAi
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
def indexing_status_check
|
||||
if params[:target_type] == "AiPersona"
|
||||
@target = AiPersona.find(params[:target_id])
|
||||
if params[:target_type] == "AiAgent"
|
||||
@target = AiAgent.find(params[:target_id])
|
||||
elsif params[:target_type] == "AiTool"
|
||||
@target = AiTool.find(params[:target_id])
|
||||
else
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
class ArtifactKeyValuesController < ::ApplicationController
|
||||
requires_plugin DiscourseAi::PLUGIN_NAME
|
||||
before_action :ensure_logged_in, only: %i[set destroy]
|
||||
before_action :find_artifact
|
||||
|
||||
PER_PAGE_MAX = 100
|
||||
|
||||
def index
|
||||
page = index_params[:page].to_i
|
||||
page = 1 if page < 1
|
||||
per_page = index_params[:per_page].to_i
|
||||
per_page = PER_PAGE_MAX if per_page < 1 || per_page > PER_PAGE_MAX
|
||||
|
||||
query = build_index_query
|
||||
|
||||
total_count = query.count
|
||||
key_values =
|
||||
query
|
||||
.includes(:user)
|
||||
.order(:user_id, :key, :created_at)
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page + 1)
|
||||
|
||||
has_more = key_values.length > per_page
|
||||
key_values = key_values.first(per_page) if has_more
|
||||
|
||||
render json: {
|
||||
key_values:
|
||||
ActiveModel::ArraySerializer.new(
|
||||
key_values,
|
||||
each_serializer: AiArtifactKeyValueSerializer,
|
||||
keys_only: params[:keys_only] == "true",
|
||||
).as_json,
|
||||
has_more: has_more,
|
||||
total_count: total_count,
|
||||
users:
|
||||
key_values
|
||||
.map { |kv| kv.user }
|
||||
.uniq
|
||||
.map { |u| BasicUserSerializer.new(u, root: nil).as_json },
|
||||
}
|
||||
end
|
||||
|
||||
def destroy
|
||||
if params[:key].blank?
|
||||
render json: { error: "Key parameter is required" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
key_value = @artifact.key_values.find_by(user_id: current_user.id, key: params[:key])
|
||||
|
||||
if key_value.nil?
|
||||
render json: { error: "Key not found" }, status: :not_found
|
||||
elsif key_value.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: key_value.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def set
|
||||
key_value =
|
||||
@artifact.key_values.find_or_initialize_by(
|
||||
user: current_user,
|
||||
key: key_value_params[:key],
|
||||
)
|
||||
|
||||
key_value.assign_attributes(key_value_params.except(:key))
|
||||
|
||||
if key_value.save
|
||||
render json: AiArtifactKeyValueSerializer.new(key_value).as_json
|
||||
else
|
||||
render json: { errors: key_value.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def key_value_params
|
||||
params.permit(:key, :value, :public)
|
||||
end
|
||||
|
||||
def index_params
|
||||
@index_params ||= params.permit(:page, :per_page, :key, :keys_only, :all_users)
|
||||
end
|
||||
|
||||
def build_index_query
|
||||
query = @artifact.key_values
|
||||
|
||||
query =
|
||||
if current_user&.admin?
|
||||
query
|
||||
elsif current_user
|
||||
query.where("user_id = ? OR public = true", current_user.id)
|
||||
else
|
||||
query.where(public: true)
|
||||
end
|
||||
|
||||
query = query.where("key = ?", index_params[:key]) if index_params[:key].present?
|
||||
|
||||
if index_params[:all_users].to_s != "true"
|
||||
if current_user
|
||||
query = query.where(user_id: current_user.id)
|
||||
else
|
||||
query = query.where("1 = 0")
|
||||
end
|
||||
end
|
||||
|
||||
query
|
||||
end
|
||||
|
||||
def find_artifact
|
||||
@artifact = AiArtifact.find_by(id: params[:artifact_id])
|
||||
raise Discourse::NotFound if !@artifact
|
||||
raise Discourse::NotFound if [email protected]? && guardian.anonymous?
|
||||
raise Discourse::NotFound if [email protected]? && !guardian.can_see?(@artifact.post)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user