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

Commit 6e35293

Browse files
committed
getting basic responder going
1 parent ce29cad commit 6e35293

File tree

4 files changed

+213
-19
lines changed

4 files changed

+213
-19
lines changed

discourse_automation/llm_tool_triage.rb

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@
77

88
triggerables %i[post_created_edited]
99

10-
field :model,
11-
component: :choices,
12-
required: true,
13-
extra: {
14-
content: DiscourseAi::Automation.available_models,
15-
}
16-
1710
field :tool,
1811
component: :choices,
1912
required: true,
@@ -22,17 +15,8 @@
2215
}
2316

2417
script do |context, fields|
25-
model = fields["model"]["value"]
2618
tool_id = fields["tool"]["value"]
2719

28-
category_id = fields.dig("category", "value")
29-
tags = fields.dig("tags", "value")
30-
31-
if post.topic.private_message?
32-
include_personal_messages = fields.dig("include_personal_messages", "value")
33-
next if !include_personal_messages
34-
end
35-
3620
begin
3721
RateLimiter.new(
3822
Discourse.system_user,
@@ -50,10 +34,7 @@
5034

5135
DiscourseAi::Automation::LlmToolTriage.handle(
5236
post: post,
53-
model: model,
5437
tool_id: tool_id,
55-
category_id: category_id,
56-
tags: tags,
5738
automation: self.automation,
5839
)
5940
rescue => e

lib/ai_bot/tool_runner.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def mini_racer_context
3838
attach_index(ctx)
3939
attach_upload(ctx)
4040
attach_chain(ctx)
41+
attach_discourse(ctx)
4142
ctx.eval(framework_script)
4243
ctx
4344
end
@@ -71,6 +72,24 @@ def framework_script
7172
setCustomRaw: _chain_set_custom_raw,
7273
};
7374
75+
const discourse = {
76+
getPost: _discourse_get_post,
77+
getUser: _discourse_get_user,
78+
getPersona: function(name) {
79+
return {
80+
respondTo: function(params) {
81+
result = _discourse_respond_to_persona(name, params);
82+
if (result.error) {
83+
throw new Error(result.error);
84+
}
85+
return result;
86+
},
87+
};
88+
},
89+
};
90+
91+
const context = #{JSON.generate(@context)};
92+
7493
function details() { return ""; };
7594
JS
7695
end
@@ -239,6 +258,86 @@ def attach_chain(mini_racer_context)
239258
mini_racer_context.attach("_chain_set_custom_raw", ->(raw) { self.custom_raw = raw })
240259
end
241260

