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

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

45 

46# ── Plugin Metadata ───────────────────────── 

47 

48 

49class PluginStatus(StrEnum): 

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

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

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

53 ERROR = "error" # 加载失败 

54 DISABLED = "disabled" # 已禁用 

55 

56 

57@dataclass 

58class PluginManifest: 

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

60 

61 name: str = "" 

62 version: str = "0.1.0" 

63 description: str = "" 

64 author: str = "" 

65 license: str = "MIT" 

66 

67 # Entry point 

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

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

70 

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

76 

77 # Dependencies 

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

79 min_agentos_version: str = "1.0.0" 

80 

81 # Discovery 

82 discoverable: bool = True 

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

84 tags: list[str] = field(default_factory=list) 

85 

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 ) 

106 

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 } 

123 

124 

125# ── Plugin Base ───────────────────────────── 

126 

127 

128class BasePlugin: 

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

130 

131 Lifecycle: 

132 1. __init__() → registered 

133 2. load() → loaded 

134 3. activate() → active 

135 4. deactivate() → loaded 

136 5. unload() → registered 

137 

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 ) 

145 

146 def on_load(self): 

147 self.register_tool("my_tool", my_function) 

148 

149 def on_activate(self): 

150 print("Plugin activated!") 

151 """ 

152 

153 manifest: PluginManifest 

154 

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

160 

161 # ── Lifecycle ── 

162 

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

171 

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

180 

181 def deactivate(self) -> None: 

182 """停用插件。""" 

183 try: 

184 self.on_deactivate() 

185 self._status = PluginStatus.LOADED 

186 except Exception: 

187 pass 

188 

189 def unload(self) -> None: 

190 """卸载插件。""" 

191 try: 

192 self.on_unload() 

193 self._status = PluginStatus.REGISTERED 

194 except Exception: 

195 pass 

196 

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

198 

199 def on_load(self) -> None: 

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

201 

202 def on_activate(self) -> None: 

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

204 

205 def on_deactivate(self) -> None: 

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

207 

208 def on_unload(self) -> None: 

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

210 

211 # ── Tool Registration ── 

212 

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

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

215 self._tools[name] = func 

216 

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

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

219 self._tools.pop(name, None) 

220 

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

222 """注册中间件。""" 

223 self._middleware.append(middleware) 

224 

225 # ── Properties ── 

226 

227 @property 

228 def status(self) -> PluginStatus: 

229 return self._status 

230 

231 @property 

232 def tools(self) -> dict[str, Callable]: 

233 return dict(self._tools) 

234 

235 @property 

236 def middleware(self) -> list[Callable]: 

237 return list(self._middleware) 

238 

239 @property 

240 def is_active(self) -> bool: 

241 return self._status == PluginStatus.ACTIVE 

242 

243 

244# ── Plugin Registry ───────────────────────── 

245 

246 

247class PluginRegistry: 

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

249 

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

251 

252 Usage: 

253 registry = PluginRegistry() 

254 

255 # Discover from directory 

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

257 

258 # Load and activate 

259 registry.load_all() 

260 registry.activate_all() 

261 

262 # Get all tools from active plugins 

263 all_tools = registry.get_all_tools() 

264 """ 

265 

266 _instance: PluginRegistry | None = None 

267 

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 

273 

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 

283 

284 # ── Registration ── 

285 

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

287 """注册插件。""" 

288 name = plugin.manifest.name 

289 if name in self._plugins: 

290 return False 

291 

292 self._plugins[name] = plugin 

293 self._manifests[name] = plugin.manifest 

294 return True 

295 

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

303 

304 self._plugins.pop(name, None) 

305 self._manifests.pop(name, None) 

306 

307 # Clean tool index 

308 self._tools_index = {tn: pn for tn, pn in self._tools_index.items() if pn != name} 

309 return True 

310 

311 # ── Discovery ── 

312 

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

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

315 

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

