diff --git a/.gitignore b/.gitignore index 5e66125..3584ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ htmlcov/ .nox/ coverage/ .hypothesis/ +backend/tests # SQLite database files *.sqlite diff --git a/backend/app/core/user_initializer/initializers/__init__.py b/backend/app/core/user_initializer/initializers/__init__.py index 2e3bbdb..f7c10fb 100644 --- a/backend/app/core/user_initializer/initializers/__init__.py +++ b/backend/app/core/user_initializer/initializers/__init__.py @@ -9,8 +9,9 @@ from . import settings_initializer from . import components_initializer from . import navigation_initializer -from . import pages_initializer -from . import brain_drive_basic_ai_chat_initializer -from . import brain_drive_settings_initializer +from . import github_plugin_initializer # GitHub plugin installer (runs first for plugins) +from . import pages_initializer # Pages initializer (updated for BrainDriveChat) +# from . import brain_drive_basic_ai_chat_initializer # Replaced by GitHub installer +# from . import brain_drive_settings_initializer # Replaced by GitHub installer # Add more imports as needed when new initializers are created \ No newline at end of file diff --git a/backend/app/core/user_initializer/initializers/brain_drive_basic_ai_chat_initializer.py b/backend/app/core/user_initializer/initializers/brain_drive_basic_ai_chat_initializer.py deleted file mode 100644 index 920f8fd..0000000 --- a/backend/app/core/user_initializer/initializers/brain_drive_basic_ai_chat_initializer.py +++ /dev/null @@ -1,354 +0,0 @@ -""" -BrainDrive Basic AI Chat initializer. - -This initializer creates the BrainDrive Basic AI Chat plugin and its modules for a new user. -""" - -import logging -import json -import datetime -import os -import shutil -from pathlib import Path -from typing import Dict, Any, List -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import text - -from app.core.user_initializer.base import UserInitializerBase -from app.core.user_initializer.registry import register_initializer -from app.core.user_initializer.utils import prepare_record_for_new_user - -logger = logging.getLogger(__name__) - -# Define the plugins directory path -PLUGINS_DIR = Path(__file__).parent.parent.parent.parent.parent / "plugins" - -class BrainDriveBasicAIChatInitializer(UserInitializerBase): - """Initializer for BrainDrive Basic AI Chat plugin and its modules.""" - - name = "brain_drive_basic_ai_chat_initializer" - description = "Initializes BrainDrive Basic AI Chat plugin and its modules for a new user" - priority = 500 # Medium priority - dependencies = [] # No dependencies - - # Hardcoded plugin data from backend/plugins/BrainDriveBasicAIChat/plugin.json - PLUGIN_DATA = { - "id": "BrainDriveBasicAIChat", - "name": "BrainDrive Basic AI Chat", - "description": "Basic AI Chat Modules", - "version": "1.0.0", - "type": "frontend", - "enabled": True, - "icon": "Dashboard", - "category": "Utilities", - "status": "activated", - "official": True, - "author": "BrainDrive Team", - "last_updated": "2025-03-06", - "compatibility": "1.0.0", - "downloads": 0, - "scope": "BrainDriveBasicAIChat", - "bundle_method": "webpack", - "bundle_location": "frontend/dist/remoteEntry.js", - "is_local": False, - "long_description": None, - "config_fields": None, - "messages": None, - "dependencies": None, - "plugin_slug": "BrainDriveBasicAIChat" - } - - # Hardcoded module data from backend/plugins/BrainDriveBasicAIChat/plugin.json - MODULE_DATA = [ - { - "id": "ai-prompt-chat-v2", - "plugin_id": "BrainDriveBasicAIChat", - "name": "AIPromptChat", - "display_name": "AI Prompt Chat V2", - "description": "Interactive chat interface for AI prompts and responses", - "icon": "Chat", - "category": "AI Tools", - "enabled": True, - "priority": 1, - "props": "{\"initialGreeting\": \"Hello! How can I assist you today?\", \"promptQuestion\": \"Type your message here...\"}", - "config_fields": "{\"initialGreeting\": {\"type\": \"string\", \"label\": \"Initial Greeting\", \"default\": \"Hello! How can I assist you today?\"}, \"promptQuestion\": {\"type\": \"string\", \"label\": \"Prompt Placeholder\", \"default\": \"Type your message here...\"}}", - "messages": "{\"sends\": [{\"type\": \"ai.prompt\", \"description\": \"AI prompt sent by user\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\", \"description\": \"User prompt text\", \"required\": true}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the prompt was sent\", \"required\": true}}}}], \"receives\": [{\"type\": \"model.selection\", \"description\": \"Selected model information\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"model\": {\"type\": \"object\", \"description\": \"Selected model information\", \"required\": true}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the model was selected\", \"required\": true}}}}]}", - "required_services": "{\"api\": {\"methods\": [\"get\", \"post\", \"postStreaming\"], \"version\": \"1.0.0\"}, \"event\": {\"methods\": [\"sendMessage\", \"subscribeToMessages\", \"unsubscribeFromMessages\"], \"version\": \"1.0.0\"}, \"theme\": {\"methods\": [\"getCurrentTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 4, \"minHeight\": 4, \"defaultWidth\": 6, \"defaultHeight\": 6}", - "tags": "[\"AI\", \"Chat\", \"Prompt\", \"LLM\"]" - }, - { - "id": "model-selection-v2", - "plugin_id": "BrainDriveBasicAIChat", - "name": "ComponentModelSelection", - "display_name": "Model Selection v2", - "description": "Select an AI model from multiple providers with enhanced UI", - "icon": "ModelTraining", - "category": "LLM Servers", - "enabled": True, - "priority": 1, - "props": "{\"moduleId\": \"model-selection-v2\", \"label\": \"Select Model\", \"labelPosition\": \"top\", \"providerSettings\": [\"ollama_servers_settings\"], \"targetComponent\": \"\"}", - "config_fields": "{\"moduleId\": {\"type\": \"string\", \"label\": \"Module ID\", \"default\": \"model-selection-v2\"}, \"label\": {\"type\": \"string\", \"label\": \"Label Text\", \"default\": \"Select Model\"}, \"labelPosition\": {\"type\": \"select\", \"label\": \"Label Position\", \"default\": \"top\", \"options\": [{\"value\": \"top\", \"label\": \"Top\"}, {\"value\": \"left\", \"label\": \"Left\"}, {\"value\": \"right\", \"label\": \"Right\"}, {\"value\": \"bottom\", \"label\": \"Bottom\"}]}, \"providerSettings\": {\"type\": \"array\", \"label\": \"Provider Settings\", \"default\": [\"ollama_servers_settings\"], \"description\": \"List of settings to use for model retrieval (e.g., ollama_servers_settings, openai_servers_settings)\"}, \"targetComponent\": {\"type\": \"string\", \"label\": \"Target Component\", \"default\": \"\", \"description\": \"Component to send model selection events to (leave empty to broadcast to all)\"}}", - "messages": "{\"sends\": [{\"type\": \"model.selection\", \"description\": \"Selected model information\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"model\": {\"type\": \"object\", \"description\": \"Selected model information\", \"required\": true, \"properties\": {\"name\": {\"type\": \"string\", \"description\": \"Model name\", \"required\": true}, \"provider\": {\"type\": \"string\", \"description\": \"Provider type (e.g., ollama, openai)\", \"required\": true}, \"providerId\": {\"type\": \"string\", \"description\": \"Provider settings ID\", \"required\": true}, \"serverName\": {\"type\": \"string\", \"description\": \"Server name\", \"required\": true}, \"serverId\": {\"type\": \"string\", \"description\": \"Server ID\", \"required\": true}}}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the model was selected\", \"required\": true}}}}], \"receives\": []}", - "required_services": "{\"api\": {\"methods\": [\"get\", \"post\"], \"version\": \"1.0.0\"}, \"event\": {\"methods\": [\"sendMessage\", \"subscribeToMessages\", \"unsubscribeFromMessages\"], \"version\": \"1.0.0\"}, \"settings\": {\"methods\": [\"getSetting\", \"setSetting\", \"getSettingDefinitions\"], \"version\": \"1.0.0\"}, \"theme\": {\"methods\": [\"getCurrentTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 3, \"minHeight\": 1, \"defaultWidth\": 6, \"defaultHeight\": 1}", - "tags": "[\"LLM\", \"Model\", \"Selection\", \"AI\", \"Providers\", \"OpenAI\", \"Ollama\"]" - }, - { - "id": "ai-chat-history", - "plugin_id": "BrainDriveBasicAIChat", - "name": "AIChatHistory", - "display_name": "AI Chat History", - "description": "Displays and manages conversation history with options to select, rename, and delete conversations", - "icon": "History", - "category": "AI Tools", - "enabled": True, - "priority": 1, - "props": "{}", - "config_fields": "{}", - "messages": "{\"sends\": [{\"type\": \"conversation.selection\", \"description\": \"Selected conversation information\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"conversation_id\": {\"type\": \"string\", \"description\": \"Conversation ID\", \"required\": true}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the conversation was selected\", \"required\": true}}}}, {\"type\": \"model.selection\", \"description\": \"Selected model information from conversation\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"model\": {\"type\": \"object\", \"description\": \"Selected model information\", \"required\": true, \"properties\": {\"name\": {\"type\": \"string\", \"description\": \"Model name\", \"required\": true}, \"provider\": {\"type\": \"string\", \"description\": \"Provider type (e.g., ollama, openai)\", \"required\": true}, \"providerId\": {\"type\": \"string\", \"description\": \"Provider settings ID\", \"required\": true}, \"serverName\": {\"type\": \"string\", \"description\": \"Server name\", \"required\": true}, \"serverId\": {\"type\": \"string\", \"description\": \"Server ID\", \"required\": true}}}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the model was selected\", \"required\": true}}}}, {\"type\": \"conversation.new\", \"description\": \"New chat event\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the new chat was requested\", \"required\": true}}}}], \"receives\": [{\"type\": \"model.selection\", \"description\": \"Selected model information\", \"contentSchema\": {\"type\": \"object\", \"properties\": {\"model\": {\"type\": \"object\", \"description\": \"Selected model information\", \"required\": true}, \"timestamp\": {\"type\": \"string\", \"description\": \"ISO timestamp of when the model was selected\", \"required\": true}}}}]}", - "required_services": "{\"api\": {\"methods\": [\"get\", \"post\", \"put\", \"delete\"], \"version\": \"1.0.0\"}, \"event\": {\"methods\": [\"sendMessage\", \"subscribeToMessages\", \"unsubscribeFromMessages\"], \"version\": \"1.0.0\"}, \"theme\": {\"methods\": [\"getCurrentTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 3, \"minHeight\": 1, \"defaultWidth\": 6, \"defaultHeight\": 1}", - "tags": "[\"AI\", \"Chat\", \"History\", \"Conversation\", \"Management\"]" - } - ] - - async def initialize(self, user_id: str, db: AsyncSession, **kwargs) -> bool: - """Initialize BrainDrive Basic AI Chat plugin and its modules for a new user.""" - try: - logger.info(f"Initializing BrainDrive Basic AI Chat plugin for user {user_id}") - - # Create the plugin for the user - plugin_data = self.PLUGIN_DATA.copy() - - # Prepare the plugin data for the new user - prepared_plugin = prepare_record_for_new_user( - plugin_data, - user_id, - preserve_fields=["name", "description", "version", "type", "icon", - "category", "status", "official", "author", - "last_updated", "compatibility", "scope", - "bundle_method", "bundle_location", "is_local", - "long_description", "plugin_slug"], - user_id_field="user_id" - ) - - # Update the ID to use the new format: user_id_plugin_slug - plugin_slug = prepared_plugin.get("plugin_slug", "BrainDriveBasicAIChat") - prepared_plugin["id"] = f"{user_id}_{plugin_slug}" - - # Create the plugin using direct SQL - current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Create SQL statement for plugin - plugin_stmt = text(""" - INSERT INTO plugin - (id, name, description, version, type, enabled, icon, category, status, - official, author, last_updated, compatibility, downloads, scope, - bundle_method, bundle_location, is_local, long_description, - config_fields, messages, dependencies, created_at, updated_at, user_id, plugin_slug) - VALUES - (:id, :name, :description, :version, :type, :enabled, :icon, :category, - :status, :official, :author, :last_updated, :compatibility, :downloads, - :scope, :bundle_method, :bundle_location, :is_local, :long_description, - :config_fields, :messages, :dependencies, :created_at, :updated_at, :user_id, :plugin_slug) - """) - - # Execute statement with parameters - await db.execute(plugin_stmt, { - "id": prepared_plugin["id"], - "name": prepared_plugin["name"], - "description": prepared_plugin["description"], - "version": prepared_plugin["version"], - "type": prepared_plugin["type"], - "enabled": prepared_plugin.get("enabled", True), - "icon": prepared_plugin.get("icon"), - "category": prepared_plugin.get("category"), - "status": prepared_plugin.get("status", "activated"), - "official": prepared_plugin.get("official", True), - "author": prepared_plugin.get("author", "BrainDrive Team"), - "last_updated": prepared_plugin.get("last_updated"), - "compatibility": prepared_plugin.get("compatibility", "1.0.0"), - "downloads": prepared_plugin.get("downloads", 0), - "scope": prepared_plugin.get("scope"), - "bundle_method": prepared_plugin.get("bundle_method"), - "bundle_location": prepared_plugin.get("bundle_location"), - "is_local": prepared_plugin.get("is_local", False), - "long_description": prepared_plugin.get("long_description"), - "config_fields": prepared_plugin.get("config_fields"), - "messages": prepared_plugin.get("messages"), - "dependencies": prepared_plugin.get("dependencies"), - "created_at": current_time, - "updated_at": current_time, - "user_id": user_id, - "plugin_slug": prepared_plugin.get("plugin_slug") - }) - - logger.info(f"Created plugin {prepared_plugin['name']} for user {user_id}") - - # Now initialize all modules for this plugin - for module_data in self.MODULE_DATA: - # Prepare the module data for the new user - prepared_module = prepare_record_for_new_user( - module_data, - user_id, - preserve_fields=["name", "display_name", "description", "icon", - "category", "enabled", "priority", "props", - "config_fields", "messages", "required_services", - "dependencies", "layout", "tags"], - user_id_field="user_id" - ) - - # Create the module using direct SQL - module_stmt = text(""" - INSERT INTO module - (id, plugin_id, name, display_name, description, icon, category, - enabled, priority, props, config_fields, messages, required_services, - dependencies, layout, tags, created_at, updated_at, user_id) - VALUES - (:id, :plugin_id, :name, :display_name, :description, :icon, :category, - :enabled, :priority, :props, :config_fields, :messages, :required_services, - :dependencies, :layout, :tags, :created_at, :updated_at, :user_id) - """) - - # Execute statement with parameters - await db.execute(module_stmt, { - "id": prepared_module["id"], - "plugin_id": prepared_plugin["id"], # Use the new plugin ID - "name": prepared_module["name"], - "display_name": prepared_module.get("display_name"), - "description": prepared_module.get("description"), - "icon": prepared_module.get("icon"), - "category": prepared_module.get("category"), - "enabled": prepared_module.get("enabled", True), - "priority": prepared_module.get("priority", 0), - "props": prepared_module.get("props"), - "config_fields": prepared_module.get("config_fields"), - "messages": prepared_module.get("messages"), - "required_services": prepared_module.get("required_services"), - "dependencies": prepared_module.get("dependencies"), - "layout": prepared_module.get("layout"), - "tags": prepared_module.get("tags"), - "created_at": current_time, - "updated_at": current_time, - "user_id": user_id - }) - - logger.info(f"Created module {prepared_module['name']} for user {user_id}") - - # Create directory structure and copy plugin files - try: - # Define paths - original_plugin_dir = PLUGINS_DIR / "BrainDriveBasicAIChat" - user_dir = PLUGINS_DIR / user_id - plugin_dir = user_dir / prepared_plugin["id"] - - # Create user directory if it doesn't exist - if not user_dir.exists(): - user_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created user directory: {user_dir}") - - # Create plugin directory if it doesn't exist - if not plugin_dir.exists(): - plugin_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created plugin directory: {plugin_dir}") - - # Create frontend/dist directory structure - frontend_dist_dir = plugin_dir / "frontend" / "dist" - frontend_dist_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created frontend/dist directory: {frontend_dist_dir}") - - # Copy original plugin files to user plugin directory - if original_plugin_dir.exists(): - # Copy files from original plugin directory to user plugin directory - for item in original_plugin_dir.glob("**/*"): - if item.is_file(): - # Get relative path from original plugin directory - rel_path = item.relative_to(original_plugin_dir) - # Create destination path - dest_path = plugin_dir / rel_path - # Create parent directories if they don't exist - dest_path.parent.mkdir(parents=True, exist_ok=True) - # Copy the file - shutil.copy2(item, dest_path) - logger.info(f"Copied {item} to {dest_path}") - - # Create a dummy remoteEntry.js file in the frontend/dist directory if it doesn't exist - remote_entry_path = frontend_dist_dir / "remoteEntry.js" - if not remote_entry_path.exists(): - with open(remote_entry_path, 'w') as f: - f.write('// Placeholder for remote entry file\n') - f.write('window.BrainDriveBasicAIChat = { get: function() { return Promise.resolve(() => ({ default: {} })); } };\n') - logger.info(f"Created placeholder remoteEntry.js file at {remote_entry_path}") - - logger.info(f"Copied original plugin files to user plugin directory: {plugin_dir}") - else: - logger.warning(f"Original plugin directory not found: {original_plugin_dir}") - except Exception as e: - logger.error(f"Error creating directory structure or copying files: {e}") - # Continue with the initialization even if file copying fails - - await db.commit() - logger.info(f"BrainDrive Basic AI Chat plugin initialized successfully for user {user_id}") - return True - - except Exception as e: - logger.error(f"Error initializing BrainDrive Basic AI Chat plugin for user {user_id}: {e}") - await db.rollback() - return False - - async def cleanup(self, user_id: str, db: AsyncSession, **kwargs) -> bool: - """Clean up the plugin and its modules if initialization fails.""" - try: - logger.info(f"Cleaning up BrainDrive Basic AI Chat plugin for user {user_id}") - - # Delete all modules for this plugin for this user - module_stmt = text(""" - DELETE FROM module - WHERE user_id = :user_id AND plugin_id IN ( - SELECT id FROM plugin WHERE user_id = :user_id AND name = 'BrainDrive Basic AI Chat' - ) - """) - - await db.execute(module_stmt, {"user_id": user_id}) - - # Delete plugin - plugin_stmt = text(""" - DELETE FROM plugin - WHERE user_id = :user_id AND name = 'BrainDrive Basic AI Chat' - """) - - await db.execute(plugin_stmt, {"user_id": user_id}) - - # Clean up directory structure - try: - plugin_id = f"{user_id}_BrainDriveBasicAIChat" - plugin_dir = PLUGINS_DIR / user_id / plugin_id - - if plugin_dir.exists(): - shutil.rmtree(plugin_dir) - logger.info(f"Removed plugin directory: {plugin_dir}") - - # Check if user directory is empty and remove it if it is - user_dir = PLUGINS_DIR / user_id - if user_dir.exists() and not any(user_dir.iterdir()): - user_dir.rmdir() - logger.info(f"Removed empty user directory: {user_dir}") - except Exception as e: - logger.error(f"Error cleaning up directory structure: {e}") - # Continue with the cleanup even if directory removal fails - - await db.commit() - logger.info(f"BrainDrive Basic AI Chat plugin cleaned up successfully for user {user_id}") - return True - - except Exception as e: - logger.error(f"Error cleaning up BrainDrive Basic AI Chat plugin for user {user_id}: {e}") - await db.rollback() - return False - -# Register the initializer -register_initializer(BrainDriveBasicAIChatInitializer) \ No newline at end of file diff --git a/backend/app/core/user_initializer/initializers/brain_drive_settings_initializer.py b/backend/app/core/user_initializer/initializers/brain_drive_settings_initializer.py deleted file mode 100644 index 0be1905..0000000 --- a/backend/app/core/user_initializer/initializers/brain_drive_settings_initializer.py +++ /dev/null @@ -1,357 +0,0 @@ -""" -BrainDrive Settings initializer. - -This initializer creates the BrainDrive Settings plugin and its modules for a new user. -""" - -import logging -import json -import datetime -import os -import shutil -from pathlib import Path -from typing import Dict, Any, List -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import text - -from app.core.user_initializer.base import UserInitializerBase -from app.core.user_initializer.registry import register_initializer -from app.core.user_initializer.utils import prepare_record_for_new_user - -logger = logging.getLogger(__name__) - -# Define the plugins directory path -PLUGINS_DIR = Path(__file__).parent.parent.parent.parent.parent / "plugins" - -class BrainDriveSettingsInitializer(UserInitializerBase): - """Initializer for BrainDrive Settings plugin and its modules.""" - - name = "brain_drive_settings_initializer" - description = "Initializes BrainDrive Settings plugin and its modules for a new user" - priority = 500 # Medium priority - dependencies = [] # No dependencies - - # Hardcoded plugin data from backend/plugins/BrainDriveSettings/plugin.json - PLUGIN_DATA = { - "id": "BrainDriveSettings", - "name": "BrainDrive Settings", - "description": "Basic BrainDrive Settings Plugin", - "version": "1.0.0", - "type": "frontend", - "enabled": True, - "icon": "Dashboard", - "category": "Utilities", - "status": "activated", - "official": True, - "author": "BrainDrive Team", - "last_updated": "2025-03-06", - "compatibility": "1.0.0", - "downloads": 0, - "scope": "BrainDriveSettings", - "bundle_method": "webpack", - "bundle_location": "frontend/dist/remoteEntry.js", - "is_local": False, - "long_description": None, - "config_fields": None, - "messages": None, - "dependencies": None, - "plugin_slug": "BrainDriveSettings" - } - - # Hardcoded module data from backend/plugins/BrainDriveSettings/plugin.json - MODULE_DATA = [ - { - "id": "componentTheme", - "plugin_id": "BrainDriveSettings", - "name": "ComponentTheme", - "display_name": "Theme Settings", - "description": "Change application theme", - "icon": "DarkMode", - "category": "Settings", - "enabled": True, - "priority": 1, - "props": "{}", - "config_fields": "{}", - "messages": "{\"sends\": [], \"receives\": []}", - "required_services": "{\"theme\": {\"methods\": [\"getCurrentTheme\", \"setTheme\", \"toggleTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}, \"settings\": {\"methods\": [\"getSetting\", \"setSetting\", \"registerSettingDefinition\", \"getSettingDefinitions\", \"subscribe\", \"subscribeToCategory\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 6, \"minHeight\": 1, \"defaultWidth\": 12, \"defaultHeight\": 1}", - "tags": "[\"Settings\", \"Theme Settings\"]" - }, - { - "id": "componentGeneralSettings", - "plugin_id": "BrainDriveSettings", - "name": "ComponentGeneralSettings", - "display_name": "General Settings", - "description": "Manage general application settings", - "icon": "Settings", - "category": "Settings", - "enabled": True, - "priority": 1, - "props": "{}", - "config_fields": "{}", - "messages": "{\"sends\": [], \"receives\": []}", - "required_services": "{\"api\": {\"methods\": [\"get\", \"post\"], \"version\": \"1.0.0\"}, \"theme\": {\"methods\": [\"getCurrentTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 6, \"minHeight\": 1, \"defaultWidth\": 12, \"defaultHeight\": 1}", - "tags": "[\"Settings\", \"General Settings\"]" - }, - { - "id": "componentOllamaServer", - "plugin_id": "BrainDriveSettings", - "name": "ComponentOllamaServer", - "display_name": "Ollama Servers", - "description": "Manage multiple Ollama server connections", - "icon": "Storage", - "category": "LLM Servers", - "enabled": True, - "priority": 1, - "props": "{}", - "config_fields": "{}", - "messages": "{\"sends\": [], \"receives\": []}", - "required_services": "{\"api\": {\"methods\": [\"get\", \"post\", \"delete\"], \"version\": \"1.0.0\"}, \"theme\": {\"methods\": [\"getCurrentTheme\", \"addThemeChangeListener\", \"removeThemeChangeListener\"], \"version\": \"1.0.0\"}}", - "dependencies": "[]", - "layout": "{\"minWidth\": 6, \"minHeight\": 4, \"defaultWidth\": 8, \"defaultHeight\": 5}", - "tags": "[\"Settings\", \"Ollama Server Settings\", \"Multiple Servers\"]" - } - ] - - async def initialize(self, user_id: str, db: AsyncSession, **kwargs) -> bool: - """Initialize BrainDrive Settings plugin and its modules for a new user.""" - try: - logger.info(f"Initializing BrainDrive Settings plugin for user {user_id}") - - # Create the plugin for the user - plugin_data = self.PLUGIN_DATA.copy() - - # Prepare the plugin data for the new user - prepared_plugin = prepare_record_for_new_user( - plugin_data, - user_id, - preserve_fields=["name", "description", "version", "type", "icon", - "category", "status", "official", "author", - "last_updated", "compatibility", "scope", - "bundle_method", "bundle_location", "is_local", - "long_description", "plugin_slug"], - user_id_field="user_id" - ) - - # Update the ID to use the new format: user_id_plugin_slug - plugin_slug = prepared_plugin.get("plugin_slug", "BrainDriveSettings") - prepared_plugin["id"] = f"{user_id}_{plugin_slug}" - - # Create the plugin using direct SQL - current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Create SQL statement for plugin - plugin_stmt = text(""" - INSERT INTO plugin - (id, name, description, version, type, enabled, icon, category, status, - official, author, last_updated, compatibility, downloads, scope, - bundle_method, bundle_location, is_local, long_description, - config_fields, messages, dependencies, created_at, updated_at, user_id, plugin_slug) - VALUES - (:id, :name, :description, :version, :type, :enabled, :icon, :category, - :status, :official, :author, :last_updated, :compatibility, :downloads, - :scope, :bundle_method, :bundle_location, :is_local, :long_description, - :config_fields, :messages, :dependencies, :created_at, :updated_at, :user_id, :plugin_slug) - """) - - # Execute statement with parameters - await db.execute(plugin_stmt, { - "id": prepared_plugin["id"], - "name": prepared_plugin["name"], - "description": prepared_plugin["description"], - "version": prepared_plugin["version"], - "type": prepared_plugin["type"], - "enabled": prepared_plugin.get("enabled", True), - "icon": prepared_plugin.get("icon"), - "category": prepared_plugin.get("category"), - "status": prepared_plugin.get("status", "activated"), - "official": prepared_plugin.get("official", True), - "author": prepared_plugin.get("author", "BrainDrive Team"), - "last_updated": prepared_plugin.get("last_updated"), - "compatibility": prepared_plugin.get("compatibility", "1.0.0"), - "downloads": prepared_plugin.get("downloads", 0), - "scope": prepared_plugin.get("scope"), - "bundle_method": prepared_plugin.get("bundle_method"), - "bundle_location": prepared_plugin.get("bundle_location"), - "is_local": prepared_plugin.get("is_local", False), - "long_description": prepared_plugin.get("long_description"), - "config_fields": prepared_plugin.get("config_fields"), - "messages": prepared_plugin.get("messages"), - "dependencies": prepared_plugin.get("dependencies"), - "created_at": current_time, - "updated_at": current_time, - "user_id": user_id, - "plugin_slug": prepared_plugin.get("plugin_slug") - }) - - logger.info(f"Created plugin {prepared_plugin['name']} for user {user_id}") - - # Now initialize all modules for this plugin - for module_data in self.MODULE_DATA: - # Convert JSON strings to actual JSON for required_services, messages, etc. - module_data_processed = module_data.copy() - - # Prepare the module data for the new user - prepared_module = prepare_record_for_new_user( - module_data_processed, - user_id, - preserve_fields=["name", "display_name", "description", "icon", - "category", "enabled", "priority", "props", - "config_fields", "messages", "required_services", - "dependencies", "layout", "tags"], - user_id_field="user_id" - ) - - # Create the module using direct SQL - module_stmt = text(""" - INSERT INTO module - (id, plugin_id, name, display_name, description, icon, category, - enabled, priority, props, config_fields, messages, required_services, - dependencies, layout, tags, created_at, updated_at, user_id) - VALUES - (:id, :plugin_id, :name, :display_name, :description, :icon, :category, - :enabled, :priority, :props, :config_fields, :messages, :required_services, - :dependencies, :layout, :tags, :created_at, :updated_at, :user_id) - """) - - # Execute statement with parameters - await db.execute(module_stmt, { - "id": prepared_module["id"], - "plugin_id": prepared_plugin["id"], # Use the new plugin ID - "name": prepared_module["name"], - "display_name": prepared_module.get("display_name"), - "description": prepared_module.get("description"), - "icon": prepared_module.get("icon"), - "category": prepared_module.get("category"), - "enabled": prepared_module.get("enabled", True), - "priority": prepared_module.get("priority", 0), - "props": prepared_module.get("props"), - "config_fields": prepared_module.get("config_fields"), - "messages": prepared_module.get("messages"), - "required_services": prepared_module.get("required_services"), - "dependencies": prepared_module.get("dependencies"), - "layout": prepared_module.get("layout"), - "tags": prepared_module.get("tags"), - "created_at": current_time, - "updated_at": current_time, - "user_id": user_id - }) - - logger.info(f"Created module {prepared_module['name']} for user {user_id}") - - # Create directory structure and copy plugin files - try: - # Define paths - original_plugin_dir = PLUGINS_DIR / "BrainDriveSettings" - user_dir = PLUGINS_DIR / user_id - plugin_dir = user_dir / prepared_plugin["id"] - - # Create user directory if it doesn't exist - if not user_dir.exists(): - user_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created user directory: {user_dir}") - - # Create plugin directory if it doesn't exist - if not plugin_dir.exists(): - plugin_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created plugin directory: {plugin_dir}") - - # Create frontend/dist directory structure - frontend_dist_dir = plugin_dir / "frontend" / "dist" - frontend_dist_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created frontend/dist directory: {frontend_dist_dir}") - - # Copy original plugin files to user plugin directory - if original_plugin_dir.exists(): - # Copy files from original plugin directory to user plugin directory - for item in original_plugin_dir.glob("**/*"): - if item.is_file(): - # Get relative path from original plugin directory - rel_path = item.relative_to(original_plugin_dir) - # Create destination path - dest_path = plugin_dir / rel_path - # Create parent directories if they don't exist - dest_path.parent.mkdir(parents=True, exist_ok=True) - # Copy the file - shutil.copy2(item, dest_path) - logger.info(f"Copied {item} to {dest_path}") - - # Create a dummy remoteEntry.js file in the frontend/dist directory if it doesn't exist - remote_entry_path = frontend_dist_dir / "remoteEntry.js" - if not remote_entry_path.exists(): - with open(remote_entry_path, 'w') as f: - f.write('// Placeholder for remote entry file\n') - f.write('window.BrainDriveSettings = { get: function() { return Promise.resolve(() => ({ default: {} })); } };\n') - logger.info(f"Created placeholder remoteEntry.js file at {remote_entry_path}") - - logger.info(f"Copied original plugin files to user plugin directory: {plugin_dir}") - else: - logger.warning(f"Original plugin directory not found: {original_plugin_dir}") - except Exception as e: - logger.error(f"Error creating directory structure or copying files: {e}") - # Continue with the initialization even if file copying fails - - await db.commit() - logger.info(f"BrainDrive Settings plugin initialized successfully for user {user_id}") - return True - - except Exception as e: - logger.error(f"Error initializing BrainDrive Settings plugin for user {user_id}: {e}") - await db.rollback() - return False - - async def cleanup(self, user_id: str, db: AsyncSession, **kwargs) -> bool: - """Clean up the plugin and its modules if initialization fails.""" - try: - logger.info(f"Cleaning up BrainDrive Settings plugin for user {user_id}") - - # Delete all modules for this plugin for this user - module_stmt = text(""" - DELETE FROM module - WHERE user_id = :user_id AND plugin_id IN ( - SELECT id FROM plugin WHERE user_id = :user_id AND name = 'BrainDrive Settings' - ) - """) - - await db.execute(module_stmt, {"user_id": user_id}) - - # Delete plugin - plugin_stmt = text(""" - DELETE FROM plugin - WHERE user_id = :user_id AND name = 'BrainDrive Settings' - """) - - await db.execute(plugin_stmt, {"user_id": user_id}) - - # Clean up directory structure - try: - plugin_id = f"{user_id}_BrainDriveSettings" - plugin_dir = PLUGINS_DIR / user_id / plugin_id - - if plugin_dir.exists(): - shutil.rmtree(plugin_dir) - logger.info(f"Removed plugin directory: {plugin_dir}") - - # Check if user directory is empty and remove it if it is - user_dir = PLUGINS_DIR / user_id - if user_dir.exists() and not any(user_dir.iterdir()): - user_dir.rmdir() - logger.info(f"Removed empty user directory: {user_dir}") - except Exception as e: - logger.error(f"Error cleaning up directory structure: {e}") - # Continue with the cleanup even if directory removal fails - - await db.commit() - logger.info(f"BrainDrive Settings plugin cleaned up successfully for user {user_id}") - return True - - except Exception as e: - logger.error(f"Error cleaning up BrainDrive Settings plugin for user {user_id}: {e}") - await db.rollback() - return False - -# Register the initializer -register_initializer(BrainDriveSettingsInitializer) \ No newline at end of file diff --git a/backend/app/core/user_initializer/initializers/github_plugin_initializer.py b/backend/app/core/user_initializer/initializers/github_plugin_initializer.py new file mode 100644 index 0000000..59007ad --- /dev/null +++ b/backend/app/core/user_initializer/initializers/github_plugin_initializer.py @@ -0,0 +1,150 @@ +""" +GitHub Plugin Initializer + +This initializer installs plugins from GitHub repositories during user registration. +Uses the same installation function as the frontend for perfect consistency. +""" + +import logging +from typing import Dict, Any, List +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.user_initializer.base import UserInitializerBase +from app.core.user_initializer.registry import register_initializer + +logger = logging.getLogger(__name__) + +class GitHubPluginInitializer(UserInitializerBase): + """ + Initializer that installs plugins from GitHub repositories. + + Uses the same install_plugin_from_url() function as the frontend + to ensure identical installation behavior and error handling. + """ + + name = "github_plugin_initializer" + description = "Installs default plugins from GitHub repositories" + priority = 400 # Run after core system setup but before pages + dependencies = ["settings_initializer", "components_initializer", "navigation_initializer"] # Run after core setup + + # Default plugins to install for new users + DEFAULT_PLUGINS = [ + { + "repo_url": "https://github.com/DJJones66/BrainDriveSettings", + "version": "latest", + "name": "BrainDrive Settings" + }, + { + "repo_url": "https://github.com/DJJones66/BrainDriveChat", + "version": "latest", + "name": "BrainDrive Chat" + } + ] + + async def initialize(self, user_id: str, db: AsyncSession, **kwargs) -> bool: + """ + Install default GitHub plugins for the new user. + + Args: + user_id: The ID of the newly registered user + db: Database session + **kwargs: Additional arguments (unused) + + Returns: + bool: True if all plugins installed successfully, False otherwise + """ + logger.info(f"Starting GitHub plugin installation for user {user_id}") + + # Import the same function used by the frontend + try: + from app.plugins.remote_installer import install_plugin_from_url + except ImportError as e: + logger.error(f"Failed to import install_plugin_from_url: {e}") + return False + + successful_installs = [] + failed_installs = [] + + for plugin_config in self.DEFAULT_PLUGINS: + repo_url = plugin_config["repo_url"] + version = plugin_config["version"] + name = plugin_config["name"] + + logger.info(f"Installing {name} from {repo_url} (version: {version}) for user {user_id}") + + try: + # Use the exact same function as the frontend + result = await install_plugin_from_url( + repo_url=repo_url, + user_id=user_id, + version=version + ) + + if result.get("success", False): + successful_installs.append({ + "name": name, + "repo_url": repo_url, + "plugin_id": result.get("plugin_id"), + "plugin_slug": result.get("plugin_slug") + }) + logger.info(f"Successfully installed {name} for user {user_id}") + else: + error_msg = result.get("error", "Unknown error") + failed_installs.append({ + "name": name, + "repo_url": repo_url, + "error": error_msg + }) + logger.error(f"Failed to install {name} for user {user_id}: {error_msg}") + + except Exception as e: + failed_installs.append({ + "name": name, + "repo_url": repo_url, + "error": str(e) + }) + logger.error(f"Exception installing {name} for user {user_id}: {str(e)}") + + # Log summary + logger.info(f"GitHub plugin installation complete for user {user_id}: " + f"{len(successful_installs)} successful, {len(failed_installs)} failed") + + if successful_installs: + logger.info(f"Successfully installed: {[p['name'] for p in successful_installs]}") + + if failed_installs: + logger.warning(f"Failed to install: {[p['name'] for p in failed_installs]}") + + # Return True only if all plugins installed successfully + return len(failed_installs) == 0 + + async def cleanup(self, user_id: str, db: AsyncSession, **kwargs) -> bool: + """ + Clean up any plugins that were installed if initialization fails. + + Args: + user_id: The ID of the user + db: Database session + **kwargs: Additional arguments + + Returns: + bool: True if cleanup was successful + """ + logger.info(f"Cleaning up GitHub plugins for user {user_id}") + + try: + # For now, we'll implement basic cleanup logging + # More sophisticated cleanup could be added later if needed + logger.info(f"GitHub plugin cleanup initiated for user {user_id}") + + # Note: The existing plugin system should handle cleanup through + # the normal plugin uninstall mechanisms if needed + + return True + + except Exception as e: + logger.error(f"Error during GitHub plugin cleanup for user {user_id}: {str(e)}") + return False + +# Register the initializer +register_initializer(GitHubPluginInitializer) \ No newline at end of file diff --git a/backend/app/core/user_initializer/initializers/pages_initializer.py b/backend/app/core/user_initializer/initializers/pages_initializer.py index e992912..9c15154 100644 --- a/backend/app/core/user_initializer/initializers/pages_initializer.py +++ b/backend/app/core/user_initializer/initializers/pages_initializer.py @@ -26,7 +26,7 @@ class PagesInitializer(UserInitializerBase): name = "pages_initializer" description = "Initializes default pages for a new user" priority = 600 # Run after navigation routes - dependencies = ["navigation_initializer", "brain_drive_basic_ai_chat_initializer"] # Depends on navigation routes and AI Chat plugin + dependencies = ["navigation_initializer", "github_plugin_initializer"] # Depends on navigation routes and GitHub plugin installer # Default pages templates DEFAULT_PAGES = [ @@ -45,7 +45,7 @@ class PagesInitializer(UserInitializerBase): async def get_module_ids(self, user_id: str, db: AsyncSession) -> Dict[str, str]: """ - Get the module IDs for a user's BrainDrive Basic AI Chat plugin. + Get the module IDs for a user's BrainDrive Chat plugin (GitHub-installed). Args: user_id: The user ID @@ -55,17 +55,17 @@ async def get_module_ids(self, user_id: str, db: AsyncSession) -> Dict[str, str] Dict[str, str]: A dictionary mapping module names to their IDs """ try: - # Get the plugin ID for the user's BrainDrive Basic AI Chat plugin + # Get the plugin ID for the user's BrainDrive Chat plugin (GitHub-installed) plugin_stmt = text(""" SELECT id FROM plugin - WHERE user_id = :user_id AND plugin_slug = 'BrainDriveBasicAIChat' + WHERE user_id = :user_id AND plugin_slug = 'BrainDriveChat' """) plugin_result = await db.execute(plugin_stmt, {"user_id": user_id}) plugin_id = plugin_result.scalar_one_or_none() if not plugin_id: - logger.error(f"BrainDriveBasicAIChat plugin not found for user {user_id}") + logger.error(f"BrainDriveChat plugin not found for user {user_id}") return {} # Get the module IDs for the plugin @@ -96,7 +96,7 @@ async def initialize(self, user_id: str, db: AsyncSession, **kwargs) -> bool: try: logger.info(f"Initializing pages for user {user_id}") - # Get the module IDs for the user's BrainDrive Basic AI Chat plugin + # Get the module IDs for the user's BrainDrive Chat plugin (GitHub-installed) module_info = await self.get_module_ids(user_id, db) if not module_info: @@ -107,73 +107,77 @@ async def initialize(self, user_id: str, db: AsyncSession, **kwargs) -> bool: module_ids = module_info.get("module_ids", {}) if not plugin_id: - logger.error(f"BrainDriveBasicAIChat plugin not found for user {user_id}") + logger.error(f"BrainDriveChat plugin not found for user {user_id}") return False # Create pages for the user using default pages for page_data in self.DEFAULT_PAGES: # If this is the AI Chat page, generate dynamic content if page_data["name"] == "AI Chat": - # Generate unique module IDs for the content JSON + # Generate unique module ID for the BrainDriveChat interface timestamp_base = int(datetime.datetime.now().timestamp() * 1000) # Use milliseconds for timestamp - model_selection_module_id = f"BrainDriveBasicAIChat_{module_ids.get('ComponentModelSelection', '')}_{timestamp_base}" - chat_history_module_id = f"BrainDriveBasicAIChat_{module_ids.get('AIChatHistory', '')}_{timestamp_base + 2000}" # Add 2 seconds - ai_prompt_chat_module_id = f"BrainDriveBasicAIChat_{module_ids.get('AIPromptChat', '')}_{timestamp_base + 5000}" # Add 5 seconds - # Generate the content JSON with the actual module IDs + # Get the BrainDriveChat module ID (should be the only module in the plugin) + brain_drive_chat_module_id = None + for module_name, module_id in module_ids.items(): + if 'BrainDriveChat' in module_name: + brain_drive_chat_module_id = module_id + break + + if not brain_drive_chat_module_id: + logger.error(f"BrainDriveChat module not found for user {user_id}") + return False + + # Generate unique identifier for the layout + chat_interface_id = f"BrainDriveChat_{brain_drive_chat_module_id}_{timestamp_base}" + + # Generate the content JSON with the actual module IDs (new BrainDriveChat structure) page_data["content"] = { "layouts": { "desktop": [ - {"moduleUniqueId": model_selection_module_id, "i": model_selection_module_id, "x": 0, "y": 0, "w": 6, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": chat_history_module_id, "i": chat_history_module_id, "x": 6, "y": 0, "w": 6, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": ai_prompt_chat_module_id, "i": ai_prompt_chat_module_id, "x": 0, "y": 1, "w": 12, "h": 5, "minW": 4, "minH": 4} + { + "i": chat_interface_id, + "x": 0, + "y": 0, + "w": 12, + "h": 10, + "pluginId": "BrainDriveChat", + "args": { + "moduleId": brain_drive_chat_module_id, + "displayName": "AI Chat Interface" + } + } ], "tablet": [ - {"moduleUniqueId": model_selection_module_id, "i": model_selection_module_id, "x": 0, "y": 0, "w": 6, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": chat_history_module_id, "i": chat_history_module_id, "x": 0, "y": 1, "w": 6, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": ai_prompt_chat_module_id, "i": ai_prompt_chat_module_id, "x": 0, "y": 2, "w": 6, "h": 6, "minW": 4, "minH": 4} + { + "i": chat_interface_id, + "x": 1, + "y": 0, + "w": 4, + "h": 3, + "pluginId": "BrainDriveChat", + "args": { + "moduleId": brain_drive_chat_module_id, + "displayName": "AI Chat Interface" + } + } ], "mobile": [ - {"moduleUniqueId": model_selection_module_id, "i": model_selection_module_id, "x": 0, "y": 0, "w": 4, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": chat_history_module_id, "i": chat_history_module_id, "x": 0, "y": 1, "w": 4, "h": 1, "minW": 3, "minH": 1}, - {"moduleUniqueId": ai_prompt_chat_module_id, "i": ai_prompt_chat_module_id, "x": 0, "y": 2, "w": 4, "h": 6, "minW": 4, "minH": 4} + { + "i": chat_interface_id, + "x": 1, + "y": 0, + "w": 4, + "h": 3, + "pluginId": "BrainDriveChat", + "args": { + "moduleId": brain_drive_chat_module_id, + "displayName": "AI Chat Interface" + } + } ] }, - "modules": { - model_selection_module_id.replace("-", ""): { - "pluginId": "BrainDriveBasicAIChat", - "moduleId": module_ids.get('ComponentModelSelection', ''), - "moduleName": "ComponentModelSelection", - "config": { - "moduleId": module_ids.get('ComponentModelSelection', ''), - "label": "Select Model", - "labelPosition": "top", - "providerSettings": ["ollama_servers_settings"], - "targetComponent": "", - "displayName": "Model Selection v2" - } - }, - chat_history_module_id.replace("-", ""): { - "pluginId": "BrainDriveBasicAIChat", - "moduleId": module_ids.get('AIChatHistory', ''), - "moduleName": "AIChatHistory", - "config": { - "moduleId": module_ids.get('AIChatHistory', ''), - "displayName": "AI Chat History" - } - }, - ai_prompt_chat_module_id.replace("-", ""): { - "pluginId": "BrainDriveBasicAIChat", - "moduleId": module_ids.get('AIPromptChat', ''), - "moduleName": "AIPromptChat", - "config": { - "initialGreeting": "Hello! How can I assist you today?", - "promptQuestion": "Type your message here...", - "moduleId": module_ids.get('AIPromptChat', ''), - "displayName": "AI Prompt Chat V2" - } - } - } + "modules": {} } # Prepare the page data for the new user # This will: