discourse-ai/rename_persona_to_agent.rb
2025-05-29 15:40:46 +10:00

389 lines
14 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require "fileutils"
require "date"
class PersonaToAgentRenamer
def initialize
@plugin_root = Dir.pwd
@manifest = []
end
def run
case ARGV[0]
when "--content-only"
validate_directory
replace_content_in_files
print_content_summary
when "--migrations-only"
validate_directory
create_migration
create_post_migration
print_migration_summary
else
puts colorize("=" * 55, :blue)
puts colorize("Renaming 'persona' to 'agent' throughout the codebase", :blue)
puts colorize("=" * 55, :blue)
puts colorize("Working in: #{@plugin_root}", :blue)
validate_directory
replace_content_in_files
rename_files_and_directories
create_migration
create_post_migration
print_summary
end
end
private
def colorize(text, color)
colors = {
red: "\033[0;31m",
green: "\033[0;32m",
yellow: "\033[0;33m",
blue: "\033[0;34m",
nc: "\033[0m",
}
"#{colors[color]}#{text}#{colors[:nc]}"
end
def validate_directory
unless File.exist?(File.join(@plugin_root, "plugin.rb"))
puts colorize("Error: Script must be run from the discourse-ai plugin root directory", :red)
exit 1
end
end
def git_tracked_files
`git ls-files`.split("\n").map { |f| File.join(@plugin_root, f) }
end
def all_directories_with_persona
# Find all directories that contain 'persona' in their name
output = `find . -type d -name "*persona*" 2>/dev/null | grep -v "\\.git"`.split("\n")
output.map { |dir| File.join(@plugin_root, dir.sub("./", "")) }
end
def replace_content_in_files
puts colorize("Replacing content in files...", :blue)
file_extensions = %w[rb js gjs yml erb scss hbs json]
git_tracked_files.each do |file|
# Skip files in db/ directory and tokenizers/
next if file.include?("/db/")
next if file.include?("/tokenizers/")
next if !file_extensions.include?(File.extname(file)[1..-1])
next if File.basename(file) == "rename_persona_to_agent.rb"
# For YAML files, only process client.en.yml and server.en.yml
if File.extname(file) == ".yml"
basename = File.basename(file)
next unless basename == "client.en.yml" || basename == "server.en.yml"
end
next unless File.exist?(file)
content = File.read(file)
next unless content.match?(/persona/i)
original_content = content.dup
# Replace different case variations
content.gsub!(/persona/, "agent")
content.gsub!(/Persona/, "Agent")
content.gsub!(/PERSONA/, "AGENT")
# Handle special cases
content.gsub!(/aiPersona/, "aiAgent")
content.gsub!(/AIPersona/, "AIAgent")
content.gsub!(/ai_persona/, "ai_agent")
content.gsub!(/Ai_persona/, "Ai_agent")
content.gsub!(/ai-persona/, "ai-agent")
if content != original_content
File.write(file, content)
relative_path = file.sub("#{@plugin_root}/", "")
puts colorize("Content updated: #{relative_path}", :green)
@manifest << "Content updated: #{relative_path}"
end
end
end
def rename_files_and_directories
puts colorize("Renaming files and directories using git mv...", :blue)
# Get all directories with 'persona' in their path (excluding db/ and tokenizers/)
# Sort by depth (deepest first) to avoid conflicts when renaming parent directories
dirs_to_rename =
all_directories_with_persona
.select { |path| !path.include?("/db/") && !path.include?("/tokenizers/") }
.sort_by { |path| -path.count("/") }
# Get all files with 'persona' in their names (excluding db/ and tokenizers/)
files_to_rename =
git_tracked_files.select do |path|
!path.include?("/db/") && !path.include?("/tokenizers/") &&
File.basename(path).match?(/persona/i)
end
# First, rename individual files that have 'persona' in their filename
puts colorize(" Renaming individual files with 'persona' in filename...", :blue)
files_to_rename.each do |old_path|
next unless File.exist?(old_path)
next if File.basename(old_path) == "rename_persona_to_agent.rb"
# Skip files that are inside directories we're going to rename
# (they'll be handled when we rename the directory)
next if dirs_to_rename.any? { |dir| old_path.start_with?(dir + "/") }
new_path = old_path.gsub(/persona/, "agent").gsub(/Persona/, "Agent")
if old_path != new_path
# Ensure parent directory exists
FileUtils.mkdir_p(File.dirname(new_path))
# Use git mv to preserve history
if system("git", "mv", old_path, new_path)
old_relative = old_path.sub("#{@plugin_root}/", "")
new_relative = new_path.sub("#{@plugin_root}/", "")
puts colorize(" File renamed: #{old_relative} -> #{new_relative}", :green)
@manifest << "File renamed: #{old_relative} -> #{new_relative}"
else
puts colorize(" Failed to rename: #{old_path}", :red)
end
end
end
# Then rename directories (deepest first to avoid path conflicts)
puts colorize(" Renaming directories with 'persona' in path...", :blue)
dirs_to_rename.each do |old_dir_path|
next unless File.exist?(old_dir_path) && File.directory?(old_dir_path)
new_dir_path = old_dir_path.gsub(/persona/, "agent").gsub(/Persona/, "Agent")
if old_dir_path != new_dir_path && !File.exist?(new_dir_path)
# Create parent directory if needed
FileUtils.mkdir_p(File.dirname(new_dir_path))
# Use git mv to preserve history for the entire directory tree
if system("git", "mv", old_dir_path, new_dir_path)
old_relative = old_dir_path.sub("#{@plugin_root}/", "")
new_relative = new_dir_path.sub("#{@plugin_root}/", "")
puts colorize(" Directory renamed: #{old_relative} -> #{new_relative}", :green)
@manifest << "Directory renamed: #{old_relative} -> #{new_relative}"
# Log all files that were moved as part of this directory rename
if File.directory?(new_dir_path)
Dir
.glob("#{new_dir_path}/**/*", File::FNM_DOTMATCH)
.each do |moved_file|
next if File.directory?(moved_file)
next if File.basename(moved_file).start_with?(".")
# Calculate what the old path would have been
relative_to_new_dir = moved_file.sub(new_dir_path + "/", "")
old_file_path = File.join(old_dir_path, relative_to_new_dir)
old_file_relative = old_file_path.sub("#{@plugin_root}/", "")
new_file_relative = moved_file.sub("#{@plugin_root}/", "")
puts colorize(
" File moved: #{old_file_relative} -> #{new_file_relative}",
:green,
)
@manifest << "File moved: #{old_file_relative} -> #{new_file_relative}"
end
end
else
puts colorize(" Failed to rename directory: #{old_dir_path}", :red)
end
end
end
end
def create_migration
puts colorize("Creating database migration to copy persona tables and settings...", :blue)
timestamp = (Time.now - 86_400).strftime("%Y%m%d%H%M%S") # Yesterday to avoid UTC issues
migration_file =
File.join(@plugin_root, "db", "migrate", "#{timestamp}_copy_persona_tables_to_agent.rb")
migration_content = generate_migration_content
FileUtils.mkdir_p(File.dirname(migration_file))
File.write(migration_file, migration_content)
relative_migration = migration_file.sub("#{@plugin_root}/", "")
puts colorize("Created migration file: #{relative_migration}", :green)
@manifest << "Created migration file: #{relative_migration}"
end
def generate_migration_content
<<~RUBY
# frozen_string_literal: true
class CopyPersonaTablesToAgent < ActiveRecord::Migration[7.0]
def up
# Copy the main table structure and data
if table_exists?(:ai_personas) && !table_exists?(:ai_agents)
execute <<~SQL
CREATE TABLE ai_agents AS
SELECT * FROM ai_personas
SQL
# Copy indexes from ai_personas to ai_agents
execute <<~SQL
CREATE UNIQUE INDEX index_ai_agents_on_id
ON ai_agents USING btree (id)
SQL
# Copy any other indexes that exist on ai_personas
indexes = execute(<<~SQL).to_a
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'ai_personas'
AND indexname != 'ai_personas_pkey'
SQL
indexes.each do |index|
new_index_def = index['indexdef'].gsub('ai_personas', 'ai_agents')
new_index_name = index['indexname'].gsub('ai_personas', 'ai_agents')
new_index_def = new_index_def.gsub(index['indexname'], new_index_name)
execute(new_index_def)
end
end
# Update polymorphic associations to point to new table
execute <<~SQL
UPDATE rag_document_fragments
SET target_type = 'AiAgent'
WHERE target_type = 'AiPersona'
SQL
execute <<~SQL
UPDATE upload_references
SET target_type = 'AiAgent'
WHERE target_type = 'AiPersona'
SQL
# Migrate persona-related site settings to agent equivalents
migrate_site_setting('ai_summarization_persona', 'ai_summarization_agent')
migrate_site_setting('ai_summary_gists_persona', 'ai_summary_gists_agent')
migrate_site_setting('ai_bot_discover_persona', 'ai_bot_discover_agent')
migrate_site_setting('ai_discord_search_persona', 'ai_discord_search_agent')
end
def down
drop_table :ai_agents if table_exists?(:ai_agents)
# Revert polymorphic associations
execute <<~SQL
UPDATE rag_document_fragments
SET target_type = 'AiPersona'
WHERE target_type = 'AiAgent'
SQL
execute <<~SQL
UPDATE upload_references
SET target_type = 'AiPersona'
WHERE target_type = 'AiAgent'
SQL
# Remove the new agent settings (keep the old persona ones)
['ai_summarization_agent', 'ai_summary_gists_agent', 'ai_bot_discover_agent', 'ai_discord_search_agent'].each do |setting|
execute "DELETE FROM site_settings WHERE name = '\#{setting}'"
end
end
private
def migrate_site_setting(old_name, new_name)
execute <<~SQL
INSERT INTO site_settings (name, value, data_type, created_at, updated_at)
SELECT '\#{new_name}', value, data_type, NOW(), NOW()
FROM site_settings
WHERE name = '\#{old_name}'
AND NOT EXISTS (SELECT 1 FROM site_settings WHERE name = '\#{new_name}')
SQL
end
end
RUBY
end
def create_post_migration
puts colorize("Creating post-migration to drop old persona tables and settings...", :blue)
timestamp = (Time.now - 86_399).strftime("%Y%m%d%H%M%S") # Yesterday + 1 second to ensure this runs after the main migration
post_migrate_dir = File.join(@plugin_root, "db", "post_migrate")
migration_file = File.join(post_migrate_dir, "#{timestamp}_drop_persona_tables.rb")
migration_content = generate_post_migration_content
FileUtils.mkdir_p(post_migrate_dir)
File.write(migration_file, migration_content)
relative_migration = migration_file.sub("#{@plugin_root}/", "")
puts colorize("Created post-migration file: #{relative_migration}", :green)
@manifest << "Created post-migration file: #{relative_migration}"
end
def generate_post_migration_content
<<~RUBY
# frozen_string_literal: true
class DropPersonaTables < ActiveRecord::Migration[7.0]
def up
# Drop the old table after copying to new one
drop_table :ai_personas if table_exists?(:ai_personas)
# Remove old persona settings after copying to agent settings
old_persona_settings = [
'ai_summarization_persona',
'ai_summary_gists_persona',
'ai_bot_discover_persona',
'ai_discord_search_persona'
]
old_persona_settings.each do |setting|
execute "DELETE FROM site_settings WHERE name = '\#{setting}'"
end
end
def down
raise ActiveRecord::IrreversibleMigration, "Cannot recreate dropped persona tables and settings"
end
end
RUBY
end
def print_content_summary
puts colorize("Content replacement completed!", :green)
puts colorize("Files updated: #{@manifest.count}", :yellow)
@manifest.each { |change| puts " #{change}" }
end
def print_migration_summary
puts colorize("Database migrations created!", :green)
puts colorize("Files created: #{@manifest.count}", :yellow)
@manifest.each { |change| puts " #{change}" }
end
def print_summary
puts colorize("=" * 55, :blue)
puts colorize("Completed renaming 'persona' to 'agent' in the codebase", :green)
puts colorize("=" * 55, :blue)
puts colorize("Changes made:", :yellow)
@manifest.each { |change| puts " #{change}" }
puts colorize("Next steps:", :yellow)
puts "1. Review changes with 'git diff'"
puts "2. Run tests and fix any remaining issues"
puts "3. Run the database migrations (migrate, then post_migrate)"
puts colorize("=" * 55, :blue)
end
end
# Run the renamer if this file is executed directly
PersonaToAgentRenamer.new.run if __FILE__ == $0