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

1""" 

2AgentOS v1.14.3 — Plugin System & Tool Registry. 

3 

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 

10 

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 

18 

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""" 

30 

31from __future__ import annotations 

32 

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) 

48 

49 

50# ── Plugin Metadata ───────────────────────── 

51 

52 

53class PluginStatus(str, Enum): 

54 REGISTERED = "registered" # 已注册但未加载 

55 LOADED = "loaded" # 已加载但未激活 

56 ACTIVE = "active" # 已激活,正在运行 

57 ERROR = "error" # 加载失败 

58 DISABLED = "disabled" # 已禁用 

59 

60 

61@dataclass 

62class PluginManifest: 

63 """插件清单 — 描述插件的元数据和能力。""" 

64 

65 name: str = "" 

66 version: str = "0.1.0" 

67 description: str = "" 

68 author: str = "" 

69 license: str = "MIT" 

70 

71 # Entry point 

72 entry_point: str = "" # module:class or module:function 

73 plugin_class: str = "" # 插件主类名 

74 

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) # 提供的后端 

80 

81 # Dependencies 

82 depends_on: List[str] = field(default_factory=list) # 依赖的其他插件 

83 min_agentos_version: str = "1.0.0" 

84 

85 # Discovery 

86 discoverable: bool = True 

87 auto_activate: bool = False # 加载后自动激活 

88 tags: List[str] = field(default_factory=list) 

89 

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 ) 

110 

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 } 

127 

128 

129# ── Plugin Base ───────────────────────────── 

130 

131 

132class BasePlugin: 

133 """插件基类 — 所有插件必须继承此类。 

134 

135 Lifecycle: 

136 1. __init__() → registered 

137 2. load() → loaded 

138 3. activate() → active 

139 4. deactivate() → loaded 

140 5. unload() → registered 

141 

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 ) 

149 

150 def on_load(self): 

151 self.register_tool("my_tool", my_function) 

152 

153 def on_activate(self): 

154 print("Plugin activated!") 

155 """ 

156 

157 manifest: PluginManifest 

158 

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] = {} 

164 

165 # ── Lifecycle ── 

166 

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}") 

175 

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}") 

184 

185 def deactivate(self) -> None: 

186 """停用插件。""" 

187 try: 

188 self.on_deactivate() 

189 self._status = PluginStatus.LOADED 

190 except Exception: 

191 pass 

192 

193 def unload(self) -> None: 

194 """卸载插件。""" 

195 try: 

196 self.on_unload() 

197 self._status = PluginStatus.REGISTERED 

198 except Exception: 

199 pass 

200 

201 # ── Hooks (override in subclasses) ── 

202 

203 def on_load(self) -> None: 

204 """子类实现:加载时调用。""" 

205 pass 

206 

207 def on_activate(self) -> None: 

208 """子类实现:激活时调用。""" 

209 pass 

210 

211 def on_deactivate(self) -> None: 

212 """子类实现:停用时调用。""" 

213 pass 

214 

215 def on_unload(self) -> None: 

216 """子类实现:卸载时调用。""" 

217 pass 

218 

219 # ── Tool Registration ── 

220 

221 def register_tool(self, name: str, func: Callable) -> None: 

222 """注册工具函数。""" 

223 self._tools[name] = func 

224 

225 def unregister_tool(self, name: str) -> None: 

226 """注销工具函数。""" 

227 self._tools.pop(name, None) 

228 

229 def register_middleware(self, middleware: Callable) -> None: 

230 """注册中间件。""" 

231 self._middleware.append(middleware) 

232 

233 # ── Properties ── 

234 

235 @property 

236 def status(self) -> PluginStatus: 

237 return self._status 

238 

239 @property 

240 def tools(self) -> Dict[str, Callable]: 

241 return dict(self._tools) 

242 

243 @property 

244 def middleware(self) -> List[Callable]: 

245 return list(self._middleware) 

246 

247 @property 

248 def is_active(self) -> bool: 

249 return self._status == PluginStatus.ACTIVE 

250 

251 

252# ── Plugin Registry ───────────────────────── 

253 

254 

255class PluginRegistry: 

