mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-07-23 22:43:27 +00:00
Introduces import/export feature for tools and personas. Uploads are omitted for now, and will be added in a future PR * **Backend:** * Adds `import` and `export` actions to `Admin::AiPersonasController` and `Admin::AiToolsController`. * Introduces `DiscourseAi::PersonaExporter` and `DiscourseAi::PersonaImporter` services to manage JSON serialization and deserialization. * The export format for a persona embeds its associated custom tools. To ensure portability, `AiTool` references are serialized using their `tool_name` rather than their internal database `id`. * The import logic detects conflicts by name. A `force=true` parameter can be passed to overwrite existing records. * **Frontend:** * `AiPersonaListEditor` and `AiToolListEditor` components now include an "Import" button that handles file selection and POSTs the JSON data to the respective `import` endpoint. * `AiPersonaEditorForm` and `AiToolEditorForm` components feature an "Export" button that triggers a download of the serialized record. * Handles import conflicts (HTTP `409` for tools, `422` for personas) by showing a `dialog.confirm` prompt to allow the user to force an overwrite. * **Testing:** * Adds comprehensive request specs for the new controller actions (`#import`, `#export`). * Includes unit specs for the `PersonaExporter` and `PersonaImporter` services. * Persona import and export implemented
81 lines
2.3 KiB
Ruby
81 lines
2.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
class PersonaExporter
|
|
def initialize(persona:)
|
|
raise ArgumentError, "Invalid persona provided" if !persona.is_a?(AiPersona)
|
|
@persona = persona
|
|
end
|
|
|
|
def export
|
|
serialized_custom_tools = serialize_tools(@persona)
|
|
serialize_persona(@persona, serialized_custom_tools)
|
|
end
|
|
|
|
private
|
|
|
|
def serialize_tools(ai_persona)
|
|
custom_tool_ids =
|
|
(ai_persona.tools || []).filter_map do |tool_config|
|
|
# A tool config is an array like: ["custom-ID", {options}, force_flag]
|
|
if tool_config.is_a?(Array) && tool_config[0].to_s.start_with?("custom-")
|
|
tool_config[0].split("-", 2).last.to_i
|
|
end
|
|
end
|
|
|
|
return [] if custom_tool_ids.empty?
|
|
|
|
tools = AiTool.where(id: custom_tool_ids)
|
|
tools.map do |tool|
|
|
{
|
|
identifier: tool.tool_name, # Use tool_name for portability
|
|
name: tool.name,
|
|
description: tool.description,
|
|
tool_name: tool.tool_name,
|
|
parameters: tool.parameters,
|
|
summary: tool.summary,
|
|
script: tool.script,
|
|
}
|
|
end
|
|
end
|
|
|
|
def serialize_persona(ai_persona, serialized_custom_tools)
|
|
export_data = {
|
|
meta: {
|
|
version: "1.0",
|
|
exported_at: Time.zone.now.iso8601,
|
|
},
|
|
persona: {
|
|
name: ai_persona.name,
|
|
description: ai_persona.description,
|
|
system_prompt: ai_persona.system_prompt,
|
|
examples: ai_persona.examples,
|
|
temperature: ai_persona.temperature,
|
|
top_p: ai_persona.top_p,
|
|
response_format: ai_persona.response_format,
|
|
tools: transform_tools_for_export(ai_persona.tools, serialized_custom_tools),
|
|
},
|
|
custom_tools: serialized_custom_tools,
|
|
}
|
|
|
|
JSON.pretty_generate(export_data)
|
|
end
|
|
|
|
def transform_tools_for_export(tools_config, _serialized_custom_tools)
|
|
return [] if tools_config.blank?
|
|
|
|
tools_config.map do |tool_config|
|
|
unless tool_config.is_a?(Array) && tool_config[0].to_s.start_with?("custom-")
|
|
next tool_config
|
|
end
|
|
|
|
tool_id = tool_config[0].split("-", 2).last.to_i
|
|
tool = AiTool.find_by(id: tool_id)
|
|
next tool_config unless tool
|
|
|
|
["custom-#{tool.tool_name}", tool_config[1], tool_config[2]]
|
|
end
|
|
end
|
|
end
|
|
end
|