Coverage for src / osiris_cli / tools_registry.py: 0%
78 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-31 05:01 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-31 05:01 +0200
1#!/usr/bin/env python3
2"""
3Tool Registry - Centralized tool management
4Handles tool definitions, execution, and permissions.
5"""
7from typing import Dict, Any, List, Callable, Optional
8from dataclasses import dataclass
9import inspect
11from .session import session
14@dataclass
15class ToolDefinition:
16 """Represents a tool that can be called by the agent"""
17 name: str
18 description: str
19 parameters: Dict[str, Any]
20 function: Callable
21 requires_approval: bool = False
22 category: str = "general"
25class ToolsRegistry:
26 """
27 Central registry for all agent tools.
28 Manages tool definitions, execution, and permissions.
29 """
31 def __init__(self):
32 self._tools: Dict[str, ToolDefinition] = {}
33 self._auto_approved_tools: set = set()
35 def register(
36 self,
37 name: str,
38 description: str,
39 parameters: Dict[str, Any],
40 function: Callable,
41 requires_approval: bool = False,
42 category: str = "general"
43 ):
44 """Register a new tool"""
45 self._tools[name] = ToolDefinition(
46 name=name,
47 description=description,
48 parameters=parameters,
49 function=function,
50 requires_approval=requires_approval,
51 category=category
52 )
54 def register_decorator(
55 self,
56 description: str,
57 parameters: Dict[str, Any],
58 requires_approval: bool = False,
59 category: str = "general"
60 ):
61 """Decorator for registering tools"""
62 def decorator(func: Callable):
63 self.register(
64 name=func.__name__,
65 description=description,
66 parameters=parameters,
67 function=func,
68 requires_approval=requires_approval,
69 category=category
70 )
71 return func
72 return decorator
74 async def execute(self, name: str, arguments: Dict[str, Any]) -> Any:
75 """Execute a tool by name (Asynchronous & Non-blocking)"""
76 import asyncio
77 if name not in self._tools:
78 raise ValueError(f"Tool '{name}' not found")
80 tool = self._tools[name]
82 session.enforce_tool_policy(name)
84 # Check if tool requires approval and isn't auto-approved
85 if tool.requires_approval and name not in self._auto_approved_tools:
86 raise PermissionError(f"Tool '{name}' requires approval")
88 # Execute the tool safely
89 if asyncio.iscoroutinefunction(tool.function):
90 return await tool.function(**arguments)
92 # 2025 Best Practice: Run blocking sync tools in a separate thread
93 # This ensures the TUI animation remains fluid.
94 return await asyncio.to_thread(tool.function, **arguments)
96 def get_definitions(self) -> Dict:
97 """Return tools as dict for display"""
98 return {
99 name: {
100 "description": tool.description,
101 "parameters": tool.parameters,
102 }
103 for name, tool in self._tools.items()
104 }
106 def get_openai_format(self) -> List[Dict[str, Any]]:
107 """Get tools in OpenAI function calling format"""
108 return self.get_openai_format_filtered()
110 def get_openai_format_filtered(self, categories: Optional[List[str]] = None) -> List[Dict[str, Any]]:
111 """Get filtered tools in OpenAI format. Always includes 'general'."""
112 allowed = set(categories) if categories else None
114 filtered = []
115 for tool in self._tools.values():
116 if allowed is None or tool.category == "general" or tool.category in allowed:
117 filtered.append({
118 "type": "function",
119 "function": {
120 "name": tool.name,
121 "description": tool.description,
122 "parameters": tool.parameters
123 }
124 })
125 return filtered
127 def get_tool(self, name: str) -> Optional[ToolDefinition]:
128 """Get a tool definition by name"""
129 return self._tools.get(name)
131 def list_tools(self, category: Optional[str] = None) -> List[ToolDefinition]:
132 """List all tools, optionally filtered by category"""
133 if category:
134 return [t for t in self._tools.values() if t.category == category]
135 return list(self._tools.values())
137 def auto_approve(self, tool_name: str):
138 """Mark a tool as auto-approved"""
139 self._auto_approved_tools.add(tool_name)
141 def load_skills_from_dir(self, skills_dir: str):
142 """Dynamically load and register tools from a directory of Python files."""
143 import importlib.util
144 import sys
145 from pathlib import Path
147 path = Path(skills_dir)
148 if not path.exists():
149 return
151 for py_file in path.glob("*.py"):
152 if py_file.name.startswith("_"): continue
154 try:
155 module_name = f"osiris_skill_{py_file.stem}"
156 spec = importlib.util.spec_from_file_location(module_name, py_file)
157 if spec and spec.loader:
158 module = importlib.util.module_from_spec(spec)
159 sys.modules[module_name] = module
160 spec.loader.exec_module(module)
162 # Look for functions decorated with @tools_registry.register_decorator
163 # or a specific 'register_skill' call in the module
164 if hasattr(module, "register_skill"):
165 module.register_skill(self)
166 except Exception as e:
167 print(f"Error loading skill {py_file.name}: {e}")
169 def remove_auto_approval(self, tool_name: str):
170 """Remove auto-approval for a tool"""
171 self._auto_approved_tools.discard(tool_name)
173 def is_auto_approved(self, tool_name: str) -> bool:
174 """Check if a tool is auto-approved"""
175 return tool_name in self._auto_approved_tools
178# Global registry instance
179tools_registry = ToolsRegistry()