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

Commit 6c4c96e

Browse files
authored
FEATURE: allow persona to only force tool calls on limited replies (#827)
This introduces another configuration that allows operators to limit the amount of interactions with forced tool usage. Forced tools are very handy in initial llm interactions, but as conversation progresses they can hinder by slowing down stuff and adding confusion.
1 parent 52d90cf commit 6c4c96e

File tree

14 files changed

+149
-34
lines changed

14 files changed

+149
-34
lines changed

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def ai_persona_params
106106
:question_consolidator_llm,
107107
:allow_chat,
108108
:tool_details,
109+
:forced_tool_count,
109110
allowed_group_ids: [],
110111
rag_uploads: [:id],
111112
)

app/models/ai_persona.rb

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class AiPersona < ActiveRecord::Base
2020
validates :rag_chunk_tokens, numericality: { greater_than: 0, maximum: 50_000 }
2121
validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 }
2222
validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 }
23+
validates :forced_tool_count, numericality: { greater_than: -2, maximum: 100_000 }
2324
has_many :rag_document_fragments, dependent: :destroy, as: :target
2425

2526
belongs_to :created_by, class_name: "User"
@@ -185,6 +186,7 @@ def class_instance
185186

186187
define_method(:tools) { tools }
187188
define_method(:force_tool_use) { force_tool_use }
189+
define_method(:forced_tool_count) { @ai_persona&.forced_tool_count }
188190
define_method(:options) { options }
189191
define_method(:temperature) { @ai_persona&.temperature }
190192
define_method(:top_p) { @ai_persona&.top_p }
@@ -265,32 +267,40 @@ def ensure_not_system
265267
#
266268
# Table name: ai_personas
267269
#
268-
# id :bigint not null, primary key
269-
# name :string(100) not null
270-
# description :string(2000) not null
271-
# system_prompt :string(10000000) not null
272-
# allowed_group_ids :integer default([]), not null, is an Array
273-
# created_by_id :integer
274-
# enabled :boolean default(TRUE), not null
275-
# created_at :datetime not null
276-
# updated_at :datetime not null
277-
# system :boolean default(FALSE), not null
278-
# priority :boolean default(FALSE), not null
279-
# temperature :float
280-
# top_p :float
281-
# user_id :integer
282-
# mentionable :boolean default(FALSE), not null
283-
# default_llm :text
284-
# max_context_posts :integer
285-
# vision_enabled :boolean default(FALSE), not null
286-
# vision_max_pixels :integer default(1048576), not null
287-
# rag_chunk_tokens :integer default(374), not null
288-
# rag_chunk_overlap_tokens :integer default(10), not null
289-
# rag_conversation_chunks :integer default(10), not null
290-
# question_consolidator_llm :text
291-
# allow_chat :boolean default(FALSE), not null
292-
# tool_details :boolean default(TRUE), not null
293-
# tools :json not null
270+
# id :bigint not null, primary key
271+
# name :string(100) not null
272+
# description :string(2000) not null
273+
# system_prompt :string(10000000) not null
274+
# allowed_group_ids :integer default([]), not null, is an Array
275+
# created_by_id :integer
276+
# enabled :boolean default(TRUE), not null
277+
# created_at :datetime not null
278+
# updated_at :datetime not null
279+
# system :boolean default(FALSE), not null
280+
# priority :boolean default(FALSE), not null
281+
# temperature :float
282+
# top_p :float
283+
# user_id :integer
284+
# mentionable :boolean default(FALSE), not null
285+
# default_llm :text
286+
# max_context_posts :integer
287+
# max_post_context_tokens :integer
288+
# max_context_tokens :integer
289+
# vision_enabled :boolean default(FALSE), not null
290+
# vision_max_pixels :integer default(1048576), not null
291+
# rag_chunk_tokens :integer default(374), not null
292+
# rag_chunk_overlap_tokens :integer default(10), not null
293+
# rag_conversation_chunks :integer default(10), not null
294+
# role :enum default("bot"), not null
295+
# role_category_ids :integer default([]), not null, is an Array
296+
# role_tags :string default([]), not null, is an Array
297+
# role_group_ids :integer default([]), not null, is an Array
298+
# role_whispers :boolean default(FALSE), not null
299+
# role_max_responses_per_hour :integer default(50), not null
300+
# question_consolidator_llm :text
301+
# allow_chat :boolean default(FALSE), not null
302+
# tool_details :boolean default(TRUE), not null
303+
# tools :json not null
294304
#
295305
# Indexes
296306
#

app/serializers/localized_ai_persona_serializer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer
2525
:rag_conversation_chunks,
2626
:question_consolidator_llm,
2727
:allow_chat,
28-
:tool_details
28+
:tool_details,
29+
:forced_tool_count
2930

3031
has_one :user, serializer: BasicUserSerializer, embed: :object
3132
has_many :rag_uploads, serializer: UploadSerializer, embed: :object

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const CREATE_ATTRIBUTES = [
2828
"question_consolidator_llm",
2929
"allow_chat",
3030
"tool_details",
31+
"forced_tool_count",
3132
];
3233

