mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-01 05:51:37 +08:00
Redesigns the Data Explorer `/queries/new` page to be AI-first, with a non-AI fallback if AI is disabled. Instead of the old dual-form layout (manual create + separate AI generate that redirects to the edit page), the new page lets you describe what you want, generates the SQL inline, and saves when you're ready. Reviewer notes: - there's no database record created until user clicks "Save query" - new POST /queries/generate endpoint for query generation, no DB record - agent rework - related PR https://github.com/discourse/discourse-ai-evals/pull/16 - switched to agentic execution mode - removed ValidateSql tool (redundant with RunSql) - added full Data Explorer parameter type reference - evaluator for the DE agent
218 lines
7.7 KiB
Ruby
218 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# name: discourse-data-explorer
|
|
# about: Allows you to make SQL queries against your live database, allowing for up-to-the-minute stats reporting.
|
|
# meta_topic_id: 32566
|
|
# version: 0.3
|
|
# authors: Riking
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-data-explorer
|
|
|
|
enabled_site_setting :data_explorer_enabled
|
|
|
|
register_site_setting_area("ai-features/data_explorer") if defined?(DiscourseAi)
|
|
|
|
register_asset "stylesheets/explorer.scss"
|
|
|
|
register_svg_icon "angle-down"
|
|
register_svg_icon "angle-right"
|
|
register_svg_icon "chart-line"
|
|
register_svg_icon "angle-left"
|
|
register_svg_icon "circle-exclamation"
|
|
register_svg_icon "info"
|
|
register_svg_icon "pencil"
|
|
register_svg_icon "upload"
|
|
|
|
add_admin_route "explorer.title", "discourse-data-explorer", use_new_show_route: true
|
|
|
|
module ::DiscourseDataExplorer
|
|
PLUGIN_NAME = "discourse-data-explorer"
|
|
|
|
# This should always match the max value for the
|
|
# data_explorer_query_result_limit site setting
|
|
QUERY_RESULT_MAX_LIMIT = 10_000
|
|
end
|
|
|
|
require_relative "lib/discourse_data_explorer/engine"
|
|
|
|
after_initialize do
|
|
GlobalSetting.add_default(:max_data_explorer_api_reqs_per_10_seconds, 2)
|
|
|
|
# Available options:
|
|
# - warn
|
|
# - warn+block
|
|
# - block
|
|
GlobalSetting.add_default(:max_data_explorer_api_req_mode, "warn")
|
|
|
|
add_to_class(:guardian, :user_is_a_member_of_group?) do |group|
|
|
return false if !current_user
|
|
return true if current_user.admin?
|
|
current_user.group_ids.include?(group.id)
|
|
end
|
|
|
|
add_to_class(:guardian, :user_can_access_query?) do |query|
|
|
return false if !current_user
|
|
return true if current_user.admin?
|
|
query.groups.any? { |group| user_is_a_member_of_group?(group) }
|
|
end
|
|
|
|
add_to_class(:guardian, :group_and_user_can_access_query?) do |group, query|
|
|
return false if !current_user
|
|
return true if current_user.admin?
|
|
user_is_a_member_of_group?(group) && query.groups.exists?(id: group.id)
|
|
end
|
|
|
|
add_to_serializer(
|
|
:group_show,
|
|
:has_visible_data_explorer_queries,
|
|
include_condition: -> { scope.user_is_a_member_of_group?(object) },
|
|
) { DiscourseDataExplorer::Query.for_group(object).exists? }
|
|
|
|
register_bookmarkable(DiscourseDataExplorer::QueryGroupBookmarkable)
|
|
|
|
add_api_key_scope(
|
|
:data_explorer,
|
|
{
|
|
run_queries: {
|
|
actions: %w[discourse_data_explorer/query#run discourse_data_explorer/query#public_run],
|
|
params: %i[id],
|
|
},
|
|
},
|
|
)
|
|
|
|
reloadable_patch do
|
|
if defined?(DiscourseAutomation)
|
|
add_automation_scriptable("recurring_data_explorer_result_pm") do
|
|
queries =
|
|
DiscourseDataExplorer::Query
|
|
.where(hidden: false)
|
|
.map { |q| { id: q.id, translated_name: q.name } }
|
|
field :recipients, component: :email_group_user, required: true
|
|
field :query_id, component: :choices, required: true, extra: { content: queries }
|
|
field :query_params, component: :"key-value", accepts_placeholders: true
|
|
field :skip_empty, component: :boolean
|
|
field :users_from_group, component: :boolean
|
|
field :attach_csv,
|
|
component: :boolean,
|
|
validator: ->(attach_csv) do
|
|
return if !attach_csv
|
|
|
|
extensions = SiteSetting.authorized_extensions.split("|")
|
|
if (extensions & %w[csv *]).empty?
|
|
I18n.t(
|
|
"discourse_automation.scriptables.recurring_data_explorer_result_pm.no_csv_allowed",
|
|
)
|
|
end
|
|
end
|
|
|
|
version 1
|
|
triggerables [:recurring]
|
|
|
|
script do |_, fields, automation|
|
|
recipients = Array(fields.dig("recipients", "value")).uniq
|
|
query_id = fields.dig("query_id", "value")
|
|
query_params = fields.dig("query_params", "value") || {}
|
|
skip_empty = fields.dig("skip_empty", "value") || false
|
|
users_from_group = fields.dig("users_from_group", "value") || false
|
|
attach_csv = fields.dig("attach_csv", "value") || false
|
|
|
|
unless SiteSetting.data_explorer_enabled
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - plugin must be enabled to run automation #{automation.id}"
|
|
next
|
|
end
|
|
|
|
if recipients.blank?
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't find any recipients for automation #{automation.id}"
|
|
next
|
|
end
|
|
|
|
DiscourseDataExplorer::ReportGenerator
|
|
.generate(
|
|
query_id,
|
|
query_params,
|
|
recipients,
|
|
{ skip_empty:, users_from_group:, attach_csv:, render_url_columns: true },
|
|
)
|
|
.each do |pm|
|
|
begin
|
|
utils.send_pm(pm, automation_id: automation.id)
|
|
rescue ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't send PM for automation #{automation.id}: #{e.message}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
add_automation_scriptable("recurring_data_explorer_result_topic") do
|
|
queries =
|
|
DiscourseDataExplorer::Query
|
|
.where(hidden: false)
|
|
.map { |q| { id: q.id, translated_name: q.name } }
|
|
field :topic_id, component: :text, required: true
|
|
field :query_id, component: :choices, required: true, extra: { content: queries }
|
|
field :query_params, component: :"key-value", accepts_placeholders: true
|
|
field :skip_empty, component: :boolean
|
|
field :attach_csv, component: :boolean
|
|
|
|
version 1
|
|
triggerables [:recurring]
|
|
|
|
script do |_, fields, automation|
|
|
topic_id = fields.dig("topic_id", "value")
|
|
query_id = fields.dig("query_id", "value")
|
|
query_params = fields.dig("query_params", "value") || {}
|
|
skip_empty = fields.dig("skip_empty", "value") || false
|
|
attach_csv = fields.dig("attach_csv", "value") || false
|
|
|
|
unless SiteSetting.data_explorer_enabled
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - plugin must be enabled to run automation #{automation.id}"
|
|
next
|
|
end
|
|
|
|
topic = Topic.find_by(id: topic_id)
|
|
if topic.blank?
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't find topic ID (#{topic_id}) for automation #{automation.id}"
|
|
next
|
|
end
|
|
|
|
begin
|
|
post =
|
|
DiscourseDataExplorer::ReportGenerator.generate_post(
|
|
query_id,
|
|
query_params,
|
|
{ skip_empty:, attach_csv:, render_url_columns: true },
|
|
)
|
|
|
|
next if post.empty?
|
|
|
|
PostCreator.create!(
|
|
Discourse.system_user,
|
|
topic_id: topic.id,
|
|
raw: post["raw"],
|
|
skip_validations: true,
|
|
)
|
|
rescue ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't reply to topic ID #{topic_id} for automation #{automation.id}: #{e.message}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if defined?(DiscourseAi)
|
|
require_relative "lib/discourse_data_explorer/tools/run_sql"
|
|
require_relative "lib/discourse_data_explorer/ai_query_generator"
|
|
|
|
DiscoursePluginRegistry.register_external_ai_feature(
|
|
{
|
|
module_name: :data_explorer,
|
|
feature: :query_generation,
|
|
agent_klass: DiscourseDataExplorer::AiQueryGenerator,
|
|
enabled_by_setting: "data_explorer_ai_queries_enabled",
|
|
},
|
|
self,
|
|
)
|
|
|
|
SiteSetting.areas[:data_explorer_ai_queries_enabled] ||= "ai-features/data_explorer"
|
|
SiteSetting.areas[:data_explorer_query_generation_agent] ||= "ai-features/data_explorer"
|
|
end
|
|
end
|