feat(tools): Phase T.1-T.4 complete - manifest system, registry, implementations, runtime, collaboration, scheduler
This commit is contained in:
223
backend/app/tools/registry.py
Normal file
223
backend/app/tools/registry.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Tool Registry
|
||||
|
||||
Central registry for managing tools with dynamic registration, discovery, and statistics.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolMetadata:
|
||||
"""Tool metadata"""
|
||||
|
||||
name: str
|
||||
display_name: str
|
||||
description: str
|
||||
version: str
|
||||
author: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
enabled: bool = True
|
||||
registered_at: datetime = field(default_factory=datetime.utcnow)
|
||||
|
||||
# Statistics
|
||||
call_count: int = 0
|
||||
error_count: int = 0
|
||||
total_duration_ms: int = 0
|
||||
|
||||
@property
|
||||
def avg_duration_ms(self) -> int:
|
||||
if self.call_count == 0:
|
||||
return 0
|
||||
return self.total_duration_ms // self.call_count
|
||||
|
||||
@property
|
||||
def error_rate(self) -> float:
|
||||
if self.call_count == 0:
|
||||
return 0.0
|
||||
return self.error_count / self.call_count
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Tool registry center for dynamic tool management"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: Dict[str, ToolMetadata] = {}
|
||||
self._executors: Dict[str, Callable] = {}
|
||||
self._configs: Dict[str, dict] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
# === Registration methods ===
|
||||
|
||||
async def register(
|
||||
self,
|
||||
manifest_path: str,
|
||||
executor: Callable,
|
||||
config: Optional[dict] = None,
|
||||
) -> ToolMetadata:
|
||||
"""Register a tool"""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
manifest_file = Path(manifest_path)
|
||||
if not manifest_file.exists():
|
||||
raise FileNotFoundError(f"Manifest not found: {manifest_path}")
|
||||
|
||||
with open(manifest_file, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
from tools.schemas.validator import validate_manifest
|
||||
|
||||
manifest = validate_manifest(data)
|
||||
|
||||
metadata = ToolMetadata(
|
||||
name=manifest.name,
|
||||
display_name=manifest.display_name,
|
||||
description=manifest.description,
|
||||
version=manifest.version,
|
||||
author=manifest.author,
|
||||
tags=manifest.tags or [],
|
||||
dependencies=manifest.dependencies or [],
|
||||
enabled=manifest.enabled,
|
||||
)
|
||||
|
||||
async with self._lock:
|
||||
self._tools[manifest.name] = metadata
|
||||
self._executors[manifest.name] = executor
|
||||
if config:
|
||||
self._configs[manifest.name] = config
|
||||
|
||||
return metadata
|
||||
|
||||
async def register_from_dict(
|
||||
self,
|
||||
manifest_data: dict,
|
||||
executor: Callable,
|
||||
config: Optional[dict] = None,
|
||||
) -> ToolMetadata:
|
||||
"""Register a tool from manifest data dict"""
|
||||
from tools.schemas.validator import validate_manifest
|
||||
|
||||
manifest = validate_manifest(manifest_data)
|
||||
|
||||
metadata = ToolMetadata(
|
||||
name=manifest.name,
|
||||
display_name=manifest.display_name,
|
||||
description=manifest.description,
|
||||
version=manifest.version,
|
||||
author=manifest.author,
|
||||
tags=manifest.tags or [],
|
||||
dependencies=manifest.dependencies or [],
|
||||
enabled=manifest.enabled,
|
||||
)
|
||||
|
||||
async with self._lock:
|
||||
self._tools[manifest.name] = metadata
|
||||
self._executors[manifest.name] = executor
|
||||
if config:
|
||||
self._configs[manifest.name] = config
|
||||
|
||||
return metadata
|
||||
|
||||
async def unregister(self, name: str) -> bool:
|
||||
"""Unregister a tool"""
|
||||
async with self._lock:
|
||||
if name in self._tools:
|
||||
del self._tools[name]
|
||||
del self._executors[name]
|
||||
self._configs.pop(name, None)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def enable(self, name: str) -> None:
|
||||
"""Enable a tool"""
|
||||
async with self._lock:
|
||||
if name in self._tools:
|
||||
self._tools[name].enabled = True
|
||||
|
||||
async def disable(self, name: str) -> None:
|
||||
"""Disable a tool"""
|
||||
async with self._lock:
|
||||
if name in self._tools:
|
||||
self._tools[name].enabled = False
|
||||
|
||||
# === Query methods ===
|
||||
|
||||
async def get(self, name: str) -> Optional[ToolMetadata]:
|
||||
"""Get tool metadata"""
|
||||
return self._tools.get(name)
|
||||
|
||||
async def get_executor(self, name: str) -> Optional[Callable]:
|
||||
"""Get tool executor"""
|
||||
return self._executors.get(name)
|
||||
|
||||
async def get_config(self, name: str) -> dict:
|
||||
"""Get tool configuration"""
|
||||
return self._configs.get(name, {})
|
||||
|
||||
async def list_all(self) -> List[ToolMetadata]:
|
||||
"""List all tools"""
|
||||
return list(self._tools.values())
|
||||
|
||||
async def list_enabled(self) -> List[ToolMetadata]:
|
||||
"""List enabled tools"""
|
||||
return [t for t in self._tools.values() if t.enabled]
|
||||
|
||||
async def list_by_tag(self, tag: str) -> List[ToolMetadata]:
|
||||
"""List tools by tag"""
|
||||
return [t for t in self._tools.values() if tag in t.tags]
|
||||
|
||||
async def search(self, query: str) -> List[ToolMetadata]:
|
||||
"""Search tools"""
|
||||
query_lower = query.lower()
|
||||
return [
|
||||
t
|
||||
for t in self._tools.values()
|
||||
if query_lower in t.name.lower()
|
||||
or query_lower in t.description.lower()
|
||||
or query_lower in t.display_name.lower()
|
||||
]
|
||||
|
||||
# === Statistics methods ===
|
||||
|
||||
async def record_call(
|
||||
self,
|
||||
name: str,
|
||||
duration_ms: int,
|
||||
error: bool = False,
|
||||
) -> None:
|
||||
"""Record a tool call"""
|
||||
async with self._lock:
|
||||
if name in self._tools:
|
||||
tool = self._tools[name]
|
||||
tool.call_count += 1
|
||||
tool.total_duration_ms += duration_ms
|
||||
if error:
|
||||
tool.error_count += 1
|
||||
|
||||
async def get_stats(self) -> dict:
|
||||
"""Get registry statistics"""
|
||||
tools = list(self._tools.values())
|
||||
return {
|
||||
"total_tools": len(tools),
|
||||
"enabled_tools": sum(1 for t in tools if t.enabled),
|
||||
"total_calls": sum(t.call_count for t in tools),
|
||||
"total_errors": sum(t.error_count for t in tools),
|
||||
"avg_error_rate": sum(t.error_rate for t in tools) / len(tools) if tools else 0,
|
||||
}
|
||||
|
||||
|
||||
# Global registry instance
|
||||
_registry: Optional[ToolRegistry] = None
|
||||
|
||||
|
||||
def get_registry() -> ToolRegistry:
|
||||
"""Get the global tool registry instance"""
|
||||
global _registry
|
||||
if _registry is None:
|
||||
_registry = ToolRegistry()
|
||||
return _registry
|
||||
Reference in New Issue
Block a user