DEV Community

biao lin
biao lin

Posted on

Building a Plug-and-Play Agent Architecture with Python

Building a Plug-and-Play Agent Architecture with Python

Modern software systems increasingly rely on intelligent agents that can perceive their environment, make decisions, and execute actions. However, building a monolithic agent system often leads to tight coupling, poor maintainability, and limited extensibility. In this post, we'll design a plug-and-play agent architecture using Python that allows you to hot-swap components, add new capabilities without modifying core code, and maintain clean separation of concerns.

Why Plug-and-Play?

Traditional agent architectures suffer from several pain points:

  • Rigid component relationships: Changing one part often requires changes throughout the system
  • Difficult testing: Hard to isolate and test individual components
  • Poor reusability: Components are tightly coupled to specific applications
  • Limited extensibility: Adding new capabilities requires modifying existing code

A plug-and-play architecture solves these by defining clear interfaces, using dependency injection, and implementing a registry pattern for dynamic component discovery.

Core Architecture Overview

Our architecture consists of three main layers:

  1. Agent Registry: Central catalog of available components
  2. Plugin System: Mechanism for loading and integrating components
  3. Agent Core: Runtime engine that orchestrates components

Let's implement each layer with practical code examples.

1. The Agent Registry

The registry serves as a service locator for all agent components. It provides lookup, registration, and lifecycle management.

# agent_registry.py
from typing import Dict, Type, Any, Optional, List
import inspect
import logging

logger = logging.getLogger(__name__)

class AgentRegistry:
    """Central registry for agent components."""

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._components: Dict[str, Dict[str, Any]] = {}
            cls._instance._plugins: Dict[str, Type] = {}
        return cls._instance

    def register_component(self, name: str, component: Any, 
                          component_type: str, metadata: Optional[Dict] = None):
        """Register a component with the registry."""
        self._components[name] = {
            'instance': component,
            'type': component_type,
            'metadata': metadata or {}
        }
        logger.info(f"Registered component '{name}' of type '{component_type}'")

    def get_component(self, name: str) -> Optional[Any]:
        """Retrieve a registered component by name."""
        if name in self._components:
            return self._components[name]['instance']
        return None

    def get_components_by_type(self, component_type: str) -> List[Dict]:
        """Get all components of a specific type."""
        return [
            {name: info} for name, info in self._components.items()
            if info['type'] == component_type
        ]

    def unregister_component(self, name: str):
        """Remove a component from the registry."""
        if name in self._components:
            del self._components[name]
            logger.info(f"Unregistered component '{name}'")

    def list_components(self) -> List[str]:
        """List all registered component names."""
        return list(self._components.keys())

    def register_plugin(self, plugin_class: Type):
        """Register a plugin class for dynamic loading."""
        if hasattr(plugin_class, 'plugin_name'):
            name = plugin_class.plugin_name
        else:
            name = plugin_class.__name__
        self._plugins[name] = plugin_class
        logger.info(f"Registered plugin '{name}'")

    def get_plugin(self, name: str) -> Optional[Type]:
        """Get a registered plugin class."""
        return self._plugins.get(name)
Enter fullscreen mode Exit fullscreen mode

2. The Plugin System

The plugin system handles dynamic loading of components from external modules, enabling runtime extensibility.

# plugin_system.py
import importlib
import pkgutil
import inspect
import logging
from typing import List, Type, Optional
from pathlib import Path

logger = logging.getLogger(__name__)

