Skip to content
Back to top

# Source code for mindroot.lib.plugins.installation

    import os
    import sys
    import json
    import shutil
    import tempfile
    import requests
    import zipfile
    import subprocess
    from pkg_resources import require as pkg_require
    from .manifest import update_plugin_manifest
    import traceback

    def download_github_files(repo_path, tag=None):
        """Download GitHub repo files to temp directory.

        Args:
            repo_path (str): GitHub repository path (user/repo)
            tag (str, optional): Specific tag to download

        Returns:
            tuple: (plugin_dir, plugin_root, plugin_info)
        """
        download_url = f"https://github.com/{repo_path}/archive/refs/tags/{tag}.zip" if tag else \
                      f"https://github.com/{repo_path}/archive/refs/heads/main.zip"

        repo = repo_path.split("/")[1]
        temp_dir = tempfile.mkdtemp()
        zip_path = os.path.join(temp_dir, 'repo.zip')

        print(f"Github install: Downloading {download_url} to {zip_path}")
        try:
            response = requests.get(download_url)
            response.raise_for_status()

            with open(zip_path, 'wb') as f:
                f.write(response.content)

            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
                zip_ref.extractall(temp_dir)

            os.remove(zip_path)

            # Find plugin_info.json
            for root, _, files in os.walk(temp_dir):
                if 'plugin_info.json' in files:
                    with open(os.path.join(root, 'plugin_info.json'), 'r') as f:
                        plugin_info = json.load(f)

                    # Setup local plugin directory
                    if os.path.exists(f"./local/plugins/{repo}"):
                        shutil.rmtree(f"./local/plugins/{repo}")

                    shutil.move(temp_dir, f"./local/plugins/{repo}")

                    # Handle subdirectory structure
                    files = os.listdir(f"./local/plugins/{repo}")
                    subdir = [f for f in files if os.path.isdir(os.path.join(f"./local/plugins/{repo}", f))][0]
                    if subdir != repo:
                        shutil.move(f"./local/plugins/{repo}/{subdir}", f"./local/plugins/{repo}/{repo}")

                    return f"./local/plugins/{repo}/{repo}", root, plugin_info

            raise ValueError("No plugin_info.json found in repository")

        except Exception as e:
            shutil.rmtree(temp_dir)
            raise e

    def check_plugin_dependencies(plugin_path):
        """Check if all plugin dependencies are met.

        Args:
            plugin_path (str): Path to plugin directory

        Returns:
            bool: True if all dependencies are met
        """
        requirements_file = os.path.join(plugin_path, 'requirements.txt')
        if os.path.exists(requirements_file):
            with open(requirements_file, 'r') as f:
                requirements = f.read().splitlines()
            for requirement in requirements:
                try:
                    pkg_require(requirement)
                except:
                    return False
        return True

    def install_plugin_dependencies(plugin_path):
        """Install plugin dependencies from requirements.txt.

        Args:
            plugin_path (str): Path to plugin directory

        Returns:
            bool: True if installation successful
        """
        requirements_file = os.path.join(plugin_path, 'requirements.txt')
        if os.path.exists(requirements_file):
            try:
                subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', requirements_file])
                return True
            except subprocess.CalledProcessError:
                return False
        return True

    async def plugin_install(plugin_name, source='pypi', source_path=None, remote_source=None):
        """Install a plugin from various sources.

        Args:
            plugin_name (str): Name of the plugin
            source (str): Installation source ('pypi', 'local', 'github')
            source_path (str, optional): Path or GitHub repo reference
            remote_source (str, optional): Remote source for the plugin (e.g., GitHub repo path)

        Returns:
            bool: True if installation successful
        """
        try:
            if source == 'local':
                if not source_path:
                    raise ValueError("source_path is required for local installation")
                if not os.path.isfile(os.path.join(source_path, 'setup.py')) and \
                   not os.path.isfile(os.path.join(source_path, 'pyproject.toml')):
                    raise ValueError(f"Invalid Python project: no setup.py or pyproject.toml found")

                # Try to read plugin_info.json
                plugin_info = None
                plugin_info_path = os.path.join(source_path, 'plugin_info.json')
                if os.path.exists(plugin_info_path):
                    with open(plugin_info_path, 'r') as f:
                        plugin_info = json.load(f)

                subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', source_path])
                update_plugin_manifest(plugin_name, 'local', source_path, metadata=plugin_info)

            elif source == 'pypi':
                subprocess.check_call([sys.executable, '-m', 'pip', 'install', plugin_name])
                update_plugin_manifest(plugin_name, 'pypi', None)

            elif source == 'github':
                if not source_path:
                    raise ValueError("source_path (repo/name[:tag]) required for GitHub installation")

                parts = source_path.split(':')
                repo_path = parts[0]
                tag = parts[1] if len(parts) > 1 else None

                plugin_dir, _, plugin_info = download_github_files(repo_path, tag)
                subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', plugin_dir])

                update_plugin_manifest(
                    plugin_info['name'],
                    'github',
                    os.path.abspath(plugin_dir),
                    remote_source=repo_path,
                    version=plugin_info.get('version', '0.0.1'),
                    metadata=plugin_info
                )

            else:
                raise ValueError(f"Unsupported installation source: {source}")

            return True

        except Exception as e:
            raise RuntimeError(f"Plugin installation failed: {str(e)}")

    def plugin_update(plugin_name):
        """Update an installed plugin.

        Args:
            plugin_name (str): Name of the plugin to update

        Returns:
            bool: True if update successful
        """
        # This is a synchronous function that can't use the async plugin_install directly
        try:
            # Just use pip directly for now
            subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', plugin_name])
            return True
        except Exception as e:
            print(f"Error updating plugin {plugin_name}: {str(e)}")
            return False

    # Import pkg_resources here to avoid circular imports

    async def install_recommended_plugins(agent_name, context=None):
        """
        Install plugins recommended for an agent based on its required_plugins list.

        Args:
            agent_name (str): Name of the agent

        Returns:
            dict: Results of plugin installation
        """
        try:
            # Load agent data
            from pathlib import Path
            agent_path = f"data/agents/local/{agent_name}/agent.json"
            if not os.path.exists(agent_path):
                agent_path = f"data/agents/shared/{agent_name}/agent.json"

            if not os.path.exists(agent_path):
                return {"error": f"Agent {agent_name} not found"}

            with open(agent_path, 'r') as f:
                agent_data = json.load(f)

            # Get recommended plugins (check both fields for backward compatibility)
            recommended_plugins = agent_data.get('recommended_plugins', agent_data.get('required_plugins', []))

            # Get plugin sources from check_recommended_plugins endpoint
            from fastapi import HTTPException
            try:
                # Implement the same logic as check_recommended_plugins directly here
                # to avoid circular imports
                plugin_info = {}
                plugin_sources = {}

                # Define indices directory path
                # Try multiple possible locations for indices
                possible_locations = [
                    Path("indices"),
                    Path("data/indices"),
                    Path(os.getcwd()) / "indices",
                    Path(os.getcwd()).parent / "indices",
                    Path("/files/mindroot/indices")
                ]

                indices_dir = None

                # Determine which indices directory exists
                for location in possible_locations:
                    print(f"Checking for indices in: {location}")
                    if location.exists():
                        # Check if it has any .json files
                        indices_dir = location if any(location.glob("*.json")) else None
                        print(f"Found indices directory at: {indices_dir} with {len(list(location.glob('*.json')))} JSON files")
                        break

                if not indices_dir:
                    print("WARNING: Could not find indices directory in any of the expected locations")
                    indices_dir = Path("indices")  # Fallback to default
                # Get all available indices
                available_indices = []
                for index_file in indices_dir.glob("*.json"):
                    # Also try looking for indices in subdirectories
                    print(f"Found index file: {index_file}")

                    try:
                        print(f"Reading index file for plugin sources: {index_file}")
                        with open(index_file, 'r') as f:
                            index_data = json.load(f)
                            available_indices.append(index_data)
                    except Exception as e:
                        print(f"Error reading index file {index_file}: {str(e)}")

                # Also check for indices in subdirectories
                for subdir in indices_dir.glob("*/"):
                    # Also try looking for indices in subdirectories
                    print(f"Checking for index files in subdirectory: {subdir}")

                    # Look for index.json in the subdirectory
                    index_file_path = subdir / "index.json"
                    if index_file_path.exists():
                        try:
                            print(f"Reading index file: {index_file_path}")
                            with open(index_file_path, 'r') as f:
                                index_data = json.load(f)
                                available_indices.append(index_data)
                        except Exception as e:
                            print(f"Error reading index file {index_file_path}: {str(e)}")
                    else:
                        print(f"No index.json found in {subdir}")

                print(f"Found {len(available_indices)} indices with {sum(len(index.get('plugins', [])) for index in available_indices)} total plugins")

                # Look for each recommended plugin in the indices
                print(f"Looking for recommended plugins: {recommended_plugins}")
                for plugin_source in recommended_plugins:
                    for index in available_indices:
                        for plugin in index.get('plugins', []):
                            remote_source = plugin.get('remote_source') or plugin.get('github_url') or plugin.get('source')
                            print(f"Checking index plugin {remote_source} against recommended {plugin_source}")
                            if remote_source == plugin_source:
                                if remote_source and 'github.com/' in remote_source:
                                    github_path = remote_source.split('github.com/')[1]
                                    print(f"Extracted GitHub path for {plugin_source}: {github_path}")
                                    plugin_sources[plugin_source] = github_path
                                elif remote_source:
                                    plugin_sources[plugin_source] = remote_source
                print(f"Found plugin sources: {plugin_sources}")

                plugin_info = {"plugin_sources": plugin_sources}
                plugin_sources = plugin_info.get("plugin_sources", {}) if plugin_info else {}
            except Exception as e:
                trace = traceback.format_exc()
                print(f"Error getting plugin sources: {str(e)} {trace}")
                plugin_sources = {}

            # Install each recommended plugin
            results = []
            for plugin_source in recommended_plugins:
                # Check if plugin is already installed
                try:
                    # Try to import the plugin to check if it's installed
                    plugin_name = plugin_source.split('/')[-1]

                    try:
                        # Use pkg_resources to check if package is installed
                        import pkg_resources
                        pkg_resources.get_distribution(plugin_name)
                        # Plugin is already installed
                        results.append({"plugin": plugin_name, "status": "already_installed"})
                        print("Plugin already installed:", plugin_name)
                    except pkg_resources.DistributionNotFound:
                        # Plugin is not installed, get source info and install it
                        try:
                            # Get the source information for this plugin
                            source_info = plugin_source
                            print(f"Installing plugin {plugin_name} from source: {source_info}")
                            # Determine installation source and path
                            if '/' in source_info:  # GitHub repo format (user/repo)
                                source = 'github'
                                source_path = source_info
                                remote_source = source_info
                            else:  # PyPI package
                                source = 'pypi'
                                source_path = None
                                remote_source = None

                            # Install the plugin
                            await plugin_install(
                                plugin_name,
                                source,
                                source_path,
                                remote_source
                            )

                            results.append({
                                "plugin": plugin_name,
                                "status": "success",
                                "source": source,
                                "source_path": source_path
                            })
                        except Exception as e:
                            results.append({
                                "plugin": plugin_name,
                                "status": "error",
                                "message": str(e),
                                "source_info": source_info
                            })
                except Exception as e:
                    results.append({
                        "plugin": plugin_name,
                        "status": "error",
                        "message": f"Error checking installation: {str(e)}"
                    })

            return {"results": results}
        except Exception as e:
            return {"error": str(e)}