3334
const SYSTEM_ATTRIBUTES = [
@@ -154,6 +155,7 @@ export default class AiPersona extends RestModel {
154155

155156
const persona = AiPersona.create(attrs);
156157
persona.forcedTools = (this.forcedTools || []).slice();
158+
persona.forced_tool_count = this.forced_tool_count || -1;
157159
return persona;
158160
}
159161
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { computed } from "@ember/object";
2+
import I18n from "discourse-i18n";
3+
import ComboBox from "select-kit/components/combo-box";
4+
5+
export default ComboBox.extend({
6+
content: computed(function () {
7+
const content = [
8+
{
9+
id: -1,
10+
name: I18n.t("discourse_ai.ai_persona.tool_strategies.all"),
11+
},
12+
];
13+
14+
[1, 2, 5].forEach((i) => {
15+
content.push({
16+
id: i,
17+
name: I18n.t("discourse_ai.ai_persona.tool_strategies.replies", {
18+
count: i,
19+
}),
20+
});
21+
});
22+
23+
return content;
24+
}),
25+
26+
selectKitOptions: {
27+
filterable: false,
28+
},
29+
});

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import AdminUser from "admin/models/admin-user";
2020
import ComboBox from "select-kit/components/combo-box";
2121
import GroupChooser from "select-kit/components/group-chooser";
2222
import DTooltip from "float-kit/components/d-tooltip";
23+
import AiForcedToolStrategySelector from "./ai-forced-tool-strategy-selector";
2324
import AiLlmSelector from "./ai-llm-selector";
2425
import AiPersonaToolOptions from "./ai-persona-tool-options";
2526
import AiToolSelector from "./ai-tool-selector";
@@ -49,7 +50,11 @@ export default class PersonaEditor extends Component {
4950
}
5051

5152
get allowForceTools() {
52-
return !this.editingModel?.system && this.editingModel?.tools?.length > 0;
53+
return !this.editingModel?.system && this.selectedToolNames.length > 0;
54+
}
55+
56+
get hasForcedTools() {
57+
return this.forcedToolNames.length > 0;
5358
}
5459

5560
@action
@@ -381,12 +386,23 @@ export default class PersonaEditor extends Component {
381386
<div class="control-group">
382387
<label>{{I18n.t "discourse_ai.ai_persona.forced_tools"}}</label>
383388
<AiToolSelector
384-
class="ai-persona-editor__tools"
389+
class="ai-persona-editor__forced_tools"
385390
@value={{this.forcedToolNames}}
386391
@tools={{this.selectedTools}}
387392
@onChange={{this.forcedToolsChanged}}
388393
/>
389394
</div>
395+
{{#if this.hasForcedTools}}
396+
<div class="control-group">
397+
<label>{{I18n.t
398+
"discourse_ai.ai_persona.forced_tool_strategy"
399+
}}</label>
400+
<AiForcedToolStrategySelector
401+
class="ai-persona-editor__forced_tool_strategy"
402+
@value={{this.editingModel.forced_tool_count}}
403+
/>
404+
</div>
405+
{{/if}}
390406
{{/if}}
391407
{{#unless this.editingModel.system}}
392408
<AiPersonaToolOptions

config/locales/client.en.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ en:
116116
select_option: "Select an option..."
117117

118118
ai_persona:
119+
tool_strategies:
120+
all: "Apply to all replies"
121+
replies:
122+
one: "Apply to first reply only"
123+
other: "Apply to first %{count} replies"
119124
back: Back
120125
name: Name
121126
edit: Edit
@@ -142,6 +147,7 @@ en:
142147
question_consolidator_llm: Language Model for Question Consolidator
143148
question_consolidator_llm_help: The language model to use for the question consolidator, you may choose a less powerful model to save costs.
144149
system_prompt: System Prompt
150+
forced_tool_strategy: Forced Tool Strategy
145151
allow_chat: "Allow Chat"
146152
allow_chat_help: "If enabled, users in allowed groups can DM this persona"
147153
save: Save
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class AddForcedToolCountToAiPersonas < ActiveRecord::Migration[7.1]
4+
def change
5+
add_column :ai_personas, :forced_tool_count, :integer, default: -1, null: false
6+
end
7+
end

lib/ai_bot/bot.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def force_tool_if_needed(prompt, context)
7272
forced_tools = persona.force_tool_use.map { |tool| tool.name }
7373
force_tool = forced_tools.find { |name| !context[:chosen_tools].include?(name) }
7474

75+
if force_tool && persona.forced_tool_count > 0
76+
user_turns = prompt.messages.select { |m| m[:type] == :user }.length
77+
force_tool = false if user_turns > persona.forced_tool_count
78+
end
79+
7580
if force_tool
7681
context[:chosen_tools] << force_tool
7782
prompt.tool_choice = force_tool

lib/ai_bot/personas/persona.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def force_tool_use
117117
[]
118118
end
119119

120+
def forced_tool_count
121+
-1
122+
end
123+
120124
def required_tools
121125
[]
122126
end

0 commit comments

Comments
 (0)