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

Commit fc7e213

Browse files
committed
FEATURE: silent triage using tools
1 parent 6aaf8a0 commit fc7e213

File tree

6 files changed

+171
-5
lines changed

6 files changed

+171
-5
lines changed

config/locales/client.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ en:
107107
whisper:
108108
label: "Reply as Whisper"
109109
description: "Whether the persona's response should be a whisper"
110+
silent_mode:
111+
label: "Silent Mode"
112+
description: "In silent mode persona will recieve the content but will not post anything on the forum - useful when performing triage using tools"
110113
llm_triage:
111114
fields:
112115
system_prompt:

discourse_automation/llm_persona_triage.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
content: DiscourseAi::Automation.available_persona_choices,
1515
}
1616
field :whisper, component: :boolean
17+
field :silent_mode, component: :boolean
1718

1819
script do |context, fields|
1920
post = context["post"]
2021
next if post&.user&.bot?
2122

22-
persona_id = fields["persona"]["value"]
23-
whisper = fields["whisper"]["value"]
23+
persona_id = fields.dig("persona", "value")
24+
whisper = !!fields.dig("whisper", "value")
25+
silent_mode = !!fields.dig("silent_mode", "value")
2426

2527
begin
2628
RateLimiter.new(
@@ -42,6 +44,7 @@
4244
persona_id: persona_id,
4345
whisper: whisper,
4446
automation: self.automation,
47+
silent_mode: silent_mode,
4548
)
4649
rescue => e
4750
Discourse.warn_exception(

lib/ai_bot/playground.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ def self.reply_to_post(
188188
whisper: nil,
189189
add_user_to_pm: false,
190190
stream_reply: false,
191-
auto_set_title: false
191+
auto_set_title: false,
192+
silent_mode: false
192193
)
193194
ai_persona = AiPersona.find_by(id: persona_id)
194195
raise Discourse::InvalidParameters.new(:persona_id) if !ai_persona
@@ -207,7 +208,15 @@ def self.reply_to_post(
207208
add_user_to_pm: add_user_to_pm,
208209
stream_reply: stream_reply,
209210
auto_set_title: auto_set_title,
211+
silent_mode: silent_mode,
210212
)
213+
rescue => e
214+
if Rails.env.test?
215+
p e
216+
puts e.backtrace[0..10]
217+
else
218+
raise e
219+
end
211220
end
212221

213222
def initialize(bot)
@@ -475,13 +484,19 @@ def reply_to(
475484
add_user_to_pm: true,
476485
stream_reply: nil,
477486
auto_set_title: true,
487+
silent_mode: false,
478488
&blk
479489
)
480490
# this is a multithreading issue
481491
# post custom prompt is needed and it may not
482492
# be properly loaded, ensure it is loaded
483493
PostCustomPrompt.none
484494

495+
if silent_mode
496+
auto_set_title = false
497+
stream_reply = false
498+
end
499+
485500
reply = +""
486501
post_streamer = nil
487502

@@ -590,7 +605,7 @@ def reply_to(
590605
end
591606
end
592607

593-
return if reply.blank?
608+
return if reply.blank? || silent_mode
594609

595610
if stream_reply
596611
post_streamer.finish

lib/ai_bot/tool_runner.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ def framework_script
8989
},
9090
};
9191
},
92+
createChatMessage: function(params) {
93+
const result = _discourse_create_chat_message(params);
94+
if (result.error) {
95+
throw new Error(result.error);
96+
}
97+
return result;
98+
},
9299
};
93100
94101
const context = #{JSON.generate(@context)};
@@ -345,6 +352,55 @@ def attach_discourse(mini_racer_context)
345352
end,
346353
)
347354