317 """ 

318 discovered: list[str] = [] 

319 base = Path(path) 

320 

321 if not base.exists(): 

322 return discovered 

323 

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) 

329 

330 manifest = PluginManifest.from_dict(data) 

331 if manifest.discoverable: 

332 self._manifests[manifest.name] = manifest 

333 discovered.append(manifest.name) 

334 

335 except Exception: 

336 continue 

337 

338 self._discovery_paths.append(path) 

339 return discovered 

340 

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

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

343 try: 

344 from importlib.metadata import entry_points 

345 

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 

357 

358 return count 

359 except ImportError: 

360 return 0 

361 

362 # ── Loading ── 

363 

364 def load(self, name: str) -> BasePlugin | None: 

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

366 manifest = self._manifests.get(name) 

367 if not manifest: 

368 return None 

369 

370 # Check dependencies 

371 if not self._check_dependencies(manifest): 

372 return None 

373 

374 # Load plugin class 

375 plugin = self._instantiate_plugin(manifest) 

376 if not plugin: 

377 return None 

378 

379 try: 

380 plugin.load() 

381 self._plugins[name] = plugin 

382 

383 # Index tools 

384 for tool_name in plugin.tools: 

385 self._tools_index[tool_name] = name 

386 

387 return plugin 

388 except Exception: 

389 return None 

390 

391 def load_all(self) -> dict[str, BasePlugin | None]: 

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

393 results: dict[str, BasePlugin | None] = {} 

394 

395 order = self._resolve_order() 

396 

397 for name in order: 

398 if name not in self._plugins: 

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

400 

401 return results 

402 

403 # ── Activation ── 

404 

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 

410 

411 try: 

412 plugin.activate() 

413 return True 

414 except Exception: 

415 return False 

416 

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 

425 

426 # ── Hot Reload ── 

427 

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

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

430 plugin = self._plugins.get(name) 

431 if not plugin: 

432 return False 

433 

434 was_active = plugin.is_active 

435 

436 if was_active: 

437 plugin.deactivate() 

438 plugin.unload() 

439 

440 # Reload 

441 new_plugin = self.load(name) 

442 if not new_plugin: 

443 return False 

444 

445 if was_active: 

446 new_plugin.activate() 

447 

448 return True 

449 

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 

457 

458 # ── Queries ── 

459 

460 def get_plugin(self, name: str) -> BasePlugin | None: 

461 return self._plugins.get(name) 

462 

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) 

472 

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 

480 

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 

497 

498 def get_active_count(self) -> int: 

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

500 

501 # ── Internal ── 

502 

503 def _instantiate_plugin(self, manifest: PluginManifest) -> BasePlugin | None: 

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

505 if not manifest.entry_point: 

506 return None 

507 

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

513 

514 if not isinstance(instance, BasePlugin): 

515 return None 

516 

517 return instance 

518 except Exception: 

519 return None 

520 

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 

528 

529 def _resolve_order(self) -> list[str]: 

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

531 # Kahn's algorithm 

532 in_degree: dict[str, int] = {} 

533 graph: dict[str, list[str]] = {} 

534 

535 for name in self._manifests: 

536 in_degree[name] = 0 

537 graph[name] = [] 

538 

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 

544 

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

546 order = [] 

547 

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) 

555 

556 return order 

557 

558 

559# ── File Watcher for Hot Reload ───────────── 

560 

561 

562class PluginFileWatcher: 

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

564 

565 Usage: 

566 watcher = PluginFileWatcher(registry) 

567 await watcher.start() 

568 """ 

569 

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

580 

581 async def start(self) -> None: 

582 """启动监控。""" 

583 self._running = True 

584 self._snapshot_files() 

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

586 

587 async def stop(self) -> None: 

588 """停止监控。""" 

589 self._running = False 

590 if self._task: 

591 self._task.cancel() 

592 

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 

603 

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 

611 

612 def _check_and_reload(self) -> None: 

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

614 changed = False 

615 

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 

634 

635 if changed: 

636 self._registry.reload_all() 

637 

638 

639# ── Built-in Plugins ──────────────────────── 

640 

641 

642class AuditLoggerPlugin(BasePlugin): 

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

644 

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 ) 

652 

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

663 

664 self.register_middleware(audit_log) 

665 

666 

667class HealthCheckPlugin(BasePlugin): 

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

669 

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 ) 

677 

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 } 

685 

686 self.register_tool("health_check", health_check) 

687 

688 

689# ── Quick Start ───────────────────────────── 

690 

691 

692def create_registry_with_builtins() -> PluginRegistry: 

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

694 registry = PluginRegistry() 

695 registry.register(HealthCheckPlugin()) 

696 registry.register(AuditLoggerPlugin()) 

697 

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

699 registry.load(name) 

700 registry.activate(name) 

701 

702 return registry 

703 

704 

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

706 

707 

708@dataclass 

709class RegisteredPlugin: 

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

711 

712 manifest: PluginManifest 

713 status: PluginStatus = PluginStatus.REGISTERED 

714 loaded_at: float | None = None 

715 error: str | None = None 

716 

717 

718@dataclass 

719class DiscoveredPlugin: 

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

721 

722 name: str 

723 path: str 

724 manifest: PluginManifest | None = None 

725 

726 

727class PluginDiscovery: 

728 """插件发现器。""" 

729 

730 def __init__(self, paths: list[str] | None = None): 

731 self._paths = paths or [] 

732 

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

734 return [] 

735 

736 

737class PluginLoader: 

738 """插件加载器。""" 

739 

740 def __init__(self, registry: PluginRegistry | None = None): 

741 self._registry = registry or PluginRegistry() 

742 

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

744 return 0 

745 

746 

747class LifecycleManager: 

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

749 

750 def __init__(self, registry: PluginRegistry | None = None): 

751 self._registry = registry or PluginRegistry() 

752 

753 def start_all(self) -> None: 

754 pass 

755 

756 def stop_all(self) -> None: 

757 pass