From d7ed8180afc3e50e50813efee2a780b0cc95c024 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 16 Mar 2024 08:05:03 +1100 Subject: [PATCH] FEATURE: allow suppression of notifications from report generation (#533) * FEATURE: allow suppression of notifications from report generation Previously we needed to do this by hand, unfortunately this uses up too many tokens and is very hard to discover. New option means that we can trivially disable notifications without needing any prompt engineering. * URI.parse is safer, use it --- config/locales/client.en.yml | 3 ++ discourse_automation/llm_report.rb | 3 ++ lib/automation/report_runner.rb | 44 ++++++++++++++++- .../modules/automation/report_runner_spec.rb | 49 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) 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

+ HTML + + expect(report.ordered_posts.first.raw.strip).to eq(expected.strip) + end + it "can exclude tags" do freeze_time