355+
mini_racer_context.attach(
356+
"_discourse_create_chat_message",
357+
->(params) do
358+
in_attached_function do
359+
params = params.symbolize_keys
360+
channel_name = params[:channel_name]
361+
username = params[:username]
362+
message = params[:message]
363+
364+
# Validate parameters
365+
return { error: "Missing required parameter: channel_name" } if channel_name.blank?
366+
return { error: "Missing required parameter: username" } if username.blank?
367+
return { error: "Missing required parameter: message" } if message.blank?
368+
369+
# Find the user
370+
user = User.find_by(username: username)
371+
return { error: "User not found: #{username}" } if user.nil?
372+
373+
# Find the channel
374+
channel = Chat::Channel.find_by(name: channel_name)
375+
if channel.nil?
376+
# Try finding by slug if not found by name
377+
channel = Chat::Channel.find_by(slug: channel_name.parameterize)
378+
end
379+
return { error: "Channel not found: #{channel_name}" } if channel.nil?
380+
381+
begin
382+
guardian = Guardian.new(user)
383+
message =
384+
ChatSDK::Message.create(
385+
raw: message,
386+
channel_id: channel.id,
387+
guardian: guardian,
388+
enforce_membership: !channel.direct_message_channel?,
389+
)
390+
391+
{
392+
success: true,
393+
message_id: message.id,
394+
message: message.message,
395+
created_at: message.created_at.iso8601,
396+
}
397+
rescue => e
398+
{ error: "Failed to create chat message: #{e.message}" }
399+
end
400+
end
401+
end,
402+
)
403+
348404
mini_racer_context.attach(
349405
"_discourse_search",
350406
->(params) do

lib/automation/llm_persona_triage.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
module DiscourseAi
33
module Automation
44
module LlmPersonaTriage
5-
def self.handle(post:, persona_id:, whisper: false, automation: nil)
5+
def self.handle(post:, persona_id:, whisper: false, silent_mode: false, automation: nil)
66
DiscourseAi::AiBot::Playground.reply_to_post(
77
post: post,
88
persona_id: persona_id,
99
whisper: whisper,
10+
silent_mode: silent_mode,
1011
)
1112
rescue => e
1213
Discourse.warn_exception(

spec/lib/discourse_automation/llm_persona_triage_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,92 @@ def add_automation_field(name, value, type: "text")
239239
# should not inject persona into allowed users
240240
expect(topic.topic_allowed_users.pluck(:user_id).sort).to eq(original_user_ids.sort)
241241
end
242+
243+
describe "LLM Persona Triage with Chat Message Creation" do
244+
fab!(:user)
245+
fab!(:bot_user) { Fabricate(:user) }
246+
fab!(:chat_channel) { Fabricate(:category_channel) }
247+
248+
fab!(:custom_tool) do
249+
AiTool.create!(
250+
name: "Chat Notifier",
251+
tool_name: "chat_notifier",
252+
description: "Creates a chat notification in a channel",
253+
parameters: [
254+
{ name: "channel_id", type: "integer", description: "Chat channel ID" },
255+
{ name: "message", type: "string", description: "Message to post" },
256+
],
257+
script: <<~JS,
258+
function invoke(params) {
259+
// Create a chat message using the Chat API
260+
const result = discourse.createChatMessage({
261+
channel_name: '#{chat_channel.name}',
262+
username: '#{user.username}',
263+
message: params.message
264+
});
265+
266+
chain.setCustomRaw("We are done, stopping chaing");
267+
268+
return {
269+
success: true,
270+
message_id: result.message_id,
271+
url: result.url,
272+
message: params.message
273+
};
274+
}
275+
JS
276+
summary: "Notify in chat channel",
277+
created_by: Discourse.system_user,
278+
)
279+
end
280+
281+
before do
282+
SiteSetting.chat_enabled = true
283+
284+
ai_persona.update!(tools: ["custom-#{custom_tool.id}"])
285+
286+
# Set up automation fields
287+
automation.fields.create!(
288+
component: "choices",
289+
name: "persona",
290+
metadata: {
291+
value: ai_persona.id,
292+
},
293+
target: "script",
294+
)
295+
296+
automation.fields.create!(
297+
component: "boolean",
298+
name: "silent_mode",
299+
metadata: {
300+
value: true,
301+
},
302+
target: "script",
303+
)
304+
end
305+
306+
it "can silently analyze a post and create a chat notification" do
307+
post = Fabricate(:post, raw: "Please help with my billing issue")
308+
309+
# Tool response from LLM
310+
tool_call =
311+
DiscourseAi::Completions::ToolCall.new(
312+
name: "chat_notifier",
313+
parameters: {
314+
"message" => "Hello world!",
315+
},
316+
id: "tool_call_1",
317+
)
318+
319+
DiscourseAi::Completions::Llm.with_prepared_responses([tool_call]) do
320+
automation.running_in_background!
321+
automation.trigger!({ "post" => post })
322+
end
323+
324+
expect(post.topic.reload.posts.count).to eq(1)
325+
326+
expect(chat_channel.chat_messages.count).to eq(1)
327+
expect(chat_channel.chat_messages.last.message).to eq("Hello world!")
328+
end
329+
end
242330
end

0 commit comments

Comments
 (0)