256 """插件注册中心(单例)。 

257 

258 管理所有插件的生命周期、依赖解析、发现。 

259 

260 Usage: 

261 registry = PluginRegistry() 

262 

263 # Discover from directory 

264 registry.discover("/path/to/plugins/") 

265 

266 # Load and activate 

267 registry.load_all() 

268 registry.activate_all() 

269 

270 # Get all tools from active plugins 

271 all_tools = registry.get_all_tools() 

272 """ 

273 

274 _instance: Optional["PluginRegistry"] = None 

275 

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 

281 

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 

291 

292 # ── Registration ── 

293 

294 def register(self, plugin: BasePlugin) -> bool: 

295 """注册插件。""" 

296 name = plugin.manifest.name 

297 if name in self._plugins: 

298 return False 

299 

300 self._plugins[name] = plugin 

301 self._manifests[name] = plugin.manifest 

302 return True 

303 

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() 

311 

312 self._plugins.pop(name, None) 

313 self._manifests.pop(name, None) 

314 

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 

320 

321 # ── Discovery ── 

322 

323 def discover(self, path: str, recursive: bool = True) -> List[str]: 

324 """从目录中发现插件。 

325 

326 扫描 plugin.json / agentos_plugin.json 文件。 

327 """ 

328 discovered: List[str] = [] 

329 base = Path(path) 

330 

331 if not base.exists(): 

332 return discovered 

333 

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) 

339 

340 manifest = PluginManifest.from_dict(data) 

341 if manifest.discoverable: 

342 self._manifests[manifest.name] = manifest 

343 discovered.append(manifest.name) 

344 

345 except Exception: 

346 continue 

347 

348 self._discovery_paths.append(path) 

349 return discovered 

350 

351 def discover_entry_points(self, group: str = "agentos.plugins") -> int: 

352 """通过 setuptools entry_points 发现插件。""" 

353 try: 

354 from importlib.metadata import entry_points 

355 

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 

367 

368 return count 

369 except ImportError: 

370 return 0 

371 

372 # ── Loading ── 

373 

374 def load(self, name: str) -> Optional[BasePlugin]: 

375 """加载单个插件。""" 

376 manifest = self._manifests.get(name) 

377 if not manifest: 

378 return None 

379 

380 # Check dependencies 

381 if not self._check_dependencies(manifest): 

382 return None 

383 

384 # Load plugin class 

385 plugin = self._instantiate_plugin(manifest) 

386 if not plugin: 

387 return None 

388 

389 try: 

390 plugin.load() 

391 self._plugins[name] = plugin 

392 

393 # Index tools 

394 for tool_name in plugin.tools: 

395 self._tools_index[tool_name] = name 

396 

397 return plugin 

398 except Exception: 

399 return None 

400 

401 def load_all(self) -> Dict[str, Optional[BasePlugin]]: 

402 """加载所有已发现但未加载的插件(按依赖拓扑排序)。""" 

403 results: Dict[str, Optional[BasePlugin]] = {} 

404 

405 order = self._resolve_order() 

406 

407 for name in order: 

408 if name not in self._plugins: 

409 results[name] = self.load(name) 

410 

411 return results 

412 

413 # ── Activation ── 

414 

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 

420 

421 try: 

422 plugin.activate() 

423 return True 

424 except Exception: 

425 return False 

426 

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 

435 

436 # ── Hot Reload ── 

437 

438 def reload(self, name: str) -> bool: 

439 """热重载插件(停用 → 卸载 → 重新加载 → 激活)。""" 

440 plugin = self._plugins.get(name) 

441 if not plugin: 

442 return False 

443 

444 was_active = plugin.is_active 

445 

446 if was_active: 

447 plugin.deactivate() 

448 plugin.unload() 

449 

450 # Reload 

451 new_plugin = self.load(name) 

452 if not new_plugin: 

453 return False 

454 

455 if was_active: 

456 new_plugin.activate() 

457 

458 return True 

459 

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 

467 

468 # ── Queries ── 

469 

470 def get_plugin(self, name: str) -> Optional[BasePlugin]: 

471 return self._plugins.get(name) 

472 

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) 

482 

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 

490 

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 

505 

506 def get_active_count(self) -> int: 

507 return sum(1 for p in self._plugins.values() if p.is_active) 

508 

509 # ── Internal ── 

510 

511 def _instantiate_plugin(self, manifest: PluginManifest) -> Optional[BasePlugin]: 

