Skip to content

Commit e255c7a

Browse files
authored
FEATURE: automation triage using personas (#1126)
## LLM Persona Triage - Allows automated responses to posts using AI personas - Configurable to respond as regular posts or whispers - Adds context-aware formatting for topics and private messages - Provides special handling for topic metadata (title, category, tags) ## LLM Tool Triage - Enables custom AI tools to process and respond to posts - Tools can analyze post content and invoke personas when needed - Zero-parameter tools can be used for automated workflows - Not enabled in production yet ## Implementation Details - Added new scriptable registration in discourse_automation/ directory - Created core implementation in lib/automation/ modules - Enhanced PromptMessagesBuilder with topic-style formatting - Added helper methods for persona and tool selection in UI - Extended AI Bot functionality to support whisper responses - Added rate limiting to prevent abuse ## Other Changes - Added comprehensive test coverage for both automation types - Enhanced tool runner with LLM integration capabilities - Improved error handling and logging This feature allows forum admins to configure AI personas to automatically respond to posts based on custom criteria and leverage AI tools for more complex triage workflows. Tool Triage has been disabled in production while we finalize details of new scripting capabilities.
1 parent 8863cf0 commit e255c7a

17 files changed

+903
-65
lines changed

config/locales/client.en.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ en:
9090
label: "Top P"
9191
description: "Top P to use for the LLM, increase to increase randomness (leave empty to use model default)"
9292

93+
llm_tool_triage:
94+
fields:
95+
model:
96+
label: "Model"
97+
description: "The default language model used for triage"
98+
tool:
99+
label: "Tool"
100+
description: "Tool to use for triage (tool must have no parameters defined)"
101+
102+
103+
llm_persona_triage:
104+
fields:
105+
persona:
106+
label: "Persona"
107+
description: "AI Persona to use for triage (must have default LLM and User set)"
108+
whisper:
109+
label: "Reply as Whisper"
110+
description: "Whether the persona's response should be a whisper"
93111
llm_triage:
94112
fields:
95113
system_prompt:

config/locales/server.en.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ en:
66
spam: "Flag as spam and hide post"
77
spam_silence: "Flag as spam, hide post and silence user"
88
scriptables:
9+
llm_tool_triage:
10+
title: Triage posts using AI Tool
11+
description: "Triage posts using custom logic in an AI tool"
12+
llm_persona_triage:
13+
title: Triage posts using AI Persona
14+
description: "Respond to posts using a specific AI persona"
915
llm_triage:
1016
title: Triage posts using AI
1117
description: "Triage posts using a large language model"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
if defined?(DiscourseAutomation)
4+
DiscourseAutomation::Scriptable.add("llm_persona_triage") do
5+
version 1
6+
run_in_background
7+
8+
triggerables %i[post_created_edited]
9+
10+
field :persona,
11+
component: :choices,
12+
required: true,
13+
extra: {
14+
content: DiscourseAi::Automation.available_persona_choices,
15+
}
16+
field :whisper, component: :boolean
17+
18+
script do |context, fields|
19+
post = context["post"]
20+
next if post&.user&.bot?
21+
22+
persona_id = fields["persona"]["value"]
23+
whisper = fields["whisper"]["value"]
24+
25+
begin
26+
RateLimiter.new(
27+
Discourse.system_user,
28+
"llm_persona_triage_#{post.id}",
29+
SiteSetting.ai_automation_max_triage_per_post_per_minute,
30+
1.minute,
31+
).performed!
32+
33+
RateLimiter.new(
34+
Discourse.system_user,
35+
"llm_persona_triage",
36+
SiteSetting.ai_automation_max_triage_per_minute,
37+
1.minute,
38+
).performed!
39+
40+
DiscourseAi::Automation::LlmPersonaTriage.handle(
41+
post: post,
42+
persona_id: persona_id,
43+
whisper: whisper,
44+
automation: self.automation,
45+
)
46+
rescue => e
47+
Discourse.warn_exception(
48+
e,
49+
message: "llm_persona_triage: skipped triage on post #{post.id}",
50+
)
51+
raise e if Rails.env.tests?
52+
end
53+
end
54+
end
55+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
# TODO: this is still highly experimental and subject to a lot of change
4+
# leaving it off in production for now Sam
5+
if defined?(DiscourseAutomation) && !Rails.env.production?
6+
DiscourseAutomation::Scriptable.add("llm_tool_triage") do
7+
version 1
8+
run_in_background
9+
10+
triggerables %i[post_created_edited]
11+
12+
field :tool,
13+
component: :choices,
14+
required: true,
15+
extra: {
16+
content: DiscourseAi::Automation.available_custom_tools,
17+
}
18+
19+
script do |context, fields|
20+
tool_id = fields["tool"]["value"]
21+
post = context["post"]
22+
return if post&.user&.bot?
23+
24+
begin
25+
RateLimiter.new(
26+
Discourse.system_user,
27+
"llm_tool_triage_#{post.id}",
28+
SiteSetting.ai_automation_max_triage_per_post_per_minute,
29+
1.minute,
30+
).performed!
31+
32+
RateLimiter.new(
33+
Discourse.system_user,
34+
"llm_tool_triage",
35+
SiteSetting.ai_automation_max_triage_per_minute,
36+
1.minute,
37+
).performed!
38+
39+
DiscourseAi::Automation::LlmToolTriage.handle(
40+
post: post,
41+
tool_id: tool_id,
42+
automation: self.automation,
43+
)
44+
rescue => e
45+
Discourse.warn_exception(e, message: "llm_tool_triage: skipped triage on post #{post.id}")
46+
end
47+
end
48+
end
49+
end

lib/ai_bot/playground.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def update_playground_with(post)
170170
schedule_bot_reply(post) if can_attach?(post)
171171
end
172172

173-
def conversation_context(post)
173+
def conversation_context(post, style: nil)
174174
# Pay attention to the `post_number <= ?` here.
175175
# We want to inject the last post as context because they are translated differently.
176176

@@ -205,6 +205,7 @@ def conversation_context(post)
205205
)
206206