261+
def attach_discourse(mini_racer_context)
262+
mini_racer_context.attach(
263+
"_discourse_get_post",
264+
->(post_id) do
265+
in_attached_function do
266+
post = Post.find_by(id: post_id)
267+
return nil if post.nil?
268+
guardian = Guardian.new(Discourse.system_user)
269+
recursive_as_json(PostSerializer.new(post, scope: guardian, root: false))
270+
end
271+
end,
272+
)
273+
274+
mini_racer_context.attach(
275+
"_discourse_get_user",
276+
->(user_id_or_username) do
277+
in_attached_function do
278+
user = nil
279+
280+
if user_id_or_username.is_a?(Integer) ||
281+
user_id_or_username.to_i.to_s == user_id_or_username
282+
user = User.find_by(id: user_id_or_username.to_i)
283+
else
284+
user = User.find_by(username: user_id_or_username)
285+
end
286+
287+
return nil if user.nil?
288+
289+
guardian = Guardian.new(Discourse.system_user)
290+
recursive_as_json(UserSerializer.new(user, scope: guardian, root: false))
291+
end
292+
end,
293+
)
294+
295+
mini_racer_context.attach(
296+
"_discourse_respond_to_persona",
297+
->(persona_name, params) do
298+
in_attached_function do
299+
# if we have 1000s of personas this can be slow ... we may need to optimize
300+
persona_class = AiPersona.all_personas.find { |persona| persona.name == persona_name }
301+
return { error: "Persona not found" } if persona_class.nil?
302+
303+
persona = persona_class.new
304+
bot = DiscourseAi::AiBot::Bot.as(@bot_user || persona.user, persona: persona)
305+
playground = DiscourseAi::AiBot::Playground.new(bot)
306+
307+
if @context[:post_id]
308+
post = Post.find_by(id: @context[:post_id])
309+
return { error: "Post not found" } if post.nil?
310+
311+
reply_post = playground.reply_to(post, custom_instructions: params["instructions"])
312+
313+
if reply_post
314+
return(
315+
{ success: true, post_id: reply_post.id, post_number: reply_post.post_number }
316+
)
317+
else
318+
return { error: "Failed to create reply" }
319+
end
320+
elsif @context[:message_id] && @context[:channel_id]
321+
message = Chat::Message.find_by(id: @context[:message_id])
322+
channel = Chat::Channel.find_by(id: @context[:channel_id])
323+
return { error: "Message or channel not found" } if message.nil? || channel.nil?
324+
325+
reply =
326+
playground.reply_to_chat_message(message, channel, @context[:context_post_ids])
327+
328+
if reply
329+
return { success: true, message_id: reply.id }
330+
else
331+
return { error: "Failed to create chat reply" }
332+
end
333+
else
334+
return { error: "No valid context for response" }
335+
end
336+
end
337+
end,
338+
)
339+
end
340+
242341
def attach_upload(mini_racer_context)
243342
mini_racer_context.attach(
244343
"_upload_create",
@@ -343,6 +442,33 @@ def in_attached_function
343442
ensure
344443
self.running_attached_function = false
345444
end
445+
446+
def recursive_as_json(obj)
447+
case obj
448+
when Array
449+
obj.map { |item| recursive_as_json(item) }
450+
when Hash
451+
obj.transform_values { |value| recursive_as_json(value) }
452+
when ActiveModel::Serializer, ActiveModel::ArraySerializer
453+
recursive_as_json(obj.as_json)
454+
when ActiveRecord::Base
455+
recursive_as_json(obj.as_json)
456+
else
457+
# Handle objects that respond to as_json but aren't handled above
458+
if obj.respond_to?(:as_json)
459+
result = obj.as_json
460+
if result.equal?(obj)
461+
# If as_json returned the same object, return it to avoid infinite recursion
462+
result
463+
else
464+
recursive_as_json(result)
465+
end
466+
else
467+
# Primitive values like strings, numbers, booleans, nil
468+
obj
469+
end
470+
end
471+
end
346472
end
347473
end
348474
end

lib/automation/llm_tool_triage.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
module DiscourseAi
3+
module Automation
4+
module LlmToolTriage
5+
def self.handle(post:, tool_id:, automation: nil)
6+
tool = AiTool.find_by(id: tool_id)
7+
return if !tool
8+
return if !tool.parameters.blank?
9+
10+
context = {
11+
post_id: post.id,
12+
automation_id: automation&.id,
13+
automation_name: automation&.name,
14+
}
15+
16+
runner = tool.runner({}, llm: nil, bot_user: Discourse.system_user, context: context)
17+
runner.invoke
18+
end
19+
end
20+
end
21+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe DiscourseAi::Automation::LlmToolTriage do
6+
fab!(:solver) { Fabricate(:user) }
7+
fab!(:new_user) { Fabricate(:user, trust_level: TrustLevel[0], created_at: 1.day.ago) }
8+
fab!(:topic) { Fabricate(:topic, user: new_user) }
9+
fab!(:post) { Fabricate(:post, topic: topic, user: new_user, raw: "How do I reset my password?") }
10+
fab!(:llm_model)
11+
fab!(:ai_persona) do
12+
persona = Fabricate(:ai_persona, default_llm: llm_model)
13+
persona.create_user
14+
persona
15+
end
16+
17+
fab!(:tool) do
18+
tool_script = <<~JS
19+
function invoke(params) {
20+
const postId = context.post_id;
21+
const post = discourse.getPost(postId);
22+
const user = discourse.getUser(post.user_id);
23+
24+
if (user.trust_level > 0) {
25+
return {
26+
processed: false,
27+
reason: "User is not new"
28+
};
29+
}
30+
31+
const helper = discourse.getPersona("#{ai_persona.name}");
32+
const answer = helper.respondTo({ post_id: post.id });
33+
34+
return {
35+
answer: answer,
36+
processed: true,
37+
reason: "answered question"
38+
};
39+
}
40+
JS
41+
42+
AiTool.create!(
43+
name: "New User Question Answerer",
44+
tool_name: "new_user_question_answerer",
45+
description: "Automatically answers questions from new users when possible",
46+
parameters: [], # No parameters as required by llm_tool_triage
47+
script: tool_script,
48+
created_by_id: Discourse.system_user.id,
49+
summary: "Answers new user questions",
50+
enabled: true,
51+
)
52+
end
53+
54+
before do
55+
SiteSetting.discourse_ai_enabled = true
56+
SiteSetting.ai_bot_enabled = true
57+
end
58+
59+
it "It is able to answer new user questions" do
60+
result = nil
61+
DiscourseAi::Completions::Llm.with_prepared_responses(
62+
["this is how you reset your password"],
63+
) { result = described_class.handle(post: post, tool_id: tool.id) }
64+
p result
65+
end
66+
end

0 commit comments

Comments
 (0)