Skip to content

Commit 76e5d9e

Browse files
committed
feat(plugins): implement core plugin system with dependency management
- Added PluginManager with dynamic loading/unloading capabilities - Implemented basic dependency management and version checking - Added plugin metadata and lifecycle management - Created example plugin implementation - Added comprehensive error handling for plugin operations Addresses the following roadmap items: - [x] Plugin architecture - [x] Basic plugin structure - [x] Dynamic loading/unloading - [x] Dependency management (basic) - [x] Version compatibility (basic) - [ ] Plugin isolation (future work)
1 parent 9cacc14 commit 76e5d9e

10 files changed

Lines changed: 608 additions & 195 deletions

File tree

agentspring/plugin/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
This module provides a flexible plugin system for AgentSpring, allowing for dynamic
3+
loading and management of plugins with dependency resolution, version checking,
4+
and resource management.
5+
"""
6+
7+
from .base import BasePlugin, PluginMetadata, PluginState
8+
from .manager import PluginManager
9+
from .exceptions import (
10+
PluginError,
11+
PluginLoadError,
12+
PluginDependencyError,
13+
PluginConflictError,
14+
PluginVersionError,
15+
)
16+
17+
__version__ = "0.1.0"
18+
19+
__all__ = [
20+
# Core classes
21+
'BasePlugin',
22+
'PluginManager',
23+
'PluginMetadata',
24+
'PluginState',
25+
26+
# Exceptions
27+
'PluginError',
28+
'PluginLoadError',
29+
'PluginDependencyError',
30+
'PluginConflictError',
31+
'PluginVersionError',
32+
]
33+
34+
# Set default logging handler to avoid "No handler found" warnings
35+
import logging
36+
logging.getLogger('agentspring.plugin').addHandler(logging.NullHandler())

agentspring/plugin/base.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from dataclasses import dataclass, field
2+
from enum import Enum, auto
3+
from typing import Any, Dict, List, Optional, Type, TypeVar, Generic, Callable
4+
from pathlib import Path
5+
import importlib
6+
import inspect
7+
import logging
8+
9+
logger = logging.getLogger(__name__)
10+
11+
class PluginState(Enum):
12+
"""Represents the state of a plugin."""
13+
UNLOADED = auto()
14+
LOADED = auto()
15+
ENABLED = auto()
16+
DISABLED = auto()
17+
ERROR = auto()
18+
19+
@dataclass
20+
class PluginMetadata:
21+
"""Metadata for a plugin."""
22+
name: str
23+
version: str
24+
description: str = ""
25+
author: str = ""
26+
license: str = "MIT"
27+
requires: List[str] = field(default_factory=list) # List of required plugin names
28+
conflicts: List[str] = field(default_factory=list) # List of conflicting plugin names
29+
python_requires: str = ">=3.9" # Python version requirement
30+
dependencies: Dict[str, str] = field(default_factory=dict) # Package dependencies
31+
32+
@classmethod
33+
def from_dict(cls, data: Dict[str, Any]) -> 'PluginMetadata':
34+
"""Create PluginMetadata from a dictionary."""
35+
return cls(**{
36+
k: v for k, v in data.items()
37+
if k in inspect.signature(cls).parameters
38+
})
39+
40+
class BasePlugin:
41+
"""Base class for all plugins."""
42+
43+
def __init__(self):
44+
self._state = PluginState.UNLOADED
45+
self._metadata: Optional[PluginMetadata] = None
46+
self._module = None
47+
self._path: Optional[Path] = None
48+
self._resources = {}
49+
50+
@property
51+
def state(self) -> PluginState:
52+
"""Get the current state of the plugin."""
53+
return self._state
54+
55+
@property
56+
def metadata(self) -> PluginMetadata:
57+
"""Get the plugin metadata."""
58+
if self._metadata is None:
59+
raise ValueError("Plugin metadata not loaded")
60+
return self._metadata
61+
62+
@property
63+
def name(self) -> str:
64+
"""Get the plugin name."""
65+
return self.metadata.name
66+
67+
async def load(self) -> None:
68+
"""Load the plugin."""
69+
if self._state != PluginState.UNLOADED:
70+
raise RuntimeError(f"Plugin {self.name} is already loaded")
71+
72+
self._state = PluginState.LOADED
73+
logger.info(f"Loaded plugin: {self.name}")
74+
75+
async def unload(self) -> None:
76+
"""Unload the plugin and clean up resources."""
77+
if self._state == PluginState.UNLOADED:
78+
return
79+
80+
if hasattr(self, 'on_disable'):
81+
await self.on_disable()
82+
83+
# Clean up resources
84+
for resource in self._resources.values():
85+
if hasattr(resource, 'close'):
86+
if inspect.iscoroutinefunction(resource.close):
87+
await resource.close()
88+
else:
89+
resource.close()
90+
91+
self._resources.clear()
92+
self._state = PluginState.UNLOADED
93+
logger.info(f"Unloaded plugin: {self.name}")
94+
95+
async def enable(self) -> None:
96+
"""Enable the plugin."""
97+
if self._state != PluginState.LOADED:
98+
raise RuntimeError(f"Cannot enable plugin {self.name} in state {self._state}")
99+
100+
if hasattr(self, 'on_enable'):
101+
await self.on_enable()
102+
103+
self._state = PluginState.ENABLED
104+
logger.info(f"Enabled plugin: {self.name}")
105+
106+
async def disable(self) -> None:
107+
"""Disable the plugin."""
108+
if self._state != PluginState.ENABLED:
109+
return
110+
111+
if hasattr(self, 'on_disable'):
112+
await self.on_disable()
113+
114+
self._state = PluginState.DISABLED
115+
logger.info(f"Disabled plugin: {self.name}")
116+
117+
def register_resource(self, name: str, resource: Any) -> None:
118+
"""Register a resource that needs cleanup."""
119+
self._resources[name] = resource
120+
121+
def get_resource(self, name: str) -> Any:
122+
"""Get a registered resource."""
123+
return self._resources.get(name)
124+
125+
# Lifecycle hooks (to be overridden by plugins)
126+
async def on_load(self) -> None:
127+
"""Called when the plugin is loaded."""
128+
pass
129+
130+
async def on_enable(self) -> None:
131+
"""Called when the plugin is enabled."""
132+
pass
133+
134+
async def on_disable(self) -> None:
135+
"""Called when the plugin is disabled."""
136+
pass
137+
138+
async def on_unload(self) -> None:
139+
"""Called when the plugin is unloaded."""
140+
pass

agentspring/plugin/exceptions.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Custom exceptions for the AgentSpring plugin system.
3+
"""
4+
5+
class PluginError(Exception):
6+
"""Base class for all plugin-related exceptions."""
7+
pass
8+
9+
class PluginLoadError(PluginError):
10+
"""Raised when a plugin fails to load."""
11+
pass
12+
13+
class PluginDependencyError(PluginError):
14+
"""Raised when a plugin has unresolved dependencies."""
15+
pass
16+
17+
class PluginConflictError(PluginError):
18+
"""Raised when there is a conflict between plugins."""
19+
pass
20+
21+
class PluginVersionError(PluginError):
22+
"""Raised when there is a version incompatibility."""
23+
pass
24+
25+
__all__ = [
26+
'PluginError',
27+
'PluginLoadError',
28+
'PluginDependencyError',
29+
'PluginConflictError',
30+
'PluginVersionError',
31+
]

0 commit comments

Comments
 (0)