Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 26f83a0

Browse files
committed
backend for test button
1 parent 2164e50 commit 26f83a0

File tree

4 files changed

+155
-25
lines changed

4 files changed

+155
-25
lines changed

app/controllers/discourse_ai/admin/ai_spam_controller.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,35 @@ def update
4747
render json: AiSpamSerializer.new(spam_config, root: false)
4848
end
4949

50+
def test
51+
url = params[:post_url].to_s
52+
post = nil
53+
54+
if url.match?(/^\d+$/)
55+
post_id = url.to_i
56+
post = Post.find_by(id: post_id)
57+
end
58+
59+
route = UrlHelper.rails_route_from_url(url) if !post
60+
61+
if route
62+
if route[:controller] == "topics"
63+
post_number = route[:post_number] || 1
64+
post = Post.with_deleted.find_by(post_number: post_number, topic_id: route[:topic_id])
65+
end
66+
end
67+
68+
raise Discourse::NotFound if !post
69+
70+
result =
71+
DiscourseAi::AiModeration::SpamScanner.test_post(
72+
post,
73+
custom_instructions: params[:custom_instructions],
74+
)
75+
76+
render json: result
77+
end
78+
5079
private
5180

5281
def allowed_params

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
get "/ai-usage", to: "discourse_ai/admin/ai_usage#show"
8282
get "/ai-spam", to: "discourse_ai/admin/ai_spam#show"
8383
put "/ai-spam", to: "discourse_ai/admin/ai_spam#update"
84+
post "/ai-spam/test", to: "discourse_ai/admin/ai_spam#test"
8485

8586
resources :ai_llms,
8687
only: %i[index create show update destroy],

lib/ai_moderation/spam_scanner.rb

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,26 +119,68 @@ def self.significant_change?(previous_version, current_version)
119119
distance >= MINIMUM_EDIT_DIFFERENCE
120120
end
121121

122-
def self.perform_scan(post)
123-
return if !enabled?
124-
return if !should_scan_post?(post)
125-
122+
def self.test_post(post, custom_instructions: nil)
126123
settings = AiModerationSetting.spam
127-
return if !settings || !settings.llm_model
128-
129124
llm = settings.llm_model.to_llm
130-
custom_instructions = settings.custom_instructions.presence
125+
custom_instructions = custom_instructions || settings.custom_instructions.presence
126+
context = build_context(post)
127+
prompt = completion_prompt(post, context: context, custom_instructions: custom_instructions)
131128

132-
system_prompt = build_system_prompt(custom_instructions)
133-
prompt = DiscourseAi::Completions::Prompt.new(system_prompt)
129+
result =
130+
llm.generate(
131+
prompt,
132+
temperature: 0.1,
133+
max_tokens: 100,
134+
user: Discourse.system_user,
135+
feature_name: "spam_detection_test",
136+
feature_context: {
137+
post_id: post.id,
138+
},
139+
)&.strip
140+
141+
history = nil
142+
AiSpamLog.where(post: post).order(:created_at).limit(100).each do |log|
143+
history ||= +"Scan History:\n"
144+
history << "date: #{log.created_at} is_spam: #{log.is_spam}\n"
145+
end
134146

135-
context = build_context(post)
147+
log = +"Scanning #{post.url}\n\n"
148+
149+
if history
150+
log << history
151+
log << "\n"
152+
end
153+
154+
log << "LLM: #{settings.llm_model.name}\n\n"
155+
log << "System Prompt: #{build_system_prompt(custom_instructions)}\n\n"
156+
log << "Context: #{context}\n\n"
157+
log << "Result: #{result}"
158+
159+
is_spam = (result.present? && result.downcase.include?("spam"))
160+
{ is_spam: is_spam, log: log }
161+
end
136162

163+
def self.completion_prompt(post, context:, custom_instructions:)
164+
system_prompt = build_system_prompt(custom_instructions)
165+
prompt = DiscourseAi::Completions::Prompt.new(system_prompt)
137166
args = { type: :user, content: context }
138167
upload_ids = post.upload_ids
139168
args[:upload_ids] = upload_ids.take(3) if upload_ids.present?
140-
141169
prompt.push(**args)
170+
prompt
171+
end
172+
173+
def self.perform_scan(post)
174+
return if !enabled?
175+
return if !should_scan_post?(post)
176+
177+
settings = AiModerationSetting.spam
178+
return if !settings || !settings.llm_model
179+
180+
context = build_context(post)
181+
llm = settings.llm_model.to_llm
182+
custom_instructions = settings.custom_instructions.presence
183+
prompt = completion_prompt(post, context: context, custom_instructions: custom_instructions)
142184

