This repository was archived by the owner on Jul 22, 2025. It is now read-only.
generated from discourse/discourse-plugin-skeleton
-
Notifications
You must be signed in to change notification settings - Fork 41
DEV: improve tool infra, improve forum researcher prompts, improve logging #1391
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5f451a6
FIX: pass bot feature name when responding as bot
SamSaffron ae938f5
FEATURE: allow tools to sleep
SamSaffron b5f24a7
add a test
SamSaffron 39a0ef7
prompt engineering and faster exit when research is cancelled
SamSaffron d22d5ad
Add support for getting base64 encoded uploads
SamSaffron 21a814b
FEATURE: allow base 64 encoding results from an http call
SamSaffron 86f665d
also implement encoding for get
SamSaffron 841b0dd
fix regression
SamSaffron 7b4685d
dupe line
SamSaffron a439a48
improve filter logic and feature set
SamSaffron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,9 @@ class ToolRunner | |
| MARSHAL_STACK_DEPTH = 20 | ||
| MAX_HTTP_REQUESTS = 20 | ||
|
|
||
| MAX_SLEEP_CALLS = 30 | ||
| MAX_SLEEP_DURATION_MS = 60_000 | ||
|
|
||
| def initialize(parameters:, llm:, bot_user:, context: nil, tool:, timeout: nil) | ||
| if context && !context.is_a?(DiscourseAi::Personas::BotContext) | ||
| raise ArgumentError, "context must be a BotContext object" | ||
|
|
@@ -28,6 +31,7 @@ def initialize(parameters:, llm:, bot_user:, context: nil, tool:, timeout: nil) | |
| @timeout = timeout || DEFAULT_TIMEOUT | ||
| @running_attached_function = false | ||
|
|
||
| @sleep_calls_made = 0 | ||
| @http_requests_made = 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dup of line 34 above
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice catch |
||
| end | ||
|
|
||
|
|
@@ -44,6 +48,7 @@ def mini_racer_context | |
| attach_index(ctx) | ||
| attach_upload(ctx) | ||
| attach_chain(ctx) | ||
| attach_sleep(ctx) | ||
| attach_discourse(ctx) | ||
| ctx.eval(framework_script) | ||
| ctx | ||
|
|
@@ -73,6 +78,9 @@ def framework_script | |
| const upload = { | ||
| create: _upload_create, | ||
| getUrl: _upload_get_url, | ||
| getBase64: function(id, maxPixels) { | ||
| return _upload_get_base64(id, maxPixels); | ||
| } | ||
| } | ||
|
|
||
| const chain = { | ||
|
|
@@ -310,6 +318,33 @@ def attach_chain(mini_racer_context) | |
| mini_racer_context.attach("_chain_set_custom_raw", ->(raw) { self.custom_raw = raw }) | ||
| end | ||
|
|
||
| # this is useful for polling apis | ||
| def attach_sleep(mini_racer_context) | ||
| mini_racer_context.attach( | ||
| "sleep", | ||
| ->(duration_ms) do | ||
| @sleep_calls_made += 1 | ||
| if @sleep_calls_made > MAX_SLEEP_CALLS | ||
| raise TooManyRequestsError.new("Tool made too many sleep calls") | ||
| end | ||
|
|
||
| duration_ms = duration_ms.to_i | ||
| if duration_ms > MAX_SLEEP_DURATION_MS | ||
| raise ArgumentError.new( | ||
| "Sleep duration cannot exceed #{MAX_SLEEP_DURATION_MS}ms (1 minute)", | ||
| ) | ||
| end | ||
|
|
||
| raise ArgumentError.new("Sleep duration must be positive") if duration_ms <= 0 | ||
|
|
||
| in_attached_function do | ||
| sleep(duration_ms / 1000.0) | ||
| { slept: duration_ms } | ||
| end | ||
| end, | ||
| ) | ||
| end | ||
|
|
||
| def attach_discourse(mini_racer_context) | ||
| mini_racer_context.attach( | ||
| "_discourse_get_post", | ||
|
|
@@ -571,6 +606,42 @@ def attach_discourse(mini_racer_context) | |
| end | ||
|
|
||
| def attach_upload(mini_racer_context) | ||
| mini_racer_context.attach( | ||
| "_upload_get_base64", | ||
| ->(upload_id_or_url, max_pixels) do | ||
| in_attached_function do | ||
| return nil if upload_id_or_url.blank? | ||
|
|
||
| upload = nil | ||
|
|
||
| # Handle both upload ID and short URL | ||
| if upload_id_or_url.to_s.start_with?("upload://") | ||
| # Handle short URL format | ||
| sha1 = Upload.sha1_from_short_url(upload_id_or_url) | ||
| return nil if sha1.blank? | ||
| upload = Upload.find_by(sha1: sha1) | ||
| else | ||
| # Handle numeric ID | ||
| upload_id = upload_id_or_url.to_i | ||
| return nil if upload_id <= 0 | ||
| upload = Upload.find_by(id: upload_id) | ||
| end | ||
|
|
||
| return nil if upload.nil? | ||
|
|
||
| max_pixels = max_pixels&.to_i | ||
| max_pixels = nil if max_pixels && max_pixels <= 0 | ||
|
|
||
| encoded_uploads = | ||
| DiscourseAi::Completions::UploadEncoder.encode( | ||
| upload_ids: [upload.id], | ||
| max_pixels: max_pixels || 10_000_000, # Default to 10M pixels if not specified | ||
| ) | ||
|
|
||
| encoded_uploads.first&.dig(:base64) | ||
| end | ||
| end, | ||
| ) | ||
| mini_racer_context.attach( | ||
| "_upload_get_url", | ||
| ->(short_url) do | ||
|
|
@@ -629,13 +700,18 @@ def attach_http(mini_racer_context) | |
|
|
||
| in_attached_function do | ||
| headers = (options && options["headers"]) || {} | ||
| base64_encode = options && options["base64Encode"] | ||
|
|
||
| result = {} | ||
| DiscourseAi::Personas::Tools::Tool.send_http_request( | ||
| url, | ||
| headers: headers, | ||
| ) do |response| | ||
| result[:body] = response.body | ||
| if base64_encode | ||
| result[:body] = Base64.strict_encode64(response.body) | ||
| else | ||
| result[:body] = response.body | ||
| end | ||
| result[:status] = response.code.to_i | ||
| end | ||
|
|
||
|
|
@@ -658,6 +734,7 @@ def attach_http(mini_racer_context) | |
| in_attached_function do | ||
| headers = (options && options["headers"]) || {} | ||
| body = options && options["body"] | ||
| base64_encode = options && options["base64Encode"] | ||
|
|
||
| result = {} | ||
| DiscourseAi::Personas::Tools::Tool.send_http_request( | ||
|
|
@@ -666,7 +743,11 @@ def attach_http(mini_racer_context) | |
| headers: headers, | ||
| body: body, | ||
| ) do |response| | ||
| result[:body] = response.body | ||
| if base64_encode | ||
| result[:body] = Base64.strict_encode64(response.body) | ||
| else | ||
| result[:body] = response.body | ||
| end | ||
| result[:status] = response.code.to_i | ||
| end | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,19 +33,24 @@ def filter_description | |
| <<~TEXT | ||
| Filter string to target specific content. | ||
| - Supports user (@username) | ||
| - post_type:first - only includes first posts in topics | ||
| - post_type:reply - only replies in topics | ||
| - date ranges (after:YYYY-MM-DD, before:YYYY-MM-DD for posts; topic_after:YYYY-MM-DD, topic_before:YYYY-MM-DD for topics) | ||
| - categories (category:category1,category2) | ||
| - tags (tag:tag1,tag2) | ||
| - groups (group:group1,group2). | ||
| - categories (category:category1,category2 or categories:category1,category2) | ||
| - tags (tag:tag1,tag2 or tags:tag1,tag2) | ||
| - groups (group:group1,group2 or groups:group1,group2) | ||
| - status (status:open, status:closed, status:archived, status:noreplies, status:single_user) | ||
| - keywords (keywords:keyword1,keyword2) - specific words to search for in posts | ||
| - max_results (max_results:10) the maximum number of results to return (optional) | ||
| - order (order:latest, order:oldest, order:latest_topic, order:oldest_topic) - the order of the results (optional) | ||
| - topic (topic:topic_id1,topic_id2) - add specific topics to the filter, topics will unconditionally be included | ||
| - keywords (keywords:keyword1,keyword2) - searches for specific words within post content using full-text search | ||
| - topic_keywords (topic_keywords:keyword1,keyword2) - searches for keywords within topics, returns all posts from matching topics | ||
| - topics (topic:topic_id1,topic_id2 or topics:topic_id1,topic_id2) - target specific topics by ID | ||
| - max_results (max_results:10) - limits the maximum number of results returned (optional) | ||
| - order (order:latest, order:oldest, order:latest_topic, order:oldest_topic, order:likes) - controls result ordering (optional, defaults to latest posts) | ||
|
|
||
| If multiple tags or categories are specified, they are treated as OR conditions. | ||
| Multiple filters can be combined with spaces for AND logic. Example: '@sam after:2023-01-01 tag:feature' | ||
|
|
||
| Multiple filters can be combined with spaces. Example: '@sam after:2023-01-01 tag:feature' | ||
| Use OR to combine filter segments for inclusive logic. | ||
| Example: 'category:feature,bug OR tag:feature-tag' - includes posts in feature OR bug categories, OR posts with feature-tag tag | ||
| Example: '@sam category:bug' - includes posts by @sam AND in bug category | ||
| TEXT | ||
| end | ||
|
|
||
|
|
@@ -145,10 +150,23 @@ def process_filter(filter, goals, post, &blk) | |
| results = [] | ||
|
|
||
| formatter.each_chunk { |chunk| results << run_inference(chunk[:text], goals, post, &blk) } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should you also do a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it breaks early enough in run inf not to matter. |
||
| { dry_run: false, goals: goals, filter: @filter, results: results } | ||
|
|
||
| if context.cancel_manager&.cancelled? | ||
| { | ||
| dry_run: false, | ||
| goals: goals, | ||
| filter: @filter, | ||
| results: "Cancelled by user", | ||
| cancelled_by_user: true, | ||
| } | ||
| else | ||
| { dry_run: false, goals: goals, filter: @filter, results: results } | ||
| end | ||
| end | ||
|
|
||
| def run_inference(chunk_text, goals, post, &blk) | ||
| return if context.cancel_manager&.cancelled? | ||
|
|
||
| system_prompt = goal_system_prompt(goals) | ||
| user_prompt = goal_user_prompt(goals, chunk_text) | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to pass this from the job caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm maybe ... this job though is dedicated to bot.