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

Commit 1f3c2e2

Browse files
committed
improve implementation
1 parent 00f4de9 commit 1f3c2e2

File tree

6 files changed

+153
-51
lines changed

6 files changed

+153
-51
lines changed

discourse_automation/llm_triage.rb

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
field :reply_persona,
4343
component: :choices,
4444
extra: {
45-
content: DiscourseAi::Automation.available_persona_choices,
45+
content:
46+
DiscourseAi::Automation.available_persona_choices(
47+
require_user: false,
48+
require_default_llm: true,
49+
),
4650
}
4751
field :whisper, component: :boolean
4852

4953
script do |context, fields|
50-
post = context["post"]
51-
5254
post = context["post"]
5355
next if post&.user&.bot?
5456

@@ -59,16 +61,16 @@
5961

6062
canned_reply = fields.dig("canned_reply", "value")
6163
canned_reply_user = fields.dig("canned_reply_user", "value")
62-
reply_persona_id = fields["reply_persona"]["value"]
63-
whisper = fields["whisper"]["value"]
64+
reply_persona_id = fields.dig("reply_persona", "value")
65+
whisper = fields.dig("whisper", "value")
6466

6567
# nothing to do if we already replied
6668
next if post.user.username == canned_reply_user
6769
next if post.raw.strip == canned_reply.to_s.strip
6870

69-
system_prompt = fields["system_prompt"]["value"]
70-
search_for_text = fields["search_for_text"]["value"]
71-
model = fields["model"]["value"]
71+
system_prompt = fields.dig("system_prompt", "value")
72+
search_for_text = fields.dig("search_for_text", "value")
73+
model = fields.dig("model", "value")
7274

7375
category_id = fields.dig("category", "value")
7476
tags = fields.dig("tags", "value")
@@ -120,9 +122,13 @@
120122
stop_sequences: stop_sequences,
121123
automation: self.automation,
122124
temperature: temperature,
125+
action: context["action"],
123126
)
124127
rescue => e
125-
Discourse.warn_exception(e, message: "llm_triage: skipped triage on post #{post.id}")
128+
Discourse.warn_exception(
129+
e,
130+
message: "llm_triage: skipped triage on post #{post.id} #{post.url}",
131+
)
126132
end
127133
end
128134
end

lib/ai_bot/playground.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ def self.schedule_reply(post)
162162
end
163163
end
164164

165+
def self.reply_to_post(post:, user: nil, persona_id: nil, whisper: nil)
166+
ai_persona = AiPersona.find_by(id: persona_id)
167+
raise Discourse::InvalidParameters.new(:persona_id) if !ai_persona
168+
persona_class = ai_persona.class_instance
169+
persona = persona_class.new
170+
171+
bot_user = user || ai_persona.user
172+
raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
173+
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
174+
playground = DiscourseAi::AiBot::Playground.new(bot)
175+
176+
playground.reply_to(post, whisper: whisper, context_style: :topic)
177+
end
178+
165179
def initialize(bot)
166180
@bot = bot
167181
end

lib/automation.rb

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,17 @@ def self.available_models
3838
values
3939
end
4040

41-
def self.available_persona_choices
42-
AiPersona
43-
.joins(:user)
44-
.where.not(user_id: nil)
45-
.where.not(default_llm: nil)
46-
.map do |persona|
47-
{
48-
id: persona.id,
49-
translated_name: persona.name,
50-
description: "#{persona.name} (#{persona.user.username})",
51-
}
52-
end
41+
def self.available_persona_choices(require_user: true, require_default_llm: true)
42+
relation = AiPersona.joins(:user)
43+
relation = relation.where.not(user_id: nil) if require_user
44+
relation = relation.where.not(default_llm: nil) if require_default_llm
45+
relation.map do |persona|
46+
{
47+
id: persona.id,
48+
translated_name: persona.name,
49+
description: "#{persona.name} (#{persona.user.username})",
50+
}
51+
end
5352
end
5453
end
5554
end

lib/automation/llm_persona_triage.rb

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ module DiscourseAi
33
module Automation
44
module LlmPersonaTriage
55
def self.handle(post:, persona_id:, whisper: false, automation: nil)
6-
ai_persona = AiPersona.find_by(id: persona_id)
7-
return if ai_persona.nil?
8-
9-
persona_class = ai_persona.class_instance
10-
persona = persona_class.new
11-
12-
bot_user = ai_persona.user
13-
return if bot_user.nil?
14-
15-
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
16-
playground = DiscourseAi::AiBot::Playground.new(bot)
17-
18-
playground.reply_to(post, whisper: whisper, context_style: :topic)
6+
DiscourseAi::AiBot::Playground.reply_to_post(
7+
post: post,
8+
persona_id: persona_id,
9+
whisper: whisper,
10+
)
1911
rescue => e
20-
Rails.logger.error("Error in LlmPersonaTriage: #{e.message}\n#{e.backtrace.join("\n")}")
12+
Discourse.warn_exception(
13+
e,
14+
message: "Error responding to: #{post&.url} in LlmPersonaTriage.handle",
15+
)
2116
raise e if Rails.env.test?
2217
nil
2318
end

lib/automation/llm_triage.rb

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ def self.handle(
2020
stop_sequences: nil,
2121
temperature: nil,
2222
whisper: nil,
23-
reply_persona_id: nil
23+
reply_persona_id: nil,
24+
action: nil
2425
)
2526
if category_id.blank? && tags.blank? && canned_reply.blank? && hide_topic.blank? &&
26-
flag_post.blank?
27+
flag_post.blank? && reply_persona_id.blank?
2728
raise ArgumentError, "llm_triage: no action specified!"
2829
end
2930