207207
builder = DiscourseAi::Completions::PromptMessagesBuilder.new
208+
builder.topic = post.topic
208209

209210
context.reverse_each do |raw, username, custom_prompt, upload_ids|
210211
custom_prompt_translation =
@@ -245,7 +246,7 @@ def conversation_context(post)
245246
end
246247
end
247248

248-
builder.to_a
249+
builder.to_a(style: style || (post.topic.private_message? ? :bot : :topic))
249250
end
250251

251252
def title_playground(post, user)
@@ -418,7 +419,7 @@ def get_context(participants:, conversation_context:, user:, skip_tool_details:
418419
result
419420
end
420421

421-
def reply_to(post, custom_instructions: nil, &blk)
422+
def reply_to(post, custom_instructions: nil, whisper: nil, context_style: nil, &blk)
422423
# this is a multithreading issue
423424
# post custom prompt is needed and it may not
424425
# be properly loaded, ensure it is loaded
@@ -428,12 +429,18 @@ def reply_to(post, custom_instructions: nil, &blk)
428429
post_streamer = nil
429430

430431
post_type =
431-
post.post_type == Post.types[:whisper] ? Post.types[:whisper] : Post.types[:regular]
432+
(
433+
if (whisper || post.post_type == Post.types[:whisper])
434+
Post.types[:whisper]
435+
else
436+
Post.types[:regular]
437+
end
438+
)
432439

433440
context =
434441
get_context(
435442
participants: post.topic.allowed_users.map(&:username).join(", "),
436-
conversation_context: conversation_context(post),
443+
conversation_context: conversation_context(post, style: context_style),
437444
user: post.user,
438445
)
439446
context[:post_id] = post.id

0 commit comments

Comments
 (0)