512 """从 entry_point 实例化插件。""" 

513 if not manifest.entry_point: 

514 return None 

515 

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() 

521 

522 if not isinstance(instance, BasePlugin): 

523 return None 

524 

525 return instance 

526 except Exception: 

527 return None 

528 

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 

536 

537 def _resolve_order(self) -> List[str]: 

538 """按依赖拓扑排序解析加载顺序。""" 

539 # Kahn's algorithm 

540 in_degree: Dict[str, int] = {} 

541 graph: Dict[str, List[str]] = {} 

542 

543 for name in self._manifests: 

544 in_degree[name] = 0 

545 graph[name] = [] 

546 

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 

552 

553 queue = [n for n, d in in_degree.items() if d == 0] 

554 order = [] 

555 

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) 

563 

564 return order 

565 

566 

567# ── File Watcher for Hot Reload ───────────── 

568 

569 

570class PluginFileWatcher: 

571 """文件变更监控器 — 检测到变更自动重载插件。 

572 

573 Usage: 

574 watcher = PluginFileWatcher(registry) 

575 await watcher.start() 

576 """ 

577 

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] = {} 

588 

589 async def start(self) -> None: 

590 """启动监控。""" 

591 self._running = True 

592 self._snapshot_files() 

593 self._task = asyncio.create_task(self._poll_loop()) 

594 

595 async def stop(self) -> None: 

596 """停止监控。""" 

597 self._running = False 

598 if self._task: 

599 self._task.cancel() 

600 

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 

611 

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 

619 

620 def _check_and_reload(self) -> None: 

621 """检查文件变更并触发重载。""" 

622 changed = False 

623 

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 

642 

643 if changed: 

644 self._registry.reload_all() 

645 

646 

647# ── Built-in Plugins ──────────────────────── 

648 

649 

650class AuditLoggerPlugin(BasePlugin): 

651 """内置审计日志插件。""" 

652 

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 ) 

660 

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)}") 

671 

672 self.register_middleware(audit_log) 

673 

674 

675class HealthCheckPlugin(BasePlugin): 

676 """内置健康检查插件。""" 

677 

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 ) 

685 

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 } 

693 

694 self.register_tool("health_check", health_check) 

695 

696 

697# ── Quick Start ───────────────────────────── 

698 

699 

700def create_registry_with_builtins() -> PluginRegistry: 

701 """创建注册中心并注册内置插件。""" 

702 registry = PluginRegistry() 

703 registry.register(HealthCheckPlugin()) 

704 registry.register(AuditLoggerPlugin()) 

705 

706 for name in ["health_check", "audit_logger"]: 

707 registry.load(name) 

708 registry.activate(name) 

709 

710 return registry 

711 

712 

713# ── Missing compat classes (required by agentos/__init__.py) ── 

714 

715@dataclass 

716class RegisteredPlugin: 

717 """已注册插件快照。""" 

718 manifest: PluginManifest 

719 status: PluginStatus = PluginStatus.REGISTERED 

720 loaded_at: Optional[float] = None 

721 error: Optional[str] = None 

722 

723 

724@dataclass 

725class DiscoveredPlugin: 

726 """从文件系统发现的插件元信息。""" 

727 name: str 

728 path: str 

729 manifest: Optional[PluginManifest] = None 

730 

731 

732class PluginDiscovery: 

733 """插件发现器。""" 

734 

735 def __init__(self, paths: Optional[list[str]] = None): 

736 self._paths = paths or [] 

737 

738 def discover(self) -> list[DiscoveredPlugin]: 

739 return [] 

740 

741 

742class PluginLoader: 

743 """插件加载器。""" 

744 

745 def __init__(self, registry: Optional[PluginRegistry] = None): 

746 self._registry = registry or PluginRegistry() 

747 

748 def load_from_discovery(self, discovered: list[DiscoveredPlugin]) -> int: 

749 return 0 

750 

751 

752class LifecycleManager: 

753 """插件生命周期管理器。""" 

754 

755 def __init__(self, registry: Optional[PluginRegistry] = None): 

756 self._registry = registry or PluginRegistry() 

757 

758 def start_all(self) -> None: 

759 pass 

760 

761 def stop_all(self) -> None: 

762 pass