31+
if action == :edit && category_id.blank? && tags.blank? && flag_post.blank? &&
32+
hide_topic.blank?
33+
return
34+
end
35+
3036
llm = DiscourseAi::Completions::Llm.proxy(model)
3137

3238
s_prompt = system_prompt.to_s.sub("%%POST%%", "") # Backwards-compat. We no longer sub this.
@@ -56,22 +62,24 @@ def self.handle(
5662

5763
if result.present? && result.downcase.include?(search_for_text.downcase)
5864
user = User.find_by_username(canned_reply_user) if canned_reply_user.present?
65+
original_user = user
5966
user = user || Discourse.system_user
60-
if reply_persona_id.present?
61-
ai_persona = AiPersona.find_by(id: persona_id)
62-
if ai_persona.present?
63-
persona_class = ai_persona.class_instance
64-
persona = persona_class.new
65-
66-
bot_user = ai_persona.user
67-
if bot_user.nil?
68-
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
69-
playground = DiscourseAi::AiBot::Playground.new(bot)
70-
71-
playground.reply_to(post, whisper: whisper, context_style: :topic)
72-
end
67+
if reply_persona_id.present? && action != :edit
68+
begin
69+
DiscourseAi::AiBot::Playground.reply_to_post(
70+
post: post,
71+
persona_id: reply_persona_id,
72+
whisper: whisper,
73+
user: original_user,
74+
)
75+
rescue StandardError => e
76+
Discourse.warn_exception(
77+
e,
78+
message: "Error responding to: #{post&.url} in LlmTriage.handle",
79+
)
80+
raise e if Rails.env.test?
7381
end
74-
elsif canned_reply.present?
82+
elsif canned_reply.present? && action != :edit
7583
post_type = whisper ? Post.types[:whisper] : Post.types[:regular]
7684
PostCreator.create!(
7785
user,

spec/lib/discourse_automation/llm_triage_spec.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,84 @@ def add_automation_field(name, value, type: "text")
123123
last_post = post.topic.reload.posts.order(:post_number).last
124124
expect(last_post.raw).to eq post.raw
125125
end
126+
127+
it "can respond using an AI persona when configured" do
128+
bot_user = Fabricate(:user, username: "ai_assistant")
129+
ai_persona =
130+
Fabricate(
131+
:ai_persona,
132+
name: "Help Bot",
133+
description: "AI assistant for forum help",
134+
system_prompt: "You are a helpful forum assistant",
135+
default_llm: llm_model,
136+
user_id: bot_user.id,
137+
)
138+
139+
# Configure the automation to use the persona instead of canned reply
140+
add_automation_field("canned_reply", nil, type: "message") # Clear canned reply
141+
add_automation_field("reply_persona", ai_persona.id, type: "choices")
142+
add_automation_field("whisper", true, type: "boolean")
143+
144+
post = Fabricate(:post, raw: "I need help with a problem")
145+
146+
ai_response = "I'll help you with your problem!"
147+
148+
# Set up the test to provide both the triage and the persona responses
149+
DiscourseAi::Completions::Llm.with_prepared_responses(["bad", ai_response]) do
150+
automation.running_in_background!
151+
automation.trigger!({ "post" => post })
152+
end
153+
154+
# Verify the response was created
155+
topic = post.topic.reload
156+
last_post = topic.posts.order(:post_number).last
157+
158+
# Verify the AI persona's user created the post
159+
expect(last_post.user_id).to eq(bot_user.id)
160+
161+
# Verify the content matches the AI response
162+
expect(last_post.raw).to eq(ai_response)
163+
164+
# Verify it's a whisper post (since we set whisper: true)
165+
expect(last_post.post_type).to eq(Post.types[:whisper])
166+
end
167+
168+
it "does not create replies when the action is edit" do
169+
# Set up bot user and persona
170+
bot_user = Fabricate(:user, username: "helper_bot")
171+
ai_persona =
172+
Fabricate(
173+
:ai_persona,
174+
name: "Edit Helper",
175+
description: "AI assistant for editing",
176+
system_prompt: "You help with editing",
177+
default_llm: llm_model,
178+
user_id: bot_user.id,
179+
)
180+
181+
# Configure the automation with both reply methods
182+
add_automation_field("canned_reply", "This is a canned reply", type: "message")
183+
add_automation_field("reply_persona", ai_persona.id, type: "choices")
184+
185+
# Create a post and capture its topic
186+
post = Fabricate(:post, raw: "This needs to be evaluated")
187+
topic = post.topic
188+
189+
# Get initial post count
190+
initial_post_count = topic.posts.count
191+
192+
# Run automation with action: :edit and a matching response
193+
DiscourseAi::Completions::Llm.with_prepared_responses(["bad"]) do
194+
automation.running_in_background!
195+
automation.trigger!({ "post" => post, "action" => :edit })
196+
end
197+
198+
# Topic should be updated (if configured) but no new posts
199+
topic.reload
200+
expect(topic.posts.count).to eq(initial_post_count)
201+
202+
# Verify no replies were created
203+
last_post = topic.posts.order(:post_number).last
204+
expect(last_post.id).to eq(post.id)
205+
end
126206
end

0 commit comments

Comments
 (0)