mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-27 02:05:26 +08:00
## Summary AI agents have 13 moderation tools (close_topic, delete_topic, edit_tags, edit_post, etc.) that currently execute immediately without human oversight. This adds an optional approval queue that routes these tool actions through Discourse's review queue for moderator approval before execution. - **New `require_approval` toggle** on AI agents — when enabled, moderation tool calls are intercepted and sent to the review queue instead of executing immediately - **Review queue integration** — moderators see the agent name, tool name, parameters, and a rendered snippet of the triggering post, then approve or reject - **Loop prevention** — approved tool execution is wrapped in `DiscourseAutomation.set_active_automation` to prevent automation re-trigger loops (e.g., `edit_tags` → `topic_tags_changed` → automation fires again) ### New files - `AiToolAction` model — stores tool name, parameters (JSONB), agent/bot user refs, and triggering post ID - `ReviewableAiToolAction` — Reviewable subclass with approve (executes tool) and reject (discards) actions - `ReviewableAiToolActionSerializer` — serializes target tool data and payload context - Review queue frontend component — displays tool action details and post snippet - Two migrations: `ai_tool_actions` table and `require_approval` column on `ai_agents` ### Modified files - `Tool` base class gains `requires_approval?` (default `false`), overridden to `true` on all 13 moderation tools - `Bot#invoke_tool` — intercepts tools when both tool and agent opt in to approval - Agent admin editor — new "Require approval" checkbox - Agent REST model — `require_approval` added to attribute whitelists for save payloads - Serializer, controller, plugin.rb — wired up for the new field and reviewable type
95 lines
2.8 KiB
Ruby
Vendored
95 lines
2.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module Agents
|
|
module Tools
|
|
class SetTopicTimer < Tool
|
|
TIMER_TYPES = %w[close open delete silent_close bump].freeze
|
|
|
|
def self.signature
|
|
{
|
|
name: name,
|
|
description:
|
|
"Sets or removes a timer on a topic. Timers can close, open, delete, silently close, or bump a topic after a specified duration.",
|
|
parameters: [
|
|
{
|
|
name: "topic_id",
|
|
description: "The ID of the topic",
|
|
type: "integer",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "timer_type",
|
|
description: "The type of timer: close, open, delete, silent_close, or bump",
|
|
type: "string",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "duration_hours",
|
|
description:
|
|
"Number of hours from now until the timer fires. Set to null to remove an existing timer.",
|
|
type: "integer",
|
|
},
|
|
{
|
|
name: "reason",
|
|
description: "Short explanation of why the timer is being set",
|
|
type: "string",
|
|
required: true,
|
|
},
|
|
],
|
|
}
|
|
end
|
|
|
|
def self.name
|
|
"set_topic_timer"
|
|
end
|
|
|
|
def self.requires_approval?
|
|
true
|
|
end
|
|
|
|
def invoke
|
|
topic = Topic.find_by(id: parameters[:topic_id])
|
|
if !topic
|
|
return error_response(I18n.t("discourse_ai.ai_bot.set_topic_timer.errors.not_found"))
|
|
end
|
|
|
|
if !guardian.can_moderate_topic?(topic)
|
|
return(error_response(I18n.t("discourse_ai.ai_bot.set_topic_timer.errors.not_allowed")))
|
|
end
|
|
|
|
if reason.blank?
|
|
return error_response(I18n.t("discourse_ai.ai_bot.set_topic_timer.errors.no_reason"))
|
|
end
|
|
|
|
timer_type = parameters[:timer_type].to_s
|
|
if !TIMER_TYPES.include?(timer_type)
|
|
return(
|
|
error_response(
|
|
I18n.t("discourse_ai.ai_bot.set_topic_timer.errors.invalid_timer_type"),
|
|
)
|
|
)
|
|
end
|
|
|
|
duration_hours = parameters[:duration_hours]
|
|
|
|
topic.set_or_create_timer(
|
|
TopicTimer.types[timer_type.to_sym],
|
|
duration_hours,
|
|
by_user: acting_user,
|
|
)
|
|
|
|
{ status: "success", message: I18n.t("discourse_ai.ai_bot.set_topic_timer.success") }
|
|
end
|
|
|
|
def description_args
|
|
{
|
|
topic_id: parameters[:topic_id],
|
|
timer_type: parameters[:timer_type],
|
|
duration_hours: parameters[:duration_hours],
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|