From b863ddc94bf03e1868845e10ba744bef1f68841d Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 27 Jun 2024 17:27:40 +1000 Subject: [PATCH] FEATURE: custom user defined tools (#677) Introduces custom AI tools functionality. 1. Why it was added: The PR adds the ability to create, manage, and use custom AI tools within the Discourse AI system. This feature allows for more flexibility and extensibility in the AI capabilities of the platform. 2. What it does: - Introduces a new `AiTool` model for storing custom AI tools - Adds CRUD (Create, Read, Update, Delete) operations for AI tools - Implements a tool runner system for executing custom tool scripts - Integrates custom tools with existing AI personas - Provides a user interface for managing custom tools in the admin panel 3. Possible use cases: - Creating custom tools for specific tasks or integrations (stock quotes, currency conversion etc...) - Allowing administrators to add new functionalities to AI assistants without modifying core code - Implementing domain-specific tools for particular communities or industries 4. Code structure: The PR introduces several new files and modifies existing ones: a. Models: - `app/models/ai_tool.rb`: Defines the AiTool model - `app/serializers/ai_custom_tool_serializer.rb`: Serializer for AI tools b. Controllers: - `app/controllers/discourse_ai/admin/ai_tools_controller.rb`: Handles CRUD operations for AI tools c. Views and Components: - New Ember.js components for tool management in the admin interface - Updates to existing AI persona management components to support custom tools d. Core functionality: - `lib/ai_bot/tool_runner.rb`: Implements the custom tool execution system - `lib/ai_bot/tools/custom.rb`: Defines the custom tool class e. Routes and configurations: - Updates to route configurations to include new AI tool management pages f. Migrations: - `db/migrate/20240618080148_create_ai_tools.rb`: Creates the ai_tools table g. Tests: - New test files for AI tool functionality and integration The PR integrates the custom tools system with the existing AI persona framework, allowing personas to use both built-in and custom tools. It also includes safety measures such as timeouts and HTTP request limits to prevent misuse of custom tools. Overall, this PR significantly enhances the flexibility and extensibility of the Discourse AI system by allowing administrators to create and manage custom AI tools tailored to their specific needs. Co-authored-by: Martin Brennan --- ...min-plugins-show-discourse-ai-tools-new.js | 16 ++ ...in-plugins-show-discourse-ai-tools-show.js | 17 ++ .../admin-plugins-show-discourse-ai-tools.js | 7 + .../show/discourse-ai-tools/index.hbs | 1 + .../show/discourse-ai-tools/new.hbs | 5 + .../show/discourse-ai-tools/show.hbs | 5 + .../admin/ai_personas_controller.rb | 8 + .../discourse_ai/admin/ai_tools_controller.rb | 89 +++++++ app/models/ai_persona.rb | 25 +- app/models/ai_tool.rb | 183 ++++++++++++++ app/models/llm_model.rb | 4 + .../ai_custom_tool_list_serializer.rb | 15 ++ app/serializers/ai_custom_tool_serializer.rb | 15 ++ .../admin-discourse-ai-plugin-route-map.js | 5 + .../discourse/admin/adapters/ai-tool.js | 21 ++ .../discourse/admin/models/ai-tool.js | 27 +++ .../discourse/components/ai-tool-editor.gjs | 223 ++++++++++++++++++ .../components/ai-tool-list-editor.gjs | 47 ++++ .../components/ai-tool-parameter-editor.gjs | 160 +++++++++++++ .../components/modal/ai-tool-test-modal.gjs | 83 +++++++ .../components/modal/debug-ai-modal.gjs | 32 +-- assets/javascripts/discourse/lib/utilities.js | 35 +++ .../admin-plugin-configuration-nav.js | 8 +- .../modules/ai-bot/common/ai-tools.scss | 79 +++++++ config/locales/client.en.yml | 32 ++- config/locales/server.en.yml | 12 + config/routes.rb | 7 + db/migrate/20240618080148_create_ai_tools.rb | 19 ++ lib/ai_bot/personas/persona.rb | 6 +- lib/ai_bot/tool_runner.rb | 174 ++++++++++++++ lib/ai_bot/tools/custom.rb | 60 +++++ lib/ai_bot/tools/tool.rb | 45 +++- plugin.rb | 2 + spec/fabricators/llm_model_fabricator.rb | 2 +- spec/lib/modules/ai_bot/playground_spec.rb | 83 ++++++- spec/models/ai_tool_spec.rb | 196 +++++++++++++++ .../admin/ai_tools_controller_spec.rb | 150 ++++++++++++ spec/system/ai_bot/tool_spec.rb | 61 +++++ .../page_objects/modals/ai_tool_test_modal.rb | 26 ++ 39 files changed, 1939 insertions(+), 46 deletions(-) create mode 100644 admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js create mode 100644 admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-show.js create mode 100644 admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools.js create mode 100644 admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/index.hbs create mode 100644 admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs create mode 100644 admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/show.hbs create mode 100644 app/controllers/discourse_ai/admin/ai_tools_controller.rb create mode 100644 app/models/ai_tool.rb create mode 100644 app/serializers/ai_custom_tool_list_serializer.rb create mode 100644 app/serializers/ai_custom_tool_serializer.rb create mode 100644 assets/javascripts/discourse/admin/adapters/ai-tool.js create mode 100644 assets/javascripts/discourse/admin/models/ai-tool.js create mode 100644 assets/javascripts/discourse/components/ai-tool-editor.gjs create mode 100644 assets/javascripts/discourse/components/ai-tool-list-editor.gjs create mode 100644 assets/javascripts/discourse/components/ai-tool-parameter-editor.gjs create mode 100644 assets/javascripts/discourse/components/modal/ai-tool-test-modal.gjs create mode 100644 assets/stylesheets/modules/ai-bot/common/ai-tools.scss create mode 100644 db/migrate/20240618080148_create_ai_tools.rb create mode 100644 lib/ai_bot/tool_runner.rb create mode 100644 lib/ai_bot/tools/custom.rb create mode 100644 spec/models/ai_tool_spec.rb create mode 100644 spec/requests/admin/ai_tools_controller_spec.rb create mode 100644 spec/system/ai_bot/tool_spec.rb create mode 100644 spec/system/page_objects/modals/ai_tool_test_modal.rb diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js new file mode 100644 index 00000000..5f669195 --- /dev/null +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js @@ -0,0 +1,16 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + async model() { + const record = this.store.createRecord("ai-tool"); + return record; + }, + + setupController(controller, model) { + this._super(controller, model); + const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools"); + + controller.set("allTools", toolsModel); + controller.set("presets", toolsModel.resultSetMeta.presets); + }, +}); diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-show.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-show.js new file mode 100644 index 00000000..d111e02e --- /dev/null +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-show.js @@ -0,0 +1,17 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + async model(params) { + const allTools = this.modelFor("adminPlugins.show.discourse-ai-tools"); + const id = parseInt(params.id, 10); + return allTools.findBy("id", id); + }, + + setupController(controller, model) { + this._super(controller, model); + const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools"); + + controller.set("allTools", toolsModel); + controller.set("presets", toolsModel.resultSetMeta.presets); + }, +}); diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools.js new file mode 100644 index 00000000..b24e96ab --- /dev/null +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools.js @@ -0,0 +1,7 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default class DiscourseAiToolsRoute extends DiscourseRoute { + model() { + return this.store.findAll("ai-tool"); + } +} diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/index.hbs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/index.hbs new file mode 100644 index 00000000..1b0dbe81 --- /dev/null +++ b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/index.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs new file mode 100644 index 00000000..88f8a934 --- /dev/null +++ b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/show.hbs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/show.hbs new file mode 100644 index 00000000..88f8a934 --- /dev/null +++ b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/show.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/controllers/discourse_ai/admin/ai_personas_controller.rb b/app/controllers/discourse_ai/admin/ai_personas_controller.rb index 3dc17a15..fbd3dce7 100644 --- a/app/controllers/discourse_ai/admin/ai_personas_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_personas_controller.rb @@ -19,6 +19,14 @@ module DiscourseAi DiscourseAi::AiBot::Personas::Persona.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), + } + end llms = DiscourseAi::Configuration::LlmEnumerator.values.map do |hash| { id: hash[:value], name: hash[:name] } diff --git a/app/controllers/discourse_ai/admin/ai_tools_controller.rb b/app/controllers/discourse_ai/admin/ai_tools_controller.rb new file mode 100644 index 00000000..0e61d923 --- /dev/null +++ b/app/controllers/discourse_ai/admin/ai_tools_controller.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module DiscourseAi + module Admin + class AiToolsController < ::Admin::AdminController + requires_plugin ::DiscourseAi::PLUGIN_NAME + + before_action :find_ai_tool, only: %i[show update destroy] + + def index + ai_tools = AiTool.all + render_serialized({ ai_tools: ai_tools }, AiCustomToolListSerializer, root: false) + end + + def show + render_serialized(@ai_tool, AiCustomToolSerializer) + end + + def create + ai_tool = AiTool.new(ai_tool_params) + ai_tool.created_by_id = current_user.id + + if ai_tool.save + render_serialized(ai_tool, AiCustomToolSerializer, status: :created) + else + render_json_error ai_tool + end + end + + def update + if @ai_tool.update(ai_tool_params) + render_serialized(@ai_tool, AiCustomToolSerializer) + else + render_json_error @ai_tool + end + end + + def destroy + if @ai_tool.destroy + head :no_content + else + render_json_error @ai_tool + end + end + + def test + 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 + + # we need an llm so we have a tokenizer + # but will do without if none is available + llm = LlmModel.first&.to_llm + runner = ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {}) + result = runner.invoke + + if result.is_a?(Hash) && result[:error] + render_json_error result[:error] + else + render json: { output: result } + end + rescue ActiveRecord::RecordNotFound => e + render_json_error e.message, status: 400 + rescue => e + render_json_error "Error executing the tool: #{e.message}", status: 400 + end + + private + + def find_ai_tool + @ai_tool = AiTool.find(params[:id]) + end + + def ai_tool_params + params.require(:ai_tool).permit( + :name, + :description, + :script, + :summary, + parameters: %i[name type description], + ) + end + end + end +end diff --git a/app/models/ai_persona.rb b/app/models/ai_persona.rb index a18499be..6a6e4c3f 100644 --- a/app/models/ai_persona.rb +++ b/app/models/ai_persona.rb @@ -142,16 +142,25 @@ class AiPersona < ActiveRecord::Base options = {} tools = self.tools.filter_map do |element| - inner_name, current_options = element.is_a?(Array) ? element : [element, nil] - inner_name = inner_name.gsub("Tool", "") - inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name) + klass = nil + + if element.is_a?(String) && element.start_with?("custom-") + custom_tool_id = element.split("-", 2).last.to_i + if AiTool.exists?(id: custom_tool_id, enabled: true) + klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id) + end + else + inner_name, current_options = element.is_a?(Array) ? element : [element, nil] + inner_name = inner_name.gsub("Tool", "") + inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name) + + begin + klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize + options[klass] = current_options if current_options + rescue StandardError + end - begin - klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize - options[klass] = current_options if current_options klass - rescue StandardError - nil end end diff --git a/app/models/ai_tool.rb b/app/models/ai_tool.rb new file mode 100644 index 00000000..79287573 --- /dev/null +++ b/app/models/ai_tool.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +class AiTool < ActiveRecord::Base + validates :name, presence: true, length: { maximum: 255 } + validates :description, presence: true, length: { maximum: 1000 } + validates :script, presence: true, length: { maximum: 100_000 } + validates :created_by_id, presence: true + belongs_to :created_by, class_name: "User" + + def signature + { name: name, description: description, parameters: parameters.map(&:symbolize_keys) } + end + + def runner(parameters, llm:, bot_user:, context: {}) + DiscourseAi::AiBot::ToolRunner.new( + parameters: parameters, + llm: llm, + bot_user: bot_user, + context: context, + tool: self, + ) + end + + after_commit :bump_persona_cache + + def bump_persona_cache + AiPersona.persona_cache.flush! + end + + def self.presets + [ + { + preset_id: "browse_web_jina", + name: "browse_web", + description: "Browse the web as a markdown document", + parameters: [ + { name: "url", type: "string", required: true, description: "The URL to browse" }, + ], + script: <<~SCRIPT, + let url; + function invoke(p) { + url = p.url; + result = http.get(`https://r.jina.ai/${url}`); + // truncates to 15000 tokens + return llm.truncate(result.body, 15000); + } + function details() { + return "Read: " + url + } + SCRIPT + }, + { + preset_id: "exchange_rate", + name: "exchange_rate", + description: "Get current exchange rates for various currencies", + parameters: [ + { + name: "base_currency", + type: "string", + required: true, + description: "The base currency code (e.g., USD, EUR)", + }, + { + name: "target_currency", + type: "string", + required: true, + description: "The target currency code (e.g., EUR, JPY)", + }, + { name: "amount", type: "number", description: "Amount to convert eg: 123.45" }, + ], + script: <<~SCRIPT, + // note: this script uses the open.er-api.com service, it is only updated + // once every 24 hours, for more up to date rates see: https://www.exchangerate-api.com + function invoke(params) { + const url = `https://open.er-api.com/v6/latest/${params.base_currency}`; + const result = http.get(url); + if (result.status !== 200) { + return { error: "Failed to fetch exchange rates" }; + } + const data = JSON.parse(result.body); + const rate = data.rates[params.target_currency]; + if (!rate) { + return { error: "Target currency not found" }; + } + + const rval = { + base_currency: params.base_currency, + target_currency: params.target_currency, + exchange_rate: rate, + last_updated: data.time_last_update_utc + }; + + if (params.amount) { + rval.original_amount = params.amount; + rval.converted_amount = params.amount * rate; + } + + return rval; + } + + function details() { + return "Rates By Exchange Rate API"; + } + SCRIPT + summary: "Get current exchange rates between two currencies", + }, + { + preset_id: "stock_quote", + name: "stock_quote", + description: "Get real-time stock quote information using AlphaVantage API", + parameters: [ + { + name: "symbol", + type: "string", + required: true, + description: "The stock symbol (e.g., AAPL, GOOGL)", + }, + ], + script: <<~SCRIPT, + function invoke(params) { + const apiKey = 'YOUR_ALPHAVANTAGE_API_KEY'; // Replace with your actual API key + const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${params.symbol}&apikey=${apiKey}`; + + const result = http.get(url); + if (result.status !== 200) { + return { error: "Failed to fetch stock quote" }; + } + + const data = JSON.parse(result.body); + if (data['Error Message']) { + return { error: data['Error Message'] }; + } + + const quote = data['Global Quote']; + if (!quote || Object.keys(quote).length === 0) { + return { error: "No data found for the given symbol" }; + } + + return { + symbol: quote['01. symbol'], + price: parseFloat(quote['05. price']), + change: parseFloat(quote['09. change']), + change_percent: quote['10. change percent'], + volume: parseInt(quote['06. volume']), + latest_trading_day: quote['07. latest trading day'] + }; + } + + function details() { + return "Stock data provided by AlphaVantage"; + } + SCRIPT + summary: "Get real-time stock quotes using AlphaVantage API", + }, + { preset_id: "empty_tool", script: <<~SCRIPT }, + function invoke(params) { + // logic here + return params; + } + function details() { + return "Details about this tool"; + } + SCRIPT + ].map do |preset| + preset[:preset_name] = I18n.t("discourse_ai.tools.presets.#{preset[:preset_id]}.name") + preset + end + end +end + +# == Schema Information +# +# Table name: ai_tools +# +# id :bigint not null, primary key +# name :string not null +# description :text not null +# parameters :jsonb not null +# script :text not null +# created_by_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/llm_model.rb b/app/models/llm_model.rb index 2eb45913..a818d6b2 100644 --- a/app/models/llm_model.rb +++ b/app/models/llm_model.rb @@ -41,6 +41,10 @@ class LlmModel < ActiveRecord::Base } end + def to_llm + DiscourseAi::Completions::Llm.proxy_from_obj(self) + end + def toggle_companion_user return if name == "fake" && Rails.env.production? diff --git a/app/serializers/ai_custom_tool_list_serializer.rb b/app/serializers/ai_custom_tool_list_serializer.rb new file mode 100644 index 00000000..f0e3f1cc --- /dev/null +++ b/app/serializers/ai_custom_tool_list_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AiCustomToolListSerializer < ApplicationSerializer + attributes :meta + + has_many :ai_tools, serializer: AiCustomToolSerializer, embed: :objects + + def meta + { presets: AiTool.presets } + end + + def ai_tools + object[:ai_tools] + end +end diff --git a/app/serializers/ai_custom_tool_serializer.rb b/app/serializers/ai_custom_tool_serializer.rb new file mode 100644 index 00000000..c95482ad --- /dev/null +++ b/app/serializers/ai_custom_tool_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AiCustomToolSerializer < ApplicationSerializer + attributes :id, + :name, + :description, + :summary, + :parameters, + :script, + :created_by_id, + :created_at, + :updated_at + + self.root = "ai_tool" +end diff --git a/assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js b/assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js index 97ed05e5..f71f313a 100644 --- a/assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js +++ b/assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js @@ -13,5 +13,10 @@ export default { this.route("new"); this.route("show", { path: "/:id" }); }); + + this.route("discourse-ai-tools", { path: "ai-tools" }, function () { + this.route("new"); + this.route("show", { path: "/:id" }); + }); }, }; diff --git a/assets/javascripts/discourse/admin/adapters/ai-tool.js b/assets/javascripts/discourse/admin/adapters/ai-tool.js new file mode 100644 index 00000000..15cfeb84 --- /dev/null +++ b/assets/javascripts/discourse/admin/adapters/ai-tool.js @@ -0,0 +1,21 @@ +import RestAdapter from "discourse/adapters/rest"; + +export default class Adapter extends RestAdapter { + jsonMode = true; + + basePath() { + return "/admin/plugins/discourse-ai/"; + } + + pathFor(store, type, findArgs) { + // removes underscores which are implemented in base + let path = + this.basePath(store, type, findArgs) + + store.pluralize(this.apiNameFor(type)); + return this.appendQueryParams(path, findArgs); + } + + apiNameFor() { + return "ai-tool"; + } +} diff --git a/assets/javascripts/discourse/admin/models/ai-tool.js b/assets/javascripts/discourse/admin/models/ai-tool.js new file mode 100644 index 00000000..d1f32ce5 --- /dev/null +++ b/assets/javascripts/discourse/admin/models/ai-tool.js @@ -0,0 +1,27 @@ +import RestModel from "discourse/models/rest"; + +const CREATE_ATTRIBUTES = [ + "id", + "name", + "description", + "parameters", + "script", + "summary", + "enabled", +]; + +export default class AiTool extends RestModel { + createProperties() { + return this.getProperties(CREATE_ATTRIBUTES); + } + + updateProperties() { + return this.getProperties(CREATE_ATTRIBUTES); + } + + workingCopy() { + let attrs = this.getProperties(CREATE_ATTRIBUTES); + attrs.parameters = attrs.parameters || []; + return AiTool.create(attrs); + } +} diff --git a/assets/javascripts/discourse/components/ai-tool-editor.gjs b/assets/javascripts/discourse/components/ai-tool-editor.gjs new file mode 100644 index 00000000..1882f440 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-tool-editor.gjs @@ -0,0 +1,223 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { Input } from "@ember/component"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import didUpdate from "@ember/render-modifiers/modifiers/did-update"; +import { inject as service } from "@ember/service"; +import BackButton from "discourse/components/back-button"; +import DButton from "discourse/components/d-button"; +import Textarea from "discourse/components/d-textarea"; +import DTooltip from "discourse/components/d-tooltip"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import I18n from "discourse-i18n"; +import AceEditor from "admin/components/ace-editor"; +import ComboBox from "select-kit/components/combo-box"; +import AiToolParameterEditor from "./ai-tool-parameter-editor"; +import AiToolTestModal from "./modal/ai-tool-test-modal"; + +export default class AiToolEditor extends Component { + @service router; + @service store; + @service dialog; + @service modal; + @service toasts; + + @tracked isSaving = false; + @tracked editingModel = null; + @tracked showDelete = false; + + @tracked selectedPreset = null; + + aceEditorMode = "javascript"; + aceEditorTheme = "chrome"; + + @action + updateModel() { + this.editingModel = this.args.model.workingCopy(); + this.showDelete = !this.args.model.isNew; + } + + get presets() { + return this.args.presets.map((preset) => { + return { + name: preset.preset_name, + id: preset.preset_id, + }; + }); + } + + get showPresets() { + return !this.selectedPreset && this.args.model.isNew; + } + + @action + configurePreset() { + this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId); + this.editingModel = this.args.model.workingCopy(); + this.editingModel.setProperties(this.selectedPreset); + this.showDelete = false; + } + + @action + async save() { + this.isSaving = true; + + try { + await this.args.model.save( + this.editingModel.getProperties( + "name", + "description", + "parameters", + "script", + "summary" + ) + ); + + this.toasts.success({ + data: { message: I18n.t("discourse_ai.tools.saved") }, + duration: 2000, + }); + if (!this.args.tools.any((tool) => tool.id === this.args.model.id)) { + this.args.tools.pushObject(this.args.model); + } + + this.router.transitionTo( + "adminPlugins.show.discourse-ai-tools.show", + this.args.model + ); + } catch (e) { + popupAjaxError(e); + } finally { + this.isSaving = false; + } + } + + @action + delete() { + return this.dialog.confirm({ + message: I18n.t("discourse_ai.tools.confirm_delete"), + didConfirm: () => { + return this.args.model.destroyRecord().then(() => { + this.args.tools.removeObject(this.args.model); + this.router.transitionTo( + "adminPlugins.show.discourse-ai-tools.index" + ); + }); + }, + }); + } + + @action + updateScript(script) { + this.editingModel.script = script; + } + + @action + openTestModal() { + this.modal.show(AiToolTestModal, { + model: { + tool: this.editingModel, + }, + }); + } + +