class PluginSystem:
    """Manages dynamic plugin loading and lifecycle."""

    def __init__(self, registry: 'AgentRegistry'):
        self.registry = registry
        self.loaded_plugins = {}

    def discover_plugins(self, package_path: str) -> List[str]:
        """Discover available plugins in a package."""
        discovered = []
        try:
            package = importlib.import_module(package_path)
            for importer, modname, ispkg in pkgutil.iter_modules(package.__path__):
                if ispkg:  # Only load packages (not individual modules)
                    discovered.append(f"{package_path}.{modname}")
            logger.info(f"Discovered {len(discovered)} potential plugins")
        except ImportError as e:
            logger.error(f"Failed to discover plugins: {e}")
        return discovered

    def load_plugin(self, plugin_module_path: str) -> Optional[Type]:
        """Load a plugin from a module path."""
        try:
            module = importlib.import_module(plugin_module_path)

            # Find plugin classes (classes that inherit from BasePlugin)
            for name, obj in inspect.getmembers(module):
                if (inspect.isclass(obj) and 
                    issubclass(obj, BasePlugin) and 
                    obj != BasePlugin):
                    self.registry.register_plugin(obj)
                    self.loaded_plugins[plugin_module_path] = obj
                    logger.info(f"Loaded plugin '{name}' from {plugin_module_path}")
                    return obj
            logger.warning(f"No plugin class found in {plugin_module_path}")
        except ImportError as e:
            logger.error(f"Failed to load plugin {plugin_module_path}: {e}")
        return None

    def load_all_plugins(self, package_path: str) -> int:
        """Discover and load all plugins in a package."""
        count = 0
        plugin_paths = self.discover_plugins(package_path)
        for path in plugin_paths:
            if self.load_plugin(path):
                count += 1
        logger.info(f"Loaded {count} plugins from {package_path}")
        return count

    def unload_plugin(self, plugin_module_path: str):
        """Unload a previously loaded plugin."""
        if plugin_module_path in self.loaded_plugins:
            plugin_class = self.loaded_plugins[plugin_module_path]
            # Clean up any registered components from this plugin
            for name, info in list(self.registry._components.items()):
                if hasattr(info['instance'], '__class__'):
                    if info['instance'].__class__ == plugin_class:
                        self.registry.unregister_component(name)
            del self.loaded_plugins[plugin_module_path]
            logger.info(f"Unloaded plugin from {plugin_module_path}")
Enter fullscreen mode Exit fullscreen mode

3. Base Plugin Interface

All plugins must inherit from a base class that defines the contract.

# base_plugin.py
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional

class BasePlugin(ABC):
    """Abstract base class for all plugins."""

    plugin_name: str = None
    plugin_version: str = "1.0.0"
    plugin_description: str = ""

    def __init__(self, config: Optional[Dict[str, Any]] = None):
        self.config = config or {}
        self.initialized = False

    @abstractmethod
    def initialize(self) -> bool:
        """Initialize the plugin. Return True if successful."""
        pass

    @abstractmethod
    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Execute the plugin's main functionality."""
        pass

    def shutdown(self):
        """Cleanup resources when plugin is unloaded."""
        self.initialized = False

    def get_info(self) -> Dict[str, Any]:
        """Return plugin metadata."""
        return {
            'name': self.plugin_name or self.__class__.__name__,
            'version': self.plugin_version,
            'description': self.plugin_description,
            'initialized': self.initialized
        }
Enter fullscreen mode Exit fullscreen mode

4. Example Plugins

Let's create some practical plugins to demonstrate the system.

# plugins/text_processor/__init__.py
from base_plugin import BasePlugin
from typing import Dict, Any

class TextProcessorPlugin(BasePlugin):
    """Plugin for text processing operations."""

    plugin_name = "Text Processor"
    plugin_version = "1.0.0"
    plugin_description = "Provides text analysis and transformation capabilities"

    def initialize(self) -> bool:
        self.initialized = True
        return True

    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        text = context.get('text', '')
        operation = context.get('operation', 'word_count')

        if operation == 'word_count':
            result = len(text.split())
        elif operation == 'char_count':
            result = len(text)
        elif operation == 'reverse':
            result = text[::-1]
        elif operation == 'uppercase':
            result = text.upper()
        else:
            result = f"Unknown operation: {operation}"

        return {'result': result, 'operation': operation, 'plugin': self.plugin_name}
Enter fullscreen mode Exit fullscreen mode

python
# plugins/data_validator/__init__.py
from base_plugin import
Enter fullscreen mode Exit fullscreen mode

Top comments (0)