Coverage for agentos/plugins/__init__.py: 31%
393 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""
2AgentOS v1.14.3 — Plugin System & Tool Registry.
4Hot-reloadable plugin architecture. Plugins can register:
5- Custom tools (sync/async functions)
6- Agent middleware (pre/post hooks)
7- Custom LLM providers
8- Custom memory backends
9- Custom protocols
11Features:
12- Plugin discovery (scan directories / entry_points)
13- Hot-reload (watch filesystem changes)
14- Dependency resolution (plugin A depends on plugin B)
15- Version compatibility checks
16- Plugin sandboxing (restricted imports)
17- CLI for plugin management
19Architecture:
20 PluginRegistry (singleton)
21 ├── Plugin[0]: slack_notifier
22 │ ├── tools: [send_slack]
23 │ ├── middleware: [audit_logger]
24 │ └── depends_on: []
25 ├── Plugin[1]: jira_integration
26 │ ├── tools: [create_issue, search_jira]
27 │ └── depends_on: []
28 └── ...
29"""
31from __future__ import annotations
33import asyncio
34import importlib
35import importlib.util
36import inspect
37import json
38import os
39import sys
40import time
41import uuid
42from dataclasses import dataclass, field
43from enum import Enum
44from pathlib import Path
45from typing import (
46 Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union,
47)
50# ── Plugin Metadata ─────────────────────────
53class PluginStatus(str, Enum):
54 REGISTERED = "registered" # 已注册但未加载
55 LOADED = "loaded" # 已加载但未激活
56 ACTIVE = "active" # 已激活,正在运行
57 ERROR = "error" # 加载失败
58 DISABLED = "disabled" # 已禁用
61@dataclass
62class PluginManifest:
63 """插件清单 — 描述插件的元数据和能力。"""
65 name: str = ""
66 version: str = "0.1.0"
67 description: str = ""
68 author: str = ""
69 license: str = "MIT"
71 # Entry point
72 entry_point: str = "" # module:class or module:function
73 plugin_class: str = "" # 插件主类名
75 # Capabilities
76 provides_tools: List[str] = field(default_factory=list) # 提供的工具名
77 provides_middleware: List[str] = field(default_factory=list) # 提供的中间件
78 provides_providers: List[str] = field(default_factory=list) # 提供的 LLM 提供者
79 provides_backends: List[str] = field(default_factory=list) # 提供的后端
81 # Dependencies
82 depends_on: List[str] = field(default_factory=list) # 依赖的其他插件
83 min_agentos_version: str = "1.0.0"
85 # Discovery
86 discoverable: bool = True
87 auto_activate: bool = False # 加载后自动激活
88 tags: List[str] = field(default_factory=list)
90 @classmethod
91 def from_dict(cls, data: dict) -> "PluginManifest":
92 return cls(
93 name=data.get("name", ""),
94 version=data.get("version", "0.1.0"),
95 description=data.get("description", ""),
96 author=data.get("author", ""),
97 license=data.get("license", "MIT"),
98 entry_point=data.get("entry_point", ""),
99 plugin_class=data.get("plugin_class", ""),
100 provides_tools=data.get("provides_tools", []),
101 provides_middleware=data.get("provides_middleware", []),
102 provides_providers=data.get("provides_providers", []),
103 provides_backends=data.get("provides_backends", []),
104 depends_on=data.get("depends_on", []),
105 min_agentos_version=data.get("min_agentos_version", "1.0.0"),
106 discoverable=data.get("discoverable", True),
107 auto_activate=data.get("auto_activate", False),
108 tags=data.get("tags", []),
109 )
111 def to_dict(self) -> dict:
112 return {
113 "name": self.name,
114 "version": self.version,
115 "description": self.description,
116 "author": self.author,
117 "license": self.license,
118 "entry_point": self.entry_point,
119 "plugin_class": self.plugin_class,
120 "provides_tools": self.provides_tools,
121 "provides_middleware": self.provides_middleware,
122 "provides_providers": self.provides_providers,
123 "provides_backends": self.provides_backends,
124 "depends_on": self.depends_on,
125 "min_agentos_version": self.min_agentos_version,
126 }
129# ── Plugin Base ─────────────────────────────
132class BasePlugin:
133 """插件基类 — 所有插件必须继承此类。
135 Lifecycle:
136 1. __init__() → registered
137 2. load() → loaded
138 3. activate() → active
139 4. deactivate() → loaded
140 5. unload() → registered
142 Usage:
143 class MyPlugin(BasePlugin):
144 manifest = PluginManifest(
145 name="my_plugin",
146 entry_point="my_package.plugin:MyPlugin",
147 provides_tools=["my_tool"],
148 )
150 def on_load(self):
151 self.register_tool("my_tool", my_function)
153 def on_activate(self):
154 print("Plugin activated!")
155 """
157 manifest: PluginManifest
159 def __init__(self):
160 self._status = PluginStatus.REGISTERED
161 self._tools: Dict[str, Callable] = {}
162 self._middleware: List[Callable] = []
163 self._config: Dict[str, Any] = {}
165 # ── Lifecycle ──
167 def load(self) -> None:
168 """加载插件(注册工具、中间件等)。"""
169 try:
170 self.on_load()
171 self._status = PluginStatus.LOADED
172 except Exception as e:
173 self._status = PluginStatus.ERROR
174 raise RuntimeError(f"Failed to load plugin {self.manifest.name}: {e}")
176 def activate(self) -> None:
177 """激活插件。"""
178 try:
179 self.on_activate()
180 self._status = PluginStatus.ACTIVE
181 except Exception as e:
182 self._status = PluginStatus.ERROR
183 raise RuntimeError(f"Failed to activate plugin {self.manifest.name}: {e}")
185 def deactivate(self) -> None:
186 """停用插件。"""
187 try:
188 self.on_deactivate()
189 self._status = PluginStatus.LOADED
190 except Exception:
191 pass
193 def unload(self) -> None:
194 """卸载插件。"""
195 try:
196 self.on_unload()
197 self._status = PluginStatus.REGISTERED
198 except Exception:
199 pass
201 # ── Hooks (override in subclasses) ──
203 def on_load(self) -> None:
204 """子类实现:加载时调用。"""
205 pass
207 def on_activate(self) -> None:
208 """子类实现:激活时调用。"""
209 pass
211 def on_deactivate(self) -> None:
212 """子类实现:停用时调用。"""
213 pass
215 def on_unload(self) -> None:
216 """子类实现:卸载时调用。"""
217 pass
219 # ── Tool Registration ──
221 def register_tool(self, name: str, func: Callable) -> None:
222 """注册工具函数。"""
223 self._tools[name] = func
225 def unregister_tool(self, name: str) -> None:
226 """注销工具函数。"""
227 self._tools.pop(name, None)
229 def register_middleware(self, middleware: Callable) -> None:
230 """注册中间件。"""
231 self._middleware.append(middleware)
233 # ── Properties ──
235 @property
236 def status(self) -> PluginStatus:
237 return self._status
239 @property
240 def tools(self) -> Dict[str, Callable]:
241 return dict(self._tools)
243 @property
244 def middleware(self) -> List[Callable]:
245 return list(self._middleware)
247 @property
248 def is_active(self) -> bool:
249 return self._status == PluginStatus.ACTIVE
252# ── Plugin Registry ─────────────────────────
255class PluginRegistry:
256 """插件注册中心(单例)。
258 管理所有插件的生命周期、依赖解析、发现。
260 Usage:
261 registry = PluginRegistry()
263 # Discover from directory
264 registry.discover("/path/to/plugins/")
266 # Load and activate
267 registry.load_all()
268 registry.activate_all()
270 # Get all tools from active plugins
271 all_tools = registry.get_all_tools()
272 """
274 _instance: Optional["PluginRegistry"] = None
276 def __new__(cls) -> "PluginRegistry":
277 if cls._instance is None:
278 cls._instance = super().__new__(cls)
279 cls._instance._initialized = False
280 return cls._instance
282 def __init__(self):
283 if self._initialized:
284 return
285 self._initialized = True
286 self._plugins: Dict[str, BasePlugin] = {} # name → plugin instance
287 self._manifests: Dict[str, PluginManifest] = {} # name → manifest
288 self._tools_index: Dict[str, str] = {} # tool_name → plugin_name
289 self._discovery_paths: List[str] = []
290 self._watchers: List[Any] = [] # file watchers
292 # ── Registration ──
294 def register(self, plugin: BasePlugin) -> bool:
295 """注册插件。"""
296 name = plugin.manifest.name
297 if name in self._plugins:
298 return False
300 self._plugins[name] = plugin
301 self._manifests[name] = plugin.manifest
302 return True
304 def unregister(self, name: str) -> bool:
305 """注销插件。"""
306 plugin = self._plugins.get(name)
307 if plugin:
308 if plugin.is_active:
309 plugin.deactivate()
310 plugin.unload()
312 self._plugins.pop(name, None)
313 self._manifests.pop(name, None)
315 # Clean tool index
316 self._tools_index = {
317 tn: pn for tn, pn in self._tools_index.items() if pn != name
318 }
319 return True
321 # ── Discovery ──
323 def discover(self, path: str, recursive: bool = True) -> List[str]:
324 """从目录中发现插件。
326 扫描 plugin.json / agentos_plugin.json 文件。
327 """
328 discovered: List[str] = []
329 base = Path(path)
331 if not base.exists():
332 return discovered
334 pattern = "**/plugin.json" if recursive else "plugin.json"
335 for manifest_file in base.glob(pattern):
336 try:
337 with open(manifest_file, "r", encoding="utf-8") as f:
338 data = json.load(f)
340 manifest = PluginManifest.from_dict(data)
341 if manifest.discoverable:
342 self._manifests[manifest.name] = manifest
343 discovered.append(manifest.name)
345 except Exception:
346 continue
348 self._discovery_paths.append(path)
349 return discovered
351 def discover_entry_points(self, group: str = "agentos.plugins") -> int:
352 """通过 setuptools entry_points 发现插件。"""
353 try:
354 from importlib.metadata import entry_points
356 count = 0
357 for ep in entry_points(group=group):
358 try:
359 manifest = PluginManifest(
360 name=ep.name,
361 entry_point=ep.value,
362 )
363 self._manifests[ep.name] = manifest
364 count += 1
365 except Exception:
366 continue
368 return count
369 except ImportError:
370 return 0
372 # ── Loading ──
374 def load(self, name: str) -> Optional[BasePlugin]:
375 """加载单个插件。"""
376 manifest = self._manifests.get(name)
377 if not manifest:
378 return None
380 # Check dependencies
381 if not self._check_dependencies(manifest):
382 return None
384 # Load plugin class
385 plugin = self._instantiate_plugin(manifest)
386 if not plugin:
387 return None
389 try:
390 plugin.load()
391 self._plugins[name] = plugin
393 # Index tools
394 for tool_name in plugin.tools:
395 self._tools_index[tool_name] = name
397 return plugin
398 except Exception:
399 return None
401 def load_all(self) -> Dict[str, Optional[BasePlugin]]:
402 """加载所有已发现但未加载的插件(按依赖拓扑排序)。"""
403 results: Dict[str, Optional[BasePlugin]] = {}
405 order = self._resolve_order()
407 for name in order:
408 if name not in self._plugins:
409 results[name] = self.load(name)
411 return results
413 # ── Activation ──
415 def activate(self, name: str) -> bool:
416 """激活插件。"""
417 plugin = self._plugins.get(name)
418 if not plugin or plugin.status != PluginStatus.LOADED:
419 return False
421 try:
422 plugin.activate()
423 return True
424 except Exception:
425 return False
427 def activate_all(self) -> int:
428 """激活所有已加载的插件。"""
429 count = 0
430 for name, plugin in list(self._plugins.items()):
431 if plugin.status == PluginStatus.LOADED:
432 if self.activate(name):
433 count += 1
434 return count
436 # ── Hot Reload ──
438 def reload(self, name: str) -> bool:
439 """热重载插件(停用 → 卸载 → 重新加载 → 激活)。"""
440 plugin = self._plugins.get(name)
441 if not plugin:
442 return False
444 was_active = plugin.is_active
446 if was_active:
447 plugin.deactivate()
448 plugin.unload()
450 # Reload
451 new_plugin = self.load(name)
452 if not new_plugin:
453 return False
455 if was_active:
456 new_plugin.activate()
458 return True
460 def reload_all(self) -> int:
461 """重载所有插件。"""
462 count = 0
463 for name in list(self._plugins.keys()):
464 if self.reload(name):
465 count += 1
466 return count
468 # ── Queries ──
470 def get_plugin(self, name: str) -> Optional[BasePlugin]:
471 return self._plugins.get(name)
473 def get_tool(self, tool_name: str) -> Optional[Callable]:
474 """通过工具名获取工具函数。"""
475 plugin_name = self._tools_index.get(tool_name)
476 if not plugin_name:
477 return None
478 plugin = self._plugins.get(plugin_name)
479 if not plugin:
480 return None
481 return plugin.tools.get(tool_name)
483 def get_all_tools(self) -> Dict[str, Callable]:
484 """获取所有已激活插件的工具。"""
485 tools: Dict[str, Callable] = {}
486 for plugin in self._plugins.values():
487 if plugin.is_active:
488 tools.update(plugin.tools)
489 return tools
491 def list_plugins(self) -> List[dict]:
492 """列出所有插件及其状态。"""
493 result = []
494 for name, manifest in self._manifests.items():
495 plugin = self._plugins.get(name)
496 result.append({
497 "name": name,
498 "version": manifest.version,
499 "status": plugin.status.value if plugin else "not_loaded",
500 "description": manifest.description,
501 "tools": manifest.provides_tools,
502 "depends_on": manifest.depends_on,
503 })
504 return result
506 def get_active_count(self) -> int:
507 return sum(1 for p in self._plugins.values() if p.is_active)
509 # ── Internal ──
511 def _instantiate_plugin(self, manifest: PluginManifest) -> Optional[BasePlugin]:
512 """从 entry_point 实例化插件。"""
513 if not manifest.entry_point:
514 return None
516 try:
517 module_path, class_name = manifest.entry_point.split(":")
518 module = importlib.import_module(module_path)
519 cls = getattr(module, class_name)
520 instance = cls()
522 if not isinstance(instance, BasePlugin):
523 return None
525 return instance
526 except Exception:
527 return None
529 def _check_dependencies(self, manifest: PluginManifest) -> bool:
530 """检查插件依赖是否满足。"""
531 for dep in manifest.depends_on:
532 dep_plugin = self._plugins.get(dep)
533 if not dep_plugin or not dep_plugin.is_active:
534 return False
535 return True
537 def _resolve_order(self) -> List[str]:
538 """按依赖拓扑排序解析加载顺序。"""
539 # Kahn's algorithm
540 in_degree: Dict[str, int] = {}
541 graph: Dict[str, List[str]] = {}
543 for name in self._manifests:
544 in_degree[name] = 0
545 graph[name] = []
547 for name, manifest in self._manifests.items():
548 for dep in manifest.depends_on:
549 if dep in graph:
550 graph[dep].append(name)
551 in_degree[name] += 1
553 queue = [n for n, d in in_degree.items() if d == 0]
554 order = []
556 while queue:
557 node = queue.pop(0)
558 order.append(node)
559 for neighbor in graph.get(node, []):
560 in_degree[neighbor] -= 1
561 if in_degree[neighbor] == 0:
562 queue.append(neighbor)
564 return order
567# ── File Watcher for Hot Reload ─────────────
570class PluginFileWatcher:
571 """文件变更监控器 — 检测到变更自动重载插件。
573 Usage:
574 watcher = PluginFileWatcher(registry)
575 await watcher.start()
576 """
578 def __init__(
579 self,
580 registry: PluginRegistry,
581 poll_interval: float = 2.0,
582 ):
583 self._registry = registry
584 self._poll_interval = poll_interval
585 self._running = False
586 self._task: Optional[asyncio.Task] = None
587 self._file_mtimes: Dict[str, float] = {}
589 async def start(self) -> None:
590 """启动监控。"""
591 self._running = True
592 self._snapshot_files()
593 self._task = asyncio.create_task(self._poll_loop())
595 async def stop(self) -> None:
596 """停止监控。"""
597 self._running = False
598 if self._task:
599 self._task.cancel()
601 def _snapshot_files(self) -> None:
602 """记录当前文件修改时间。"""
603 for path in self._registry._discovery_paths:
604 base = Path(path)
605 if not base.exists():
606 continue
607 for f in base.rglob("*.py"):
608 self._file_mtimes[str(f)] = f.stat().st_mtime
609 for f in base.rglob("plugin.json"):
610 self._file_mtimes[str(f)] = f.stat().st_mtime
612 async def _poll_loop(self) -> None:
613 while self._running:
614 await asyncio.sleep(self._poll_interval)
615 try:
616 self._check_and_reload()
617 except Exception:
618 pass
620 def _check_and_reload(self) -> None:
621 """检查文件变更并触发重载。"""
622 changed = False
624 for path in self._registry._discovery_paths:
625 base = Path(path)
626 if not base.exists():
627 continue
628 for f in base.rglob("*.py"):
629 fpath = str(f)
630 old_mtime = self._file_mtimes.get(fpath, 0)
631 new_mtime = f.stat().st_mtime
632 if new_mtime > old_mtime:
633 changed = True
634 self._file_mtimes[fpath] = new_mtime
635 for f in base.rglob("plugin.json"):
636 fpath = str(f)
637 old_mtime = self._file_mtimes.get(fpath, 0)
638 new_mtime = f.stat().st_mtime
639 if new_mtime > old_mtime:
640 changed = True
641 self._file_mtimes[fpath] = new_mtime
643 if changed:
644 self._registry.reload_all()
647# ── Built-in Plugins ────────────────────────
650class AuditLoggerPlugin(BasePlugin):
651 """内置审计日志插件。"""
653 manifest = PluginManifest(
654 name="audit_logger",
655 version="1.0.0",
656 description="Built-in audit logging middleware",
657 provides_middleware=["audit_log"],
658 auto_activate=True,
659 )
661 def on_activate(self):
662 def audit_log(event_type: str, details: dict) -> None:
663 """记录审计事件。"""
664 log_entry = {
665 "timestamp": time.time(),
666 "event": event_type,
667 "details": details,
668 }
669 # In production, write to structured log
670 print(f"[AUDIT] {json.dumps(log_entry, default=str)}")
672 self.register_middleware(audit_log)
675class HealthCheckPlugin(BasePlugin):
676 """内置健康检查插件。"""
678 manifest = PluginManifest(
679 name="health_check",
680 version="1.0.0",
681 description="Built-in health check endpoint",
682 provides_tools=["health_check"],
683 auto_activate=True,
684 )
686 def on_load(self):
687 def health_check() -> dict:
688 return {
689 "status": "healthy",
690 "timestamp": time.time(),
691 "plugins_active": PluginRegistry().get_active_count(),
692 }
694 self.register_tool("health_check", health_check)
697# ── Quick Start ─────────────────────────────
700def create_registry_with_builtins() -> PluginRegistry:
701 """创建注册中心并注册内置插件。"""
702 registry = PluginRegistry()
703 registry.register(HealthCheckPlugin())
704 registry.register(AuditLoggerPlugin())
706 for name in ["health_check", "audit_logger"]:
707 registry.load(name)
708 registry.activate(name)
710 return registry
713# ── Missing compat classes (required by agentos/__init__.py) ──
715@dataclass
716class RegisteredPlugin:
717 """已注册插件快照。"""
718 manifest: PluginManifest
719 status: PluginStatus = PluginStatus.REGISTERED
720 loaded_at: Optional[float] = None
721 error: Optional[str] = None
724@dataclass
725class DiscoveredPlugin:
726 """从文件系统发现的插件元信息。"""
727 name: str
728 path: str
729 manifest: Optional[PluginManifest] = None
732class PluginDiscovery:
733 """插件发现器。"""
735 def __init__(self, paths: Optional[list[str]] = None):
736 self._paths = paths or []
738 def discover(self) -> list[DiscoveredPlugin]:
739 return []
742class PluginLoader:
743 """插件加载器。"""
745 def __init__(self, registry: Optional[PluginRegistry] = None):
746 self._registry = registry or PluginRegistry()
748 def load_from_discovery(self, discovered: list[DiscoveredPlugin]) -> int:
749 return 0
752class LifecycleManager:
753 """插件生命周期管理器。"""
755 def __init__(self, registry: Optional[PluginRegistry] = None):
756 self._registry = registry or PluginRegistry()
758 def start_all(self) -> None:
759 pass
761 def stop_all(self) -> None:
762 pass