Coverage for agentos/plugins/__init__.py: 30%
386 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +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 json
37import time
38from collections.abc import Callable
39from dataclasses import dataclass, field
40from enum import StrEnum
41from pathlib import Path
42from typing import (
43 Any,
44)
46# ── Plugin Metadata ─────────────────────────
49class PluginStatus(StrEnum):
50 REGISTERED = "registered" # 已注册但未加载
51 LOADED = "loaded" # 已加载但未激活
52 ACTIVE = "active" # 已激活,正在运行
53 ERROR = "error" # 加载失败
54 DISABLED = "disabled" # 已禁用
57@dataclass
58class PluginManifest:
59 """插件清单 — 描述插件的元数据和能力。"""
61 name: str = ""
62 version: str = "0.1.0"
63 description: str = ""
64 author: str = ""
65 license: str = "MIT"
67 # Entry point
68 entry_point: str = "" # module:class or module:function
69 plugin_class: str = "" # 插件主类名
71 # Capabilities
72 provides_tools: list[str] = field(default_factory=list) # 提供的工具名
73 provides_middleware: list[str] = field(default_factory=list) # 提供的中间件
74 provides_providers: list[str] = field(default_factory=list) # 提供的 LLM 提供者
75 provides_backends: list[str] = field(default_factory=list) # 提供的后端
77 # Dependencies
78 depends_on: list[str] = field(default_factory=list) # 依赖的其他插件
79 min_agentos_version: str = "1.0.0"
81 # Discovery
82 discoverable: bool = True
83 auto_activate: bool = False # 加载后自动激活
84 tags: list[str] = field(default_factory=list)
86 @classmethod
87 def from_dict(cls, data: dict) -> PluginManifest:
88 return cls(
89 name=data.get("name", ""),
90 version=data.get("version", "0.1.0"),
91 description=data.get("description", ""),
92 author=data.get("author", ""),
93 license=data.get("license", "MIT"),
94 entry_point=data.get("entry_point", ""),
95 plugin_class=data.get("plugin_class", ""),
96 provides_tools=data.get("provides_tools", []),
97 provides_middleware=data.get("provides_middleware", []),
98 provides_providers=data.get("provides_providers", []),
99 provides_backends=data.get("provides_backends", []),
100 depends_on=data.get("depends_on", []),
101 min_agentos_version=data.get("min_agentos_version", "1.0.0"),
102 discoverable=data.get("discoverable", True),
103 auto_activate=data.get("auto_activate", False),
104 tags=data.get("tags", []),
105 )
107 def to_dict(self) -> dict:
108 return {
109 "name": self.name,
110 "version": self.version,
111 "description": self.description,
112 "author": self.author,
113 "license": self.license,
114 "entry_point": self.entry_point,
115 "plugin_class": self.plugin_class,
116 "provides_tools": self.provides_tools,
117 "provides_middleware": self.provides_middleware,
118 "provides_providers": self.provides_providers,
119 "provides_backends": self.provides_backends,
120 "depends_on": self.depends_on,
121 "min_agentos_version": self.min_agentos_version,
122 }
125# ── Plugin Base ─────────────────────────────
128class BasePlugin:
129 """插件基类 — 所有插件必须继承此类。
131 Lifecycle:
132 1. __init__() → registered
133 2. load() → loaded
134 3. activate() → active
135 4. deactivate() → loaded
136 5. unload() → registered
138 Usage:
139 class MyPlugin(BasePlugin):
140 manifest = PluginManifest(
141 name="my_plugin",
142 entry_point="my_package.plugin:MyPlugin",
143 provides_tools=["my_tool"],
144 )
146 def on_load(self):
147 self.register_tool("my_tool", my_function)
149 def on_activate(self):
150 print("Plugin activated!")
151 """
153 manifest: PluginManifest
155 def __init__(self):
156 self._status = PluginStatus.REGISTERED
157 self._tools: dict[str, Callable] = {}
158 self._middleware: list[Callable] = []
159 self._config: dict[str, Any] = {}
161 # ── Lifecycle ──
163 def load(self) -> None:
164 """加载插件(注册工具、中间件等)。"""
165 try:
166 self.on_load()
167 self._status = PluginStatus.LOADED
168 except Exception as e:
169 self._status = PluginStatus.ERROR
170 raise RuntimeError(f"Failed to load plugin {self.manifest.name}: {e}")
172 def activate(self) -> None:
173 """激活插件。"""
174 try:
175 self.on_activate()
176 self._status = PluginStatus.ACTIVE
177 except Exception as e:
178 self._status = PluginStatus.ERROR
179 raise RuntimeError(f"Failed to activate plugin {self.manifest.name}: {e}")
181 def deactivate(self) -> None:
182 """停用插件。"""
183 try:
184 self.on_deactivate()
185 self._status = PluginStatus.LOADED
186 except Exception:
187 pass
189 def unload(self) -> None:
190 """卸载插件。"""
191 try:
192 self.on_unload()
193 self._status = PluginStatus.REGISTERED
194 except Exception:
195 pass
197 # ── Hooks (override in subclasses) ──
199 def on_load(self) -> None:
200 """子类实现:加载时调用。"""
202 def on_activate(self) -> None:
203 """子类实现:激活时调用。"""
205 def on_deactivate(self) -> None:
206 """子类实现:停用时调用。"""
208 def on_unload(self) -> None:
209 """子类实现:卸载时调用。"""
211 # ── Tool Registration ──
213 def register_tool(self, name: str, func: Callable) -> None:
214 """注册工具函数。"""
215 self._tools[name] = func
217 def unregister_tool(self, name: str) -> None:
218 """注销工具函数。"""
219 self._tools.pop(name, None)
221 def register_middleware(self, middleware: Callable) -> None:
222 """注册中间件。"""
223 self._middleware.append(middleware)
225 # ── Properties ──
227 @property
228 def status(self) -> PluginStatus:
229 return self._status
231 @property
232 def tools(self) -> dict[str, Callable]:
233 return dict(self._tools)
235 @property
236 def middleware(self) -> list[Callable]:
237 return list(self._middleware)
239 @property
240 def is_active(self) -> bool:
241 return self._status == PluginStatus.ACTIVE
244# ── Plugin Registry ─────────────────────────
247class PluginRegistry:
248 """插件注册中心(单例)。
250 管理所有插件的生命周期、依赖解析、发现。
252 Usage:
253 registry = PluginRegistry()
255 # Discover from directory
256 registry.discover("/path/to/plugins/")
258 # Load and activate
259 registry.load_all()
260 registry.activate_all()
262 # Get all tools from active plugins
263 all_tools = registry.get_all_tools()
264 """
266 _instance: PluginRegistry | None = None
268 def __new__(cls) -> PluginRegistry:
269 if cls._instance is None:
270 cls._instance = super().__new__(cls)
271 cls._instance._initialized = False
272 return cls._instance
274 def __init__(self):
275 if self._initialized:
276 return
277 self._initialized = True
278 self._plugins: dict[str, BasePlugin] = {} # name → plugin instance
279 self._manifests: dict[str, PluginManifest] = {} # name → manifest
280 self._tools_index: dict[str, str] = {} # tool_name → plugin_name
281 self._discovery_paths: list[str] = []
282 self._watchers: list[Any] = [] # file watchers
284 # ── Registration ──
286 def register(self, plugin: BasePlugin) -> bool:
287 """注册插件。"""
288 name = plugin.manifest.name
289 if name in self._plugins:
290 return False
292 self._plugins[name] = plugin
293 self._manifests[name] = plugin.manifest
294 return True
296 def unregister(self, name: str) -> bool:
297 """注销插件。"""
298 plugin = self._plugins.get(name)
299 if plugin:
300 if plugin.is_active:
301 plugin.deactivate()
302 plugin.unload()
304 self._plugins.pop(name, None)
305 self._manifests.pop(name, None)
307 # Clean tool index
308 self._tools_index = {tn: pn for tn, pn in self._tools_index.items() if pn != name}
309 return True
311 # ── Discovery ──
313 def discover(self, path: str, recursive: bool = True) -> list[str]:
314 """从目录中发现插件。
316 扫描 plugin.json / agentos_plugin.json 文件。
317 """
318 discovered: list[str] = []
319 base = Path(path)
321 if not base.exists():
322 return discovered
324 pattern = "**/plugin.json" if recursive else "plugin.json"
325 for manifest_file in base.glob(pattern):
326 try:
327 with open(manifest_file, encoding="utf-8") as f:
328 data = json.load(f)
330 manifest = PluginManifest.from_dict(data)
331 if manifest.discoverable:
332 self._manifests[manifest.name] = manifest
333 discovered.append(manifest.name)
335 except Exception:
336 continue
338 self._discovery_paths.append(path)
339 return discovered
341 def discover_entry_points(self, group: str = "agentos.plugins") -> int:
342 """通过 setuptools entry_points 发现插件。"""
343 try:
344 from importlib.metadata import entry_points
346 count = 0
347 for ep in entry_points(group=group):
348 try:
349 manifest = PluginManifest(
350 name=ep.name,
351 entry_point=ep.value,
352 )
353 self._manifests[ep.name] = manifest
354 count += 1
355 except Exception:
356 continue
358 return count
359 except ImportError:
360 return 0
362 # ── Loading ──
364 def load(self, name: str) -> BasePlugin | None:
365 """加载单个插件。"""
366 manifest = self._manifests.get(name)
367 if not manifest:
368 return None
370 # Check dependencies
371 if not self._check_dependencies(manifest):
372 return None
374 # Load plugin class
375 plugin = self._instantiate_plugin(manifest)
376 if not plugin:
377 return None
379 try:
380 plugin.load()
381 self._plugins[name] = plugin
383 # Index tools
384 for tool_name in plugin.tools:
385 self._tools_index[tool_name] = name
387 return plugin
388 except Exception:
389 return None
391 def load_all(self) -> dict[str, BasePlugin | None]:
392 """加载所有已发现但未加载的插件(按依赖拓扑排序)。"""
393 results: dict[str, BasePlugin | None] = {}
395 order = self._resolve_order()
397 for name in order:
398 if name not in self._plugins:
399 results[name] = self.load(name)
401 return results
403 # ── Activation ──
405 def activate(self, name: str) -> bool:
406 """激活插件。"""
407 plugin = self._plugins.get(name)
408 if not plugin or plugin.status != PluginStatus.LOADED:
409 return False
411 try:
412 plugin.activate()
413 return True
414 except Exception:
415 return False
417 def activate_all(self) -> int:
418 """激活所有已加载的插件。"""
419 count = 0
420 for name, plugin in list(self._plugins.items()):
421 if plugin.status == PluginStatus.LOADED:
422 if self.activate(name):
423 count += 1
424 return count
426 # ── Hot Reload ──
428 def reload(self, name: str) -> bool:
429 """热重载插件(停用 → 卸载 → 重新加载 → 激活)。"""
430 plugin = self._plugins.get(name)
431 if not plugin:
432 return False
434 was_active = plugin.is_active
436 if was_active:
437 plugin.deactivate()
438 plugin.unload()
440 # Reload
441 new_plugin = self.load(name)
442 if not new_plugin:
443 return False
445 if was_active:
446 new_plugin.activate()
448 return True
450 def reload_all(self) -> int:
451 """重载所有插件。"""
452 count = 0
453 for name in list(self._plugins.keys()):
454 if self.reload(name):
455 count += 1
456 return count
458 # ── Queries ──
460 def get_plugin(self, name: str) -> BasePlugin | None:
461 return self._plugins.get(name)
463 def get_tool(self, tool_name: str) -> Callable | None:
464 """通过工具名获取工具函数。"""
465 plugin_name = self._tools_index.get(tool_name)
466 if not plugin_name:
467 return None
468 plugin = self._plugins.get(plugin_name)
469 if not plugin:
470 return None
471 return plugin.tools.get(tool_name)
473 def get_all_tools(self) -> dict[str, Callable]:
474 """获取所有已激活插件的工具。"""
475 tools: dict[str, Callable] = {}
476 for plugin in self._plugins.values():
477 if plugin.is_active:
478 tools.update(plugin.tools)
479 return tools
481 def list_plugins(self) -> list[dict]:
482 """列出所有插件及其状态。"""
483 result = []
484 for name, manifest in self._manifests.items():
485 plugin = self._plugins.get(name)
486 result.append(
487 {
488 "name": name,
489 "version": manifest.version,
490 "status": plugin.status.value if plugin else "not_loaded",
491 "description": manifest.description,
492 "tools": manifest.provides_tools,
493 "depends_on": manifest.depends_on,
494 }
495 )
496 return result
498 def get_active_count(self) -> int:
499 return sum(1 for p in self._plugins.values() if p.is_active)
501 # ── Internal ──
503 def _instantiate_plugin(self, manifest: PluginManifest) -> BasePlugin | None:
504 """从 entry_point 实例化插件。"""
505 if not manifest.entry_point:
506 return None
508 try:
509 module_path, class_name = manifest.entry_point.split(":")
510 module = importlib.import_module(module_path)
511 cls = getattr(module, class_name)
512 instance = cls()
514 if not isinstance(instance, BasePlugin):
515 return None
517 return instance
518 except Exception:
519 return None
521 def _check_dependencies(self, manifest: PluginManifest) -> bool:
522 """检查插件依赖是否满足。"""
523 for dep in manifest.depends_on:
524 dep_plugin = self._plugins.get(dep)
525 if not dep_plugin or not dep_plugin.is_active:
526 return False
527 return True
529 def _resolve_order(self) -> list[str]:
530 """按依赖拓扑排序解析加载顺序。"""
531 # Kahn's algorithm
532 in_degree: dict[str, int] = {}
533 graph: dict[str, list[str]] = {}
535 for name in self._manifests:
536 in_degree[name] = 0
537 graph[name] = []
539 for name, manifest in self._manifests.items():
540 for dep in manifest.depends_on:
541 if dep in graph:
542 graph[dep].append(name)
543 in_degree[name] += 1
545 queue = [n for n, d in in_degree.items() if d == 0]
546 order = []
548 while queue:
549 node = queue.pop(0)
550 order.append(node)
551 for neighbor in graph.get(node, []):
552 in_degree[neighbor] -= 1
553 if in_degree[neighbor] == 0:
554 queue.append(neighbor)
556 return order
559# ── File Watcher for Hot Reload ─────────────
562class PluginFileWatcher:
563 """文件变更监控器 — 检测到变更自动重载插件。
565 Usage:
566 watcher = PluginFileWatcher(registry)
567 await watcher.start()
568 """
570 def __init__(
571 self,
572 registry: PluginRegistry,
573 poll_interval: float = 2.0,
574 ):
575 self._registry = registry
576 self._poll_interval = poll_interval
577 self._running = False
578 self._task: asyncio.Task | None = None
579 self._file_mtimes: dict[str, float] = {}
581 async def start(self) -> None:
582 """启动监控。"""
583 self._running = True
584 self._snapshot_files()
585 self._task = asyncio.create_task(self._poll_loop())
587 async def stop(self) -> None:
588 """停止监控。"""
589 self._running = False
590 if self._task:
591 self._task.cancel()
593 def _snapshot_files(self) -> None:
594 """记录当前文件修改时间。"""
595 for path in self._registry._discovery_paths:
596 base = Path(path)
597 if not base.exists():
598 continue
599 for f in base.rglob("*.py"):
600 self._file_mtimes[str(f)] = f.stat().st_mtime
601 for f in base.rglob("plugin.json"):
602 self._file_mtimes[str(f)] = f.stat().st_mtime
604 async def _poll_loop(self) -> None:
605 while self._running:
606 await asyncio.sleep(self._poll_interval)
607 try:
608 self._check_and_reload()
609 except Exception:
610 pass
612 def _check_and_reload(self) -> None:
613 """检查文件变更并触发重载。"""
614 changed = False
616 for path in self._registry._discovery_paths:
617 base = Path(path)
618 if not base.exists():
619 continue
620 for f in base.rglob("*.py"):
621 fpath = str(f)
622 old_mtime = self._file_mtimes.get(fpath, 0)
623 new_mtime = f.stat().st_mtime
624 if new_mtime > old_mtime:
625 changed = True
626 self._file_mtimes[fpath] = new_mtime
627 for f in base.rglob("plugin.json"):
628 fpath = str(f)
629 old_mtime = self._file_mtimes.get(fpath, 0)
630 new_mtime = f.stat().st_mtime
631 if new_mtime > old_mtime:
632 changed = True
633 self._file_mtimes[fpath] = new_mtime
635 if changed:
636 self._registry.reload_all()
639# ── Built-in Plugins ────────────────────────
642class AuditLoggerPlugin(BasePlugin):
643 """内置审计日志插件。"""
645 manifest = PluginManifest(
646 name="audit_logger",
647 version="1.0.0",
648 description="Built-in audit logging middleware",
649 provides_middleware=["audit_log"],
650 auto_activate=True,
651 )
653 def on_activate(self):
654 def audit_log(event_type: str, details: dict) -> None:
655 """记录审计事件。"""
656 log_entry = {
657 "timestamp": time.time(),
658 "event": event_type,
659 "details": details,
660 }
661 # In production, write to structured log
662 print(f"[AUDIT] {json.dumps(log_entry, default=str)}")
664 self.register_middleware(audit_log)
667class HealthCheckPlugin(BasePlugin):
668 """内置健康检查插件。"""
670 manifest = PluginManifest(
671 name="health_check",
672 version="1.0.0",
673 description="Built-in health check endpoint",
674 provides_tools=["health_check"],
675 auto_activate=True,
676 )
678 def on_load(self):
679 def health_check() -> dict:
680 return {
681 "status": "healthy",
682 "timestamp": time.time(),
683 "plugins_active": PluginRegistry().get_active_count(),
684 }
686 self.register_tool("health_check", health_check)
689# ── Quick Start ─────────────────────────────
692def create_registry_with_builtins() -> PluginRegistry:
693 """创建注册中心并注册内置插件。"""
694 registry = PluginRegistry()
695 registry.register(HealthCheckPlugin())
696 registry.register(AuditLoggerPlugin())
698 for name in ["health_check", "audit_logger"]:
699 registry.load(name)
700 registry.activate(name)
702 return registry
705# ── Missing compat classes (required by agentos/__init__.py) ──
708@dataclass
709class RegisteredPlugin:
710 """已注册插件快照。"""
712 manifest: PluginManifest
713 status: PluginStatus = PluginStatus.REGISTERED
714 loaded_at: float | None = None
715 error: str | None = None
718@dataclass
719class DiscoveredPlugin:
720 """从文件系统发现的插件元信息。"""
722 name: str
723 path: str
724 manifest: PluginManifest | None = None
727class PluginDiscovery:
728 """插件发现器。"""
730 def __init__(self, paths: list[str] | None = None):
731 self._paths = paths or []
733 def discover(self) -> list[DiscoveredPlugin]:
734 return []
737class PluginLoader:
738 """插件加载器。"""
740 def __init__(self, registry: PluginRegistry | None = None):
741 self._registry = registry or PluginRegistry()
743 def load_from_discovery(self, discovered: list[DiscoveredPlugin]) -> int:
744 return 0
747class LifecycleManager:
748 """插件生命周期管理器。"""
750 def __init__(self, registry: PluginRegistry | None = None):
751 self._registry = registry or PluginRegistry()
753 def start_all(self) -> None:
754 pass
756 def stop_all(self) -> None:
757 pass