discourse-ai/spec/lib/discourse_automation/llm_persona_triage_spec.rb
Sam e255c7a8f0
FEATURE: automation triage using personas (#1126)
## LLM Persona Triage
- Allows automated responses to posts using AI personas
- Configurable to respond as regular posts or whispers
- Adds context-aware formatting for topics and private messages
- Provides special handling for topic metadata (title, category, tags)

## LLM Tool Triage
- Enables custom AI tools to process and respond to posts
- Tools can analyze post content and invoke personas when needed
- Zero-parameter tools can be used for automated workflows
- Not enabled in production yet

## Implementation Details
- Added new scriptable registration in discourse_automation/ directory
- Created core implementation in lib/automation/ modules
- Enhanced PromptMessagesBuilder with topic-style formatting
- Added helper methods for persona and tool selection in UI
- Extended AI Bot functionality to support whisper responses
- Added rate limiting to prevent abuse

## Other Changes
- Added comprehensive test coverage for both automation types
- Enhanced tool runner with LLM integration capabilities
- Improved error handling and logging

This feature allows forum admins to configure AI personas to automatically respond to posts based on custom criteria and leverage AI tools for more complex triage workflows.

Tool Triage has been disabled in production while we finalize details of new scripting capabilities.
2025-03-06 09:41:09 +11:00

209 lines
6.1 KiB
Ruby

# frozen_string_literal: true
return if !defined?(DiscourseAutomation)
describe DiscourseAi::Automation::LlmPersonaTriage do
fab!(:user)
fab!(:bot_user) { Fabricate(:user) }
fab!(:llm_model) do
Fabricate(:llm_model, provider: "anthropic", name: "claude-3-opus", enabled_chat_bot: true)
end
fab!(:ai_persona) do
persona =
Fabricate(
:ai_persona,
name: "Triage Helper",
description: "A persona that helps with triaging posts",
system_prompt: "You are a helpful assistant that triages posts",
default_llm: llm_model,
)
# Create the user for this persona
persona.update!(user_id: bot_user.id)
persona
end
let(:automation) { Fabricate(:automation, script: "llm_persona_triage", enabled: true) }
def add_automation_field(name, value, type: "text")
automation.fields.create!(
component: type,
name: name,
metadata: {
value: value,
},
target: "script",
)
end
before do
SiteSetting.ai_bot_enabled = true
SiteSetting.ai_bot_allowed_groups = "#{Group::AUTO_GROUPS[:trust_level_0]}"
add_automation_field("persona", ai_persona.id, type: "choices")
add_automation_field("whisper", false, type: "boolean")
end
it "can respond to a post using the specified persona" do
post = Fabricate(:post, raw: "This is a test post that needs triage")
response_text = "I've analyzed your post and can help with that."
DiscourseAi::Completions::Llm.with_prepared_responses([response_text]) do
automation.running_in_background!
automation.trigger!({ "post" => post })
end
topic = post.topic.reload
last_post = topic.posts.order(:post_number).last
expect(topic.posts.count).to eq(2)
# Verify that the response was posted by the persona's user
expect(last_post.user_id).to eq(bot_user.id)
expect(last_post.raw).to eq(response_text)
expect(last_post.post_type).to eq(Post.types[:regular]) # Not a whisper
end
it "can respond with a whisper when configured to do so" do
add_automation_field("whisper", true, type: "boolean")
post = Fabricate(:post, raw: "This is another test post for triage")
response_text = "Staff-only response to your post."
DiscourseAi::Completions::Llm.with_prepared_responses([response_text]) do
automation.running_in_background!
automation.trigger!({ "post" => post })
end
topic = post.topic.reload
last_post = topic.posts.order(:post_number).last
# Verify that the response is a whisper
expect(last_post.user_id).to eq(bot_user.id)
expect(last_post.raw).to eq(response_text)
expect(last_post.post_type).to eq(Post.types[:whisper]) # This should be a whisper
end
it "does not respond to posts made by bots" do
bot = Fabricate(:bot)
bot_post = Fabricate(:post, user: bot, raw: "This is a bot post")
# The automation should not trigger for bot posts
DiscourseAi::Completions::Llm.with_prepared_responses(["Response"]) do
automation.running_in_background!
automation.trigger!({ "post" => bot_post })
end
# Verify no new post was created
expect(bot_post.topic.reload.posts.count).to eq(1)
end
it "handles errors gracefully" do
post = Fabricate(:post, raw: "Error-triggering post")
# Set up to cause an error
ai_persona.update!(user_id: nil)
# Should not raise an error
expect {
automation.running_in_background!
automation.trigger!({ "post" => post })
}.not_to raise_error
# Verify no new post was created
expect(post.topic.reload.posts.count).to eq(1)
end
it "passes topic metadata in context when responding to topic" do
# Create a category and tags for the test
category = Fabricate(:category, name: "Test Category")
tag1 = Fabricate(:tag, name: "test-tag")
tag2 = Fabricate(:tag, name: "support")
# Create a topic with category and tags
topic =
Fabricate(
:topic,
title: "Important Question About Feature",
category: category,
tags: [tag1, tag2],
user: user,
)
# Create a post in that topic
_post =
Fabricate(
:post,
topic: topic,
user: user,
raw: "This is a test post in a categorized and tagged topic",
)
post2 =
Fabricate(:post, topic: topic, user: user, raw: "This is another post in the same topic")
# Capture the prompt sent to the LLM to verify it contains metadata
prompt = nil
DiscourseAi::Completions::Llm.with_prepared_responses(
["I've analyzed your question"],
) do |_, _, _prompts|
automation.running_in_background!
automation.trigger!({ "post" => post2 })
prompt = _prompts.first
end
context = prompt.messages[1][:content] # The second message should be the triage prompt
# Verify that topic metadata is included in the context
expect(context).to include("Important Question About Feature")
expect(context).to include("Test Category")
expect(context).to include("test-tag")
expect(context).to include("support")
end
it "passes private message metadata in context when responding to PM" do
# Create a private message topic
pm_topic = Fabricate(:private_message_topic, user: user, title: "Important PM")
# Create initial PM post
pm_post =
Fabricate(
:post,
topic: pm_topic,
user: user,
raw: "This is a private message that needs triage",
)
# Create a follow-up post
pm_post2 =
Fabricate(
:post,
topic: pm_topic,
user: user,
raw: "Adding more context to my private message",
)
# Capture the prompt sent to the LLM
prompt = nil
DiscourseAi::Completions::Llm.with_prepared_responses(
["I've received your private message"],
) do |_, _, _prompts|
automation.running_in_background!
automation.trigger!({ "post" => pm_post2 })
prompt = _prompts.first
end
context = prompt.messages[1][:content]
# Verify that PM metadata is included in the context
expect(context).to include("Important PM")
expect(context).to include(pm_post.raw)
expect(context).to include(pm_post2.raw)
end
end