143185
begin
144186
result =
@@ -156,7 +198,6 @@ def self.perform_scan(post)
156198
is_spam = (result.present? && result.downcase.include?("spam"))
157199

158200
log = AiApiAuditLog.order(id: :desc).where(feature_name: "spam_detection").first
159-
160201
AiSpamLog.transaction do
161202
log =
162203
AiSpamLog.create!(

spec/requests/admin/ai_spam_controller_spec.rb

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,78 @@
7979
end
8080
end
8181

82+
describe "#test" do
83+
fab!(:spam_post) { Fabricate(:post) }
84+
fab!(:spam_post2) { Fabricate(:post, topic: spam_post.topic, raw: "something special 123") }
85+
fab!(:setting) do
86+
AiModerationSetting.create(
87+
{
88+
setting_type: :spam,
89+
llm_model_id: llm_model.id,
90+
data: {
91+
custom_instructions: "custom instructions",
92+
},
93+
},
94+
)
95+
end
96+
97+
before { sign_in(admin) }
98+
99+
it "can scan using post url" do
100+
DiscourseAi::Completions::Llm.with_prepared_responses(["spam"]) do
101+
post "/admin/plugins/discourse-ai/ai-spam/test.json", params: { post_url: spam_post2.url }
102+
end
103+
104+
expect(response.status).to eq(200)
105+
106+
parsed = response.parsed_body
107+
expect(parsed["log"]).to include(spam_post2.raw)
108+
end
109+
110+
it "can scan using post id" do
111+
DiscourseAi::Completions::Llm.with_prepared_responses(["spam"]) do
112+
post "/admin/plugins/discourse-ai/ai-spam/test.json",
113+
params: {
114+
post_url: spam_post.id.to_s,
115+
}
116+
end
117+
118+
expect(response.status).to eq(200)
119+
120+
parsed = response.parsed_body
121+
expect(parsed["log"]).to include(spam_post.raw)
122+
end
123+
124+
it "returns proper spam test results" do
125+
freeze_time DateTime.parse("2000-01-01")
126+
127+
AiSpamLog.create!(
128+
post: spam_post,
129+
llm_model: llm_model,
130+
is_spam: false,
131+
created_at: 2.days.ago,
132+
)
133+
134+
AiSpamLog.create!(post: spam_post, llm_model: llm_model, is_spam: true, created_at: 1.day.ago)
135+
136+
DiscourseAi::Completions::Llm.with_prepared_responses(["spam"]) do
137+
post "/admin/plugins/discourse-ai/ai-spam/test.json",
138+
params: {
139+
post_url: spam_post.url,
140+
custom_instructions: "special custom instructions",
141+
}
142+
end
143+
144+
expect(response.status).to eq(200)
145+
146+
parsed = response.parsed_body
147+
expect(parsed["log"]).to include("special custom instructions")
148+
expect(parsed["log"]).to include(spam_post.raw)
149+
expect(parsed["is_spam"]).to eq(true)
150+
expect(parsed["log"]).to include("Scan History:")
151+
end
152+
end
153+
82154
describe "#show" do
83155
context "when logged in as admin" do
84156
before { sign_in(admin) }
@@ -132,19 +204,6 @@
132204

133205
expect(json["flagging_username"]).to eq(flagging_user.username)
134206
end
135-
136-
it "includes the stats" do
137-
expect(SpamScanner.flagging_user.id).not_to eq(SystemUser.id)
138-
get "/admin/plugins/discourse-ai/ai-spam.json"
139-
140-
json = response.parsed_body
141-
expect(json["stats"]).to include(
142-
"scanned_count",
143-
"spam_detected",
144-
"false_positives",
145-
"false_negatives",
146-
)
147-
end
148207
end
149208

150209
context "when not logged in as admin" do

0 commit comments

Comments
 (0)