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
This commit is contained in:
Sam 2024-03-16 08:05:03 +11:00 committed by GitHub
parent dfc13fc631
commit d7ed8180af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 1 deletions

View File

@ -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"

View File

@ -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!"

View File

@ -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(
"<a href='/u/#{mention.text.sub("@", "")}' class='mention'>#{mention.text}</a>",
)
end
parsed.to_html
end
end
end
end

View File

@ -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
<a href='/test'>test5</a> 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 &amp; is correct HTML 5 representation
expected = <<~HTML
<p><a href="/u/sam" class="mention">@sam</a> is a person<br>
<a href="/test?silent=true">test1</a> is an internal link<br>
<a href="/test?1=2&amp;silent=true">test2</a> is an internal link<br>
<a href="https://example.com" rel="noopener nofollow ugc">test3</a> is an external link<br>
<a href="http://test.localhost?silent=true">test4</a> is an internal link<br>
<a href="/test?silent=true">test5</a> is an internal link<br>
<a href="/test?test=test&amp;silent=true#anchor">test6</a> is an internal link with fragment<br>
<a href="//%5B%5Btest?silent=true" rel="noopener nofollow ugc">test7</a> is a link with an invalid URL</p>
HTML
expect(report.ordered_posts.first.raw.strip).to eq(expected.strip)
end
it "can exclude tags" do
freeze_time