From b60926c6e6a91c54220112ed02e7a37229401165 Mon Sep 17 00:00:00 2001 From: Hoa Nguyen Date: Fri, 7 Feb 2025 14:34:47 +1100 Subject: [PATCH] FEATURE: Tool name validation (#842) * FEATURE: Tool name validation - Add unique index to the name column of the ai_tools table - correct our tests for AiToolController - tool_name field which will be used to represent to LLM - Add tool_name to Tools's presets - Add duplicate tools validation for AiPersona - Add unique constraint to the name column of the ai_tools table * DEV: Validate duplicate tool_name between builin tools and custom tools * lint * chore: fix linting * fix conlict mistakes * chore: correct icon class * chore: fix failed specs * Add max_length to tool_name * chore: correct the option name * lintings * fix lintings --- .../admin/ai_personas_controller.rb | 7 ++- .../discourse_ai/admin/ai_tools_controller.rb | 1 + app/models/ai_persona.rb | 44 +++++++++++++++++ app/models/ai_tool.rb | 35 +++++++++++--- app/serializers/ai_custom_tool_serializer.rb | 1 + .../discourse/admin/models/ai-tool.js | 1 + .../discourse/components/ai-tool-editor.gjs | 18 +++++++ config/locales/client.en.yml | 6 ++- config/locales/server.en.yml | 3 ++ ...0241020010245_add_tool_name_to_ai_tools.rb | 23 +++++++++ ...41242_add_unique_constraint_to_ai_tools.rb | 25 ++++++++++ lib/ai_bot/personas/general.rb | 1 - lib/ai_bot/tools/custom.rb | 5 +- spec/fabricators/ai_tool_fabricator.rb | 10 ++++ spec/lib/modules/ai_bot/playground_spec.rb | 1 + spec/models/ai_persona_spec.rb | 47 +++++++++++++++++++ spec/models/ai_tool_spec.rb | 5 +- .../admin/ai_tools_controller_spec.rb | 7 ++- 18 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20241020010245_add_tool_name_to_ai_tools.rb create mode 100644 db/migrate/20241023041242_add_unique_constraint_to_ai_tools.rb create mode 100644 spec/fabricators/ai_tool_fabricator.rb diff --git a/app/controllers/discourse_ai/admin/ai_personas_controller.rb b/app/controllers/discourse_ai/admin/ai_personas_controller.rb index 2d07b7bbe..b0317f02f 100644 --- a/app/controllers/discourse_ai/admin/ai_personas_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_personas_controller.rb @@ -23,7 +23,12 @@ def index .each do |tool| tools << { id: "custom-#{tool.id}", - name: I18n.t("discourse_ai.tools.custom_name", name: tool.name.capitalize), + name: + I18n.t( + "discourse_ai.tools.custom_name", + name: tool.name.capitalize, + tool_name: tool.tool_name, + ), } end llms = diff --git a/app/controllers/discourse_ai/admin/ai_tools_controller.rb b/app/controllers/discourse_ai/admin/ai_tools_controller.rb index c8a85c60e..fc7b29844 100644 --- a/app/controllers/discourse_ai/admin/ai_tools_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_tools_controller.rb @@ -84,6 +84,7 @@ def ai_tool_params .require(:ai_tool) .permit( :name, + :tool_name, :description, :script, :summary, diff --git a/app/models/ai_persona.rb b/app/models/ai_persona.rb index cc1df7e02..2c573977e 100644 --- a/app/models/ai_persona.rb +++ b/app/models/ai_persona.rb @@ -22,6 +22,9 @@ class AiPersona < ActiveRecord::Base validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 } validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 } validates :forced_tool_count, numericality: { greater_than: -2, maximum: 100_000 } + + validate :tools_can_not_be_duplicated + has_many :rag_document_fragments, dependent: :destroy, as: :target belongs_to :created_by, class_name: "User" @@ -107,6 +110,47 @@ def bump_cache self.class.persona_cache.flush! end + def tools_can_not_be_duplicated + return unless tools.is_a?(Array) + + seen_tools = Set.new + + custom_tool_ids = Set.new + builtin_tool_names = Set.new + + tools.each do |tool| + inner_name, _, _ = tool.is_a?(Array) ? tool : [tool, nil] + + if inner_name.start_with?("custom-") + custom_tool_ids.add(inner_name.split("-", 2).last.to_i) + else + builtin_tool_names.add(inner_name.downcase) + end + + if seen_tools.include?(inner_name) + errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools")) + break + else + seen_tools.add(inner_name) + end + end + + return if errors.any? + + # Checking if there are any duplicate tool_names between custom and builtin tools + if builtin_tool_names.present? && custom_tool_ids.present? + AiTool + .where(id: custom_tool_ids) + .pluck(:tool_name) + .each do |tool_name| + if builtin_tool_names.include?(tool_name.downcase) + errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools")) + break + end + end + end + end + def class_instance attributes = %i[ id diff --git a/app/models/ai_tool.rb b/app/models/ai_tool.rb index ee2026cf9..97b2a983f 100644 --- a/app/models/ai_tool.rb +++ b/app/models/ai_tool.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class AiTool < ActiveRecord::Base - validates :name, presence: true, length: { maximum: 100 } + validates :name, presence: true, length: { maximum: 100 }, uniqueness: true + validates :tool_name, presence: true, length: { maximum: 100 } validates :description, presence: true, length: { maximum: 1000 } validates :summary, presence: true, length: { maximum: 255 } validates :script, presence: true, length: { maximum: 100_000 } @@ -12,8 +13,25 @@ class AiTool < ActiveRecord::Base has_many :uploads, through: :upload_references before_update :regenerate_rag_fragments + ALPHANUMERIC_PATTERN = /\A[a-zA-Z0-9_]+\z/ + + validates :tool_name, + format: { + with: ALPHANUMERIC_PATTERN, + message: I18n.t("discourse_ai.tools.name.characters"), + } + def signature - { name: name, description: description, parameters: parameters.map(&:symbolize_keys) } + { + name: function_call_name, + description: description, + parameters: parameters.map(&:symbolize_keys), + } + end + + # Backwards compatibility: if tool_name is not set (existing custom tools), use name + def function_call_name + tool_name.presence || name end def runner(parameters, llm:, bot_user:, context: {}) @@ -127,7 +145,8 @@ def self.presets [ { preset_id: "browse_web_jina", - name: "browse_web", + name: "Browse Web", + tool_name: "browse_web", description: "Browse the web as a markdown document", parameters: [ { name: "url", type: "string", required: true, description: "The URL to browse" }, @@ -148,7 +167,8 @@ def self.presets }, { preset_id: "exchange_rate", - name: "exchange_rate", + name: "Exchange Rate", + tool_name: "exchange_rate", description: "Get current exchange rates for various currencies", parameters: [ { @@ -204,7 +224,8 @@ def self.presets }, { preset_id: "stock_quote", - name: "stock_quote", + name: "Stock Quote (AlphaVantage)", + tool_name: "stock_quote", description: "Get real-time stock quote information using AlphaVantage API", parameters: [ { @@ -253,7 +274,8 @@ def self.presets }, { preset_id: "image_generation", - name: "image_generation", + name: "Image Generation (Flux)", + tool_name: "image_generation", description: "Generate images using the FLUX model from Black Forest Labs using together.ai", parameters: [ @@ -348,4 +370,5 @@ def self.presets # updated_at :datetime not null # rag_chunk_tokens :integer default(374), not null # rag_chunk_overlap_tokens :integer default(10), not null +# tool_name :string(100) default(""), not null # diff --git a/app/serializers/ai_custom_tool_serializer.rb b/app/serializers/ai_custom_tool_serializer.rb index 8fe1fd412..1ac804300 100644 --- a/app/serializers/ai_custom_tool_serializer.rb +++ b/app/serializers/ai_custom_tool_serializer.rb @@ -3,6 +3,7 @@ class AiCustomToolSerializer < ApplicationSerializer attributes :id, :name, + :tool_name, :description, :summary, :parameters, diff --git a/assets/javascripts/discourse/admin/models/ai-tool.js b/assets/javascripts/discourse/admin/models/ai-tool.js index bfeea21bf..7188a68c1 100644 --- a/assets/javascripts/discourse/admin/models/ai-tool.js +++ b/assets/javascripts/discourse/admin/models/ai-tool.js @@ -4,6 +4,7 @@ import RestModel from "discourse/models/rest"; const CREATE_ATTRIBUTES = [ "id", "name", + "tool_name", "description", "parameters", "script", diff --git a/assets/javascripts/discourse/components/ai-tool-editor.gjs b/assets/javascripts/discourse/components/ai-tool-editor.gjs index ef7c16f37..154ce390b 100644 --- a/assets/javascripts/discourse/components/ai-tool-editor.gjs +++ b/assets/javascripts/discourse/components/ai-tool-editor.gjs @@ -83,6 +83,7 @@ export default class AiToolEditor extends Component { try { const data = this.editingModel.getProperties( "name", + "tool_name", "description", "parameters", "script", @@ -178,6 +179,23 @@ export default class AiToolEditor extends Component { /> +
+ + + +
+