discourse-ai/lib/automation/llm_triage.rb

163 lines
5.5 KiB
Ruby

# frozen_string_literal: true
#
module DiscourseAi
module Automation
module LlmTriage
def self.handle(
post:,
model:,
search_for_text:,
system_prompt:,
category_id: nil,
tags: nil,
canned_reply: nil,
canned_reply_user: nil,
hide_topic: nil,
flag_post: nil,
flag_type: nil,
automation: nil,
max_post_tokens: nil,
stop_sequences: nil,
temperature: nil,
whisper: nil,
reply_persona_id: nil,
action: nil
)
if category_id.blank? && tags.blank? && canned_reply.blank? && hide_topic.blank? &&
flag_post.blank? && reply_persona_id.blank?
raise ArgumentError, "llm_triage: no action specified!"
end
if action == :edit && category_id.blank? && tags.blank? && flag_post.blank? &&
hide_topic.blank?
return
end
llm = DiscourseAi::Completions::Llm.proxy(model)
s_prompt = system_prompt.to_s.sub("%%POST%%", "") # Backwards-compat. We no longer sub this.
prompt = DiscourseAi::Completions::Prompt.new(s_prompt)
content = "title: #{post.topic.title}\n#{post.raw}"
content = llm.tokenizer.truncate(content, max_post_tokens) if max_post_tokens.present?
if post.upload_ids.present?
content = [content]
content.concat(post.upload_ids.map { |upload_id| { upload_id: upload_id } })
end
prompt.push(type: :user, content: content)
result = nil
result =
llm.generate(
prompt,
temperature: temperature,
max_tokens: 700, # ~500 words
user: Discourse.system_user,
stop_sequences: stop_sequences,
feature_name: "llm_triage",
feature_context: {
automation_id: automation&.id,
automation_name: automation&.name,
},
)&.strip
if result.present? && result.downcase.include?(search_for_text.downcase)
user = User.find_by_username(canned_reply_user) if canned_reply_user.present?
original_user = user
user = user || Discourse.system_user
if reply_persona_id.present? && action != :edit
begin
DiscourseAi::AiBot::Playground.reply_to_post(
post: post,
persona_id: reply_persona_id,
whisper: whisper,
user: original_user,
)
rescue StandardError => e
Discourse.warn_exception(
e,
message: "Error responding to: #{post&.url} in LlmTriage.handle",
)
raise e if Rails.env.test?
end
elsif canned_reply.present? && action != :edit
post_type = whisper ? Post.types[:whisper] : Post.types[:regular]
PostCreator.create!(
user,
topic_id: post.topic_id,
raw: canned_reply,
reply_to_post_number: post.post_number,
skip_validations: true,
post_type: post_type,
)
end
changes = {}
changes[:category_id] = category_id if category_id.present?
if SiteSetting.tagging_enabled? && tags.present?
changes[:tags] = post.topic.tags.map(&:name).concat(tags)
end
if changes.present?
first_post = post.topic.posts.where(post_number: 1).first
changes[:bypass_bump] = true
changes[:skip_validations] = true
first_post.revise(Discourse.system_user, changes)
end
post.topic.update!(visible: false) if hide_topic
if flag_post
score_reason =
I18n
.t("discourse_automation.scriptables.llm_triage.flagged_post")
.sub("%%LLM_RESPONSE%%", result)
.sub("%%AUTOMATION_ID%%", automation&.id.to_s)
.sub("%%AUTOMATION_NAME%%", automation&.name.to_s)
if flag_type == :spam || flag_type == :spam_silence
result =
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:spam],
message: score_reason,
queue_for_review: true,
).perform
if flag_type == :spam_silence
if result.success?
SpamRule::AutoSilence.new(post.user, post).silence_user
else
Rails.logger.warn(
"llm_triage: unable to flag post as spam, post action failed for #{post.id} with error: '#{result.errors.full_messages.join(",").truncate(3000)}'",
)
end
end
else
reviewable =
ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
reviewable.add_score(
Discourse.system_user,
ReviewableScore.types[:needs_approval],
reason: score_reason,
force_review: true,
)
# We cannot do this through the PostActionCreator because hiding a post is reserved for auto action flags.
# Those flags are off_topic, inappropiate, and spam. We want a more generic type for triage, so none of those
# fit here.
post.hide!(PostActionType.types[:notify_moderators]) if flag_type == :review_hide
end
end
end
end
end
end
end