From 6817866de99115e6cee0d6788bc507421bce6725 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Jun 2025 16:59:00 +1000 Subject: [PATCH] FEATURE: allow access to assigns from forum researcher (#1412) * FEATURE: allow access to assigns from forum researcher * FIX: should properly be checking for empty * finish PR --- lib/ai_helper/assistant.rb | 2 +- lib/personas/tools/researcher.rb | 13 +++++++ lib/summarization/fold_content.rb | 2 +- lib/utils/research/filter.rb | 28 +++++++++++++++ spec/lib/utils/research/filter_spec.rb | 47 ++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index d9f0eb3d..8213458d 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -134,7 +134,7 @@ module DiscourseAi json_summary_schema_key = bot.persona.response_format&.first.to_h helper_chunk = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym) - if helper_chunk.present? + if !helper_chunk.nil? && !helper_chunk.empty? helper_response << helper_chunk block.call(helper_chunk) if block end diff --git a/lib/personas/tools/researcher.rb b/lib/personas/tools/researcher.rb index 9fce813f..b9e9e3e2 100644 --- a/lib/personas/tools/researcher.rb +++ b/lib/personas/tools/researcher.rb @@ -47,6 +47,7 @@ module DiscourseAi - status:open|closed|archived|noreplies|single_user - topic status filters - max_results:N - limit results (per OR group) - order:latest|oldest|latest_topic|oldest_topic|likes - sort order + #{assign_tip} **OR Logic:** Each OR group processes independently - filters don't cross boundaries. @@ -56,6 +57,16 @@ module DiscourseAi TEXT end + def assign_tip + if SiteSetting.respond_to?(:assign_enabled) && SiteSetting.assign_enabled + (<<~TEXT).strip + assigned_to:username or assigned_to:username1,username2 - topics assigned to a specific user + assigned_to:* - topics assigned to any user + assigned_to:nobody - topics not assigned to any user + TEXT + end + end + def name "researcher" end @@ -113,6 +124,8 @@ module DiscourseAi else process_filter(filter, goals, post, &blk) end + rescue StandardError => e + { error: "Error processing research: #{e.message}" } end def details diff --git a/lib/summarization/fold_content.rb b/lib/summarization/fold_content.rb index 17df679b..8ab43540 100644 --- a/lib/summarization/fold_content.rb +++ b/lib/summarization/fold_content.rb @@ -118,7 +118,7 @@ module DiscourseAi partial_summary = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym) - if partial_summary.present? + if !partial_summary.nil? && !partial_summary.empty? summary << partial_summary on_partial_blk.call(partial_summary) if on_partial_blk end diff --git a/lib/utils/research/filter.rb b/lib/utils/research/filter.rb index 1422622c..eede8600 100644 --- a/lib/utils/research/filter.rb +++ b/lib/utils/research/filter.rb @@ -162,6 +162,34 @@ module DiscourseAi end end + def self.assign_allowed?(guardian) + SiteSetting.respond_to?(:assign_enabled) && SiteSetting.assign_enabled && + (guardian.can_assign? || SiteSetting.assigns_public) + end + + register_filter(/\Aassigned_to:(.+)\z/i) do |relation, name, filter| + if !assign_allowed?(filter.guardian) + raise Discourse::InvalidAccess.new( + "Assigns are not enabled or you do not have permission to see assigns.", + ) + end + + if (name == "nobody") + relation.joins("LEFT JOIN assignments a ON a.topic_id = topics.id AND a.active").where( + "a.assigned_to_id IS NULL", + ) + elsif name == "*" + relation.joins("JOIN assignments a ON a.topic_id = topics.id AND a.active").where( + "a.assigned_to_id IS NOT NULL", + ) + else + usernames = name.split(",").map(&:strip).map(&:downcase) + relation.joins("JOIN assignments a ON a.topic_id = topics.id AND a.active").where( + "a.assigned_to_id" => User.where(username_lower: usernames).select(:id), + ) + end + end + register_filter(/\Agroups?:([a-zA-Z0-9_\-,]+)\z/i) do |relation, groups_param, filter| if groups_param.include?(",") group_names = groups_param.split(",").map(&:strip) diff --git a/spec/lib/utils/research/filter_spec.rb b/spec/lib/utils/research/filter_spec.rb index a08825c2..66fb6e71 100644 --- a/spec/lib/utils/research/filter_spec.rb +++ b/spec/lib/utils/research/filter_spec.rb @@ -57,6 +57,8 @@ describe DiscourseAi::Utils::Research::Filter do fab!(:bug_post) { Fabricate(:post, topic: bug_topic, user: user) } fab!(:feature_bug_post) { Fabricate(:post, topic: feature_bug_topic, user: user) } fab!(:no_tag_post) { Fabricate(:post, topic: no_tag_topic, user: user) } + fab!(:admin1) { Fabricate(:admin, username: "admin1") } + fab!(:admin2) { Fabricate(:admin, username: "admin2") } describe "group filtering" do before do @@ -192,6 +194,51 @@ describe DiscourseAi::Utils::Research::Filter do end end + if SiteSetting.respond_to?(:assign_enabled) + describe "assign filtering" do + before do + SiteSetting.assign_enabled = true + assigner = Assigner.new(feature_topic, admin1) + assigner.assign(admin1) + + assigner = Assigner.new(bug_topic, admin1) + assigner.assign(admin2) + end + + let(:admin_guardian) { Guardian.new(admin1) } + + it "can find topics assigned to a user" do + filter = described_class.new("assigned_to:#{admin1.username}", guardian: admin_guardian) + expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id) + end + + it "can find topics assigned to multiple users" do + filter = + described_class.new( + "assigned_to:#{admin1.username},#{admin2.username}", + guardian: admin_guardian, + ) + expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id, bug_post.id) + end + + it "can find topics assigned to nobody" do + filter = described_class.new("assigned_to:nobody", guardian: admin_guardian) + expect(filter.search.pluck(:id)).to contain_exactly(feature_bug_post.id, no_tag_post.id) + end + + it "can find all assigned topics" do + filter = described_class.new("assigned_to:*", guardian: admin_guardian) + expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id, bug_post.id) + end + + it "raises an error if assigns are disabled" do + SiteSetting.assign_enabled = false + filter = described_class.new("assigned_to:sam") + expect { filter.search }.to raise_error(Discourse::InvalidAccess) + end + end + end + it "can limit number of results" do filter = described_class.new("category:Feedback max_results:1", limit: 5) expect(filter.search.pluck(:id).length).to eq(1)