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

Commit 3e74f09

Browse files
authored
FEATURE: improve custom tool infra (#1463)
- Add support for `chain.streamCustomRaw(test)` that can be used to stream text from a JS tool direct to composer - Add support for llm params in `llm.generate` which unlocks stuff like structured outputs - Add discourse.createStagedUser, discourse.createTopic and discourse.createPost - for content creation
1 parent 3cfc749 commit 3e74f09

File tree

3 files changed

+512
-5
lines changed

3 files changed

+512
-5
lines changed

lib/personas/tool_runner.rb

Lines changed: 205 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def framework_script
6868
6969
const llm = {
7070
truncate: _llm_truncate,
71-
generate: _llm_generate,
71+
generate: function(prompt, options) { return _llm_generate(prompt, options); },
7272
};
7373
7474
const index = {
@@ -85,6 +85,7 @@ def framework_script
8585
8686
const chain = {
8787
setCustomRaw: _chain_set_custom_raw,
88+
streamCustomRaw: _chain_stream_custom_raw,
8889
};
8990
9091
const discourse = {
@@ -132,6 +133,27 @@ def framework_script
132133
}
133134
return result;
134135
},
136+
createStagedUser: function(params) {
137+
const result = _discourse_create_staged_user(params);
138+
if (result.error) {
139+
throw new Error(result.error);
140+
}
141+
return result;
142+
},
143+
createTopic: function(params) {
144+
const result = _discourse_create_topic(params);
145+
if (result.error) {
146+
throw new Error(result.error);
147+
}
148+
return result;
149+
},
150+
createPost: function(params) {
151+
const result = _discourse_create_post(params);
152+
if (result.error) {
153+
throw new Error(result.error);
154+
}
155+
return result;
156+
},
135157
};
136158
137159
const context = #{JSON.generate(@context.to_json)};
@@ -182,11 +204,14 @@ def eval_with_timeout(script, timeout: nil)
182204
t&.join
183205
end
184206

185-
def invoke
207+
def invoke(progress_callback: nil)
208+
@progress_callback = progress_callback
186209
mini_racer_context.eval(tool.script)
187210
eval_with_timeout("invoke(#{JSON.generate(parameters)})")
188211
rescue MiniRacer::ScriptTerminatedError
189212
{ error: "Script terminated due to timeout" }
213+
ensure
214+
@progress_callback = nil
190215
end
191216

192217
def has_custom_context?
@@ -258,12 +283,22 @@ def attach_truncate(mini_racer_context)
258283

259284
mini_racer_context.attach(
260285
"_llm_generate",
261-
->(prompt) do
286+
->(prompt, options) do
262287
in_attached_function do
288+
options ||= {}
289+
response_format = options["response_format"]
290+
if response_format && !response_format.is_a?(Hash)
291+
raise Discourse::InvalidParameters.new("response_format must be a hash")
292+
end
263293
@llm.generate(
264294
convert_js_prompt_to_ruby(prompt),
265295
user: llm_user,
266296
feature_name: "custom_tool_#{tool.name}",
297+
response_format: response_format,
298+
temperature: options["temperature"],
299+
top_p: options["top_p"],
300+
max_tokens: options["max_tokens"],
301+
stop_sequences: options["stop_sequences"],
267302
)
268303
end
269304
end,
@@ -316,6 +351,13 @@ def attach_index(mini_racer_context)
316351

317352
def attach_chain(mini_racer_context)
318353
mini_racer_context.attach("_chain_set_custom_raw", ->(raw) { self.custom_raw = raw })
354+
mini_racer_context.attach(
355+
"_chain_stream_custom_raw",
356+
->(raw) do
357+
self.custom_raw = raw
358+
@progress_callback.call(raw) if @progress_callback
359+
end,
360+
)
319361
end
320362

321363
# this is useful for polling apis
@@ -499,6 +541,166 @@ def attach_discourse(mini_racer_context)
499541
end,
500542
)
501543

544+
mini_racer_context.attach(
545+
"_discourse_create_staged_user",
546+
->(params) do
547+
in_attached_function do
548+
params = params.symbolize_keys
549+
email = params[:email]
550+
username = params[:username]
551+
name = params[:name]
552+
553+
# Validate parameters
554+
return { error: "Missing required parameter: email" } if email.blank?
555+
return { error: "Missing required parameter: username" } if username.blank?
556+
557+
# Check if user already exists
558+
existing_user = User.find_by_email(email) || User.find_by_username(username)
559+
return { error: "User already exists", user_id: existing_user.id } if existing_user
560+
561+
begin
562+
user =
563+
User.create!(
564+
email: email,
565+
username: username,
566+
name: name || username,
567+
staged: true,
568+
approved: true,
569+
trust_level: TrustLevel[0],
570+
)
571+
572+
{ success: true, user_id: user.id, username: user.username, email: user.email }
573+
rescue => e
574+
{ error: "Failed to create staged user: #{e.message}" }
575+
end
576+
end
577+
end,
578+
)
579+
580+
mini_racer_context.attach(
581+
"_discourse_create_topic",
582+
->(params) do
583+
in_attached_function do
584+
params = params.symbolize_keys
585+
category_name = params[:category_name]
586+
category_id = params[:category_id]
587+
title = params[:title]
588+
raw = params[:raw]
589+
username = params[:username]
590+
tags = params[:tags]
591+
592+
if category_id.blank? && category_name.blank?
593+
return { error: "Missing required parameter: category_id or category_name" }
594+
end
595+
return { error: "Missing required parameter: title" } if title.blank?
596+
return { error: "Missing required parameter: raw" } if raw.blank?
597+
598+
user =
599+
if username.present?
600+
User.find_by(username: username)
601+
else
602+
Discourse.system_user
603+
end
604+
return { error: "User not found: #{username}" } if user.nil?
605+
606+
category =
607+
if category_id.present?
608+
Category.find_by(id: category_id)
609+
else
610+
Category.find_by(name: category_name) || Category.find_by(slug: category_name)
611+
end
612+
613+
return { error: "Category not found" } if category.nil?
614+
615+
begin
616+
post_creator =
617+
PostCreator.new(
618+
user,
619+
title: title,
620+
raw: raw,
621+
category: category.id,
622+
tags: tags,
623+
skip_validations: true,
624+
guardian: Guardian.new(Discourse.system_user),
625+
)
626+
627+
post = post_creator.create
628+
629+
if post_creator.errors.present?
630+
return { error: post_creator.errors.full_messages.join(", ") }
631+
end
632+
633+
{
634+
success: true,
635+
topic_id: post.topic_id,
636+
post_id: post.id,
637+
topic_slug: post.topic.slug,
638+
topic_url: post.topic.url,
639+
}
640+
rescue => e
641+
{ error: "Failed to create topic: #{e.message}" }
642+
end
643+
end
644+
end,
645+
)
646+
647+
mini_racer_context.attach(
648+
"_discourse_create_post",
649+
->(params) do
650+
in_attached_function do
651+
params = params.symbolize_keys
652+
topic_id = params[:topic_id]
653+
raw = params[:raw]
654+
username = params[:username]
655+
reply_to_post_number = params[:reply_to_post_number]
656+
657+
# Validate parameters
658+
return { error: "Missing required parameter: topic_id" } if topic_id.blank?
659+
return { error: "Missing required parameter: raw" } if raw.blank?
660+
661+
# Find the user
662+
user =
663+
if username.present?
664+
User.find_by(username: username)
665+
else
666+
Discourse.system_user
667+
end
668+
return { error: "User not found: #{username}" } if user.nil?
669+
670+
# Verify topic exists
671+
topic = Topic.find_by(id: topic_id)
672+
return { error: "Topic not found" } if topic.nil?
673+
674+
begin
675+
post_creator =
676+
PostCreator.new(
677+
user,
678+
raw: raw,
679+
topic_id: topic_id,
680+
reply_to_post_number: reply_to_post_number,
681+
skip_validations: true,
682+
guardian: Guardian.new(Discourse.system_user),
683+
)
684+
685+
post = post_creator.create
686+
687+
if post_creator.errors.present?
688+
return { error: post_creator.errors.full_messages.join(", ") }
689+
end
690+
691+
{
692+
success: true,
693+
post_id: post.id,
694+
post_number: post.post_number,
695+
cooked: post.cooked,
696+
}
697+
rescue => e
698+
{ error: "Failed to create post: #{e.message}" }
699+
end
700+
end
701+
end,
702+
)
703+
502704
mini_racer_context.attach(
503705
"_discourse_search",
504706
->(params) do

lib/personas/tools/custom.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,16 @@ def initialize(*args, **kwargs)
6666
super(*args, **kwargs)
6767
end
6868

69-
def invoke
70-
result = runner.invoke
69+
def invoke(&blk)
70+
callback =
71+
proc do |raw|
72+
if blk
73+
self.custom_raw = raw
74+
@chain_next_response = false
75+
blk.call(raw, true)
76+
end
77+
end
78+
result = runner.invoke(progress_callback: callback)
7179
if runner.custom_raw
7280
self.custom_raw = runner.custom_raw
7381
@chain_next_response = false

0 commit comments

Comments
 (0)