Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit b60926c

Browse files
authored
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
1 parent 551f674 commit b60926c

File tree

18 files changed

+225
-15
lines changed

18 files changed

+225
-15
lines changed

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ def index
2323
.each do |tool|
2424
tools << {
2525
id: "custom-#{tool.id}",
26-
name: I18n.t("discourse_ai.tools.custom_name", name: tool.name.capitalize),
26+
name:
27+
I18n.t(
28+
"discourse_ai.tools.custom_name",
29+
name: tool.name.capitalize,
30+
tool_name: tool.tool_name,
31+
),
2732
}
2833
end
2934
llms =

app/controllers/discourse_ai/admin/ai_tools_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def ai_tool_params
8484
.require(:ai_tool)
8585
.permit(
8686
:name,
87+
:tool_name,
8788
:description,
8889
:script,
8990
:summary,

app/models/ai_persona.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class AiPersona < ActiveRecord::Base
2222
validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 }
2323
validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 }
2424
validates :forced_tool_count, numericality: { greater_than: -2, maximum: 100_000 }
25+
26+
validate :tools_can_not_be_duplicated
27+
2528
has_many :rag_document_fragments, dependent: :destroy, as: :target
2629

2730
belongs_to :created_by, class_name: "User"
@@ -107,6 +110,47 @@ def bump_cache
107110
self.class.persona_cache.flush!
108111
end
109112

113+
def tools_can_not_be_duplicated
114+
return unless tools.is_a?(Array)
115+
116+
seen_tools = Set.new
117+
118+
custom_tool_ids = Set.new
119+
builtin_tool_names = Set.new
120+
121+
tools.each do |tool|
122+
inner_name, _, _ = tool.is_a?(Array) ? tool : [tool, nil]
123+
124+
if inner_name.start_with?("custom-")
125+
custom_tool_ids.add(inner_name.split("-", 2).last.to_i)
126+
else
127+
builtin_tool_names.add(inner_name.downcase)
128+
end
129+
130+
if seen_tools.include?(inner_name)
131+
errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools"))
132+
break
133+
else
134+
seen_tools.add(inner_name)
135+
end
136+
end
137+
138+
return if errors.any?
139+
140+
# Checking if there are any duplicate tool_names between custom and builtin tools
141+
if builtin_tool_names.present? && custom_tool_ids.present?
142+
AiTool
143+
.where(id: custom_tool_ids)
144+
.pluck(:tool_name)
145+
.each do |tool_name|
146+
if builtin_tool_names.include?(tool_name.downcase)
147+
errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools"))
148+
break
149+
end
150+
end
151+
end
152+
end
153+
110154
def class_instance
111155
attributes = %i[
112156
id

app/models/ai_tool.rb

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# frozen_string_literal: true
22

33
class AiTool < ActiveRecord::Base
4-
validates :name, presence: true, length: { maximum: 100 }
4+
validates :name, presence: true, length: { maximum: 100 }, uniqueness: true
5+
validates :tool_name, presence: true, length: { maximum: 100 }
56
validates :description, presence: true, length: { maximum: 1000 }
67
validates :summary, presence: true, length: { maximum: 255 }
78
validates :script, presence: true, length: { maximum: 100_000 }
@@ -12,8 +13,25 @@ class AiTool < ActiveRecord::Base
1213
has_many :uploads, through: :upload_references
1314
before_update :regenerate_rag_fragments
1415

16+
ALPHANUMERIC_PATTERN = /\A[a-zA-Z0-9_]+\z/
17+
18+
validates :tool_name,
19+
format: {
20+
with: ALPHANUMERIC_PATTERN,
21+
message: I18n.t("discourse_ai.tools.name.characters"),
22+
}
23+
1524
def signature
16-
{ name: name, description: description, parameters: parameters.map(&:symbolize_keys) }
25+
{
26+
name: function_call_name,
27+
description: description,
28+
parameters: parameters.map(&:symbolize_keys),
29+
}
30+
end
31+
32+
# Backwards compatibility: if tool_name is not set (existing custom tools), use name
33+
def function_call_name
34+
tool_name.presence || name
1735
end
1836

1937
def runner(parameters, llm:, bot_user:, context: {})
@@ -127,7 +145,8 @@ def self.presets
127145
[
128146
{
129147
preset_id: "browse_web_jina",
130-
name: "browse_web",
148+
name: "Browse Web",
149+
tool_name: "browse_web",
131150
description: "Browse the web as a markdown document",
132151
parameters: [
133152
{ name: "url", type: "string", required: true, description: "The URL to browse" },
@@ -148,7 +167,8 @@ def self.presets
148167
},
149168
{
150169
preset_id: "exchange_rate",
151-
name: "exchange_rate",
170+
name: "Exchange Rate",
171+
tool_name: "exchange_rate",
152172
description: "Get current exchange rates for various currencies",
153173
parameters: [
154174
{
@@ -204,7 +224,8 @@ def self.presets
204224
},
205225
{
206226
preset_id: "stock_quote",
207-
name: "stock_quote",
227+
name: "Stock Quote (AlphaVantage)",
228+
tool_name: "stock_quote",
208229
description: "Get real-time stock quote information using AlphaVantage API",
209230
parameters: [
210231
{
@@ -253,7 +274,8 @@ def self.presets
253274
},
254275
{
255276
preset_id: "image_generation",
256-
name: "image_generation",
277+
name: "Image Generation (Flux)",
278+
tool_name: "image_generation",
257279
description:
258280
"Generate images using the FLUX model from Black Forest Labs using together.ai",
259281
parameters: [
@@ -348,4 +370,5 @@ def self.presets
348370
# updated_at :datetime not null
349371
# rag_chunk_tokens :integer default(374), not null
350372
# rag_chunk_overlap_tokens :integer default(10), not null
373+
# tool_name :string(100) default(""), not null
351374
#

app/serializers/ai_custom_tool_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
class AiCustomToolSerializer < ApplicationSerializer
44
attributes :id,
55
:name,
6+
:tool_name,
67
:description,
78
:summary,
89
:parameters,

assets/javascripts/discourse/admin/models/ai-tool.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import RestModel from "discourse/models/rest";
44
const CREATE_ATTRIBUTES = [
55
"id",
66
"name",
7+
"tool_name",
78
"description",
89
"parameters",
910
"script",

assets/javascripts/discourse/components/ai-tool-editor.gjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default class AiToolEditor extends Component {
8383
try {
8484
const data = this.editingModel.getProperties(
8585
"name",
86+
"tool_name",
8687
"description",
8788
"parameters",
8889
"script",
@@ -178,6 +179,23 @@ export default class AiToolEditor extends Component {
178179
/>
179180
</div>
180181

182+
<div class="control-group">
183+
<label>{{i18n "discourse_ai.tools.tool_name"}}</label>
184+
<input
185+
{{on
186+
"input"
187+
(withEventValue (fn (mut this.editingModel.tool_name)))
188+
}}
189+
value={{this.editingModel.tool_name}}
190+
type="text"
191+
class="ai-tool-editor__tool_name"
192+
/>
193+
<DTooltip
194+
@icon="circle-question"
195+
@content={{i18n "discourse_ai.tools.tool_name_help"}}
196+
/>
197+
</div>
198+
181199
<div class="control-group">
182200
<label>{{i18n "discourse_ai.tools.description"}}</label>
183201
<textarea

config/locales/client.en.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,13 @@ en:
292292
short_title: "Tools"
293293
no_tools: "You have not created any tools yet"
294294
name: "Name"
295-
subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
295+
name_help: "Name will show up in the Discourse UI and is the short identifier you will use to find the tool in various settings, it should be distinct (it is required)"
296296
new: "New tool"
297-
name_help: "The unique name of the tool as used by the language model"
297+
tool_name: "Tool Name"
298+
tool_name_help: "Tool Name is presented to the large language model. It is not distinct, but it is distinct per persona. (persona validates on save)"
298299
description: "Description"
299300
description_help: "A clear description of the tool's purpose for the language model"
301+
subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
300302
summary: "Summary"
301303
summary_help: "Summary of tools purpose to be displayed to end users"
302304
script: "Script"

config/locales/server.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ en:
215215
name: "Flux image generator (Together.ai)"
216216
empty_tool:
217217
name: "Start from blank..."
218+
name:
219+
characters: "must only include numbers, letters, and underscores"
218220

219221
ai_helper:
220222
errors:
@@ -260,6 +262,7 @@ en:
260262
default_llm_required: "Default LLM model is required prior to enabling Chat"
261263
cannot_delete_system_persona: "System personas cannot be deleted, please disable it instead"
262264
cannot_edit_system_persona: "System personas can only be renamed, you may not edit tools or system prompt, instead disable and make a copy"
265+
cannot_have_duplicate_tools: "Can not have duplicate tools"
263266
github_helper:
264267
name: "GitHub Helper"
265268
description: "AI Bot specialized in assisting with GitHub-related tasks and questions"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
class AddToolNameToAiTools < ActiveRecord::Migration[7.1]
4+
def up
5+
add_column :ai_tools,
6+
:tool_name,
7+
:string,
8+
null: false,
9+
limit: 100,
10+
default: "",
11+
if_not_exists: true
12+
13+
# Migrate existing name to tool_name
14+
execute <<~SQL
15+
UPDATE ai_tools
16+
SET tool_name = regexp_replace(LOWER(name),'[^a-z0-9_]','', 'g');
17+
SQL
18+
end
19+
20+
def down
21+
remove_column :ai_tools, :tool_name, if_exists: true
22+
end
23+
end

0 commit comments

Comments
 (0)