diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6258194d..077e5011 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -62,6 +62,9 @@ en: allow_secure_categories: label: "Allow secure categories" description: "Allow the report to be generated for topics in secure categories" + suppress_notifications: + label: "Suppress Notifications" + description: "Suppress notifications the report may generate by transforming to content. This will remap mentions and internal links." debug_mode: label: "Debug Mode" description: "Enable debug mode to see the raw input and output of the LLM" diff --git a/discourse_automation/llm_report.rb b/discourse_automation/llm_report.rb index bed4e157..4ca4a983 100644 --- a/discourse_automation/llm_report.rb +++ b/discourse_automation/llm_report.rb @@ -40,6 +40,7 @@ if defined?(DiscourseAutomation) field :top_p, component: :text, required: true, default_value: 0.1 field :temperature, component: :text, required: true, default_value: 0.2 + field :suppress_notifications, component: :boolean field :debug_mode, component: :boolean script do |context, fields, automation| @@ -70,6 +71,7 @@ if defined?(DiscourseAutomation) temperature = 0.2 temperature = fields.dig("temperature", "value").to_f if fields.dig("temperature", "value") + suppress_notifications = !!fields.dig("suppress_notifications", "value") DiscourseAi::Automation::ReportRunner.run!( sender_username: sender, receivers: receivers, @@ -90,6 +92,7 @@ if defined?(DiscourseAutomation) exclude_tags: exclude_tags, temperature: temperature, top_p: top_p, + suppress_notifications: suppress_notifications, ) rescue => e Discourse.warn_exception e, message: "Error running LLM report!" diff --git a/lib/automation/report_runner.rb b/lib/automation/report_runner.rb index c8086602..029b22f2 100644 --- a/lib/automation/report_runner.rb +++ b/lib/automation/report_runner.rb @@ -52,7 +52,8 @@ module DiscourseAi exclude_category_ids: nil, exclude_tags: nil, top_p: 0.1, - temperature: 0.2 + temperature: 0.2, + suppress_notifications: false ) @sender = User.find_by(username: sender_username) @receivers = User.where(username: receivers) @@ -84,6 +85,7 @@ module DiscourseAi @top_p = nil if top_p <= 0 @temperature = nil if temperature <= 0 + @suppress_notifications = suppress_notifications if !@topic_id && !@receivers.present? && !@email_receivers.present? raise ArgumentError, "Must specify topic_id or receivers" @@ -160,6 +162,8 @@ Follow the provided writing composition instructions carefully and precisely ste receiver_usernames = @receivers.map(&:username).join(",") + result = suppress_notifications(result) if @suppress_notifications + if @topic_id PostCreator.create!(@sender, raw: result, topic_id: @topic_id, skip_validations: true) # no debug mode for topics, it is too noisy @@ -220,6 +224,44 @@ Follow the provided writing composition instructions carefully and precisely ste "anthropic:#{model}" end end + + private + + def suppress_notifications(raw) + cooked = PrettyText.cook(raw, sanitize: false) + parsed = Nokogiri::HTML5.fragment(cooked) + + parsed + .css("a") + .each do |a| + href = a["href"] + if href.present? && (href.start_with?("#{Discourse.base_url}") || href.start_with?("/")) + begin + uri = URI.parse(href) + if uri.query.present? + params = CGI.parse(uri.query) + params["silent"] = "true" + uri.query = URI.encode_www_form(params) + else + uri.query = "silent=true" + end + a["href"] = uri.to_s + rescue URI::InvalidURIError + # skip + end + end + end + + parsed + .css("span.mention") + .each do |mention| + mention.replace( + "#{mention.text}", + ) + end + + parsed.to_html + end end end end diff --git a/spec/lib/modules/automation/report_runner_spec.rb b/spec/lib/modules/automation/report_runner_spec.rb index 1a2b1df9..a2564d95 100644 --- a/spec/lib/modules/automation/report_runner_spec.rb +++ b/spec/lib/modules/automation/report_runner_spec.rb @@ -81,6 +81,55 @@ module DiscourseAi expect(debugging).not_to include(post_in_category.raw) end + it "can suppress notifications by remapping content" do + markdown = <<~MD + @sam is a person + [test1](/test) is an internal link + [test2](/test?1=2) is an internal link + [test3](https://example.com) is an external link + [test4](#{Discourse.base_url}) is an internal link + test5 is an internal link + [test6](/test?test=test#anchor) is an internal link with fragment + [test7](//[[test) is a link with an invalid URL + MD + + DiscourseAi::Completions::Llm.with_prepared_responses([markdown]) do + ReportRunner.run!( + sender_username: user.username, + receivers: [receiver.username], + title: "test report", + model: "gpt-4", + category_ids: nil, + tags: nil, + allow_secure_categories: false, + debug_mode: false, + sample_size: 100, + instructions: "make a magic report", + days: 7, + offset: 0, + priority_group_id: nil, + tokens_per_post: 150, + suppress_notifications: true, + ) + end + + report = Topic.where(title: "test report").first + + # note, magic surprise & is correct HTML 5 representation + expected = <<~HTML +
@sam is a person
+ test1 is an internal link
+ test2 is an internal link
+ test3 is an external link
+ test4 is an internal link
+ test5 is an internal link
+ test6 is an internal link with fragment
+ test7 is a link with an invalid URL