Luxx/luxx/tools/factory.py

142 lines
4.5 KiB
Python

"""Tool decorator factory with unified result wrapping"""
from typing import Callable, Any, Dict, Optional, List
from luxx.tools.core import ToolDefinition, ToolResult, registry, CommandPermission
import traceback
import logging
logger = logging.getLogger(__name__)
def tool(
name: str,
description: str,
parameters: Dict[str, Any],
category: str = "general",
required_params: Optional[List[str]] = None,
required_permission: CommandPermission = CommandPermission.READ_ONLY
):
"""
Tool registration decorator with UNIFED result wrapping
The decorator automatically wraps all return values into ToolResult format.
Tool functions only need to return plain data - no need to use ToolResult manually.
Args:
name: Tool name
description: Tool description
parameters: OpenAI format parameters schema
category: Tool category
required_params: List of required parameter names (auto-validated)
Usage:
```python
@tool(
name="my_tool",
description="This is my tool",
parameters={
"type": "object",
"properties": {
"arg1": {"type": "string"}
},
"required": ["arg1"]
},
required_params=["arg1"] # Auto-validated
)
def my_tool(arguments: dict):
# Just return plain data - decorator handles wrapping!
return {"result": "success", "count": 5}
# For errors, return a dict with "error" key:
def my_tool2(arguments: dict):
return {"error": "Something went wrong"}
# Or raise an exception:
def my_tool3(arguments: dict):
raise ValueError("Invalid input")
```
The decorator will convert:
- Plain dict → {"success": true, "data": dict, "error": null}
- {"error": "msg"} → {"success": false, "data": null, "error": "msg"}
- Exception → {"success": false, "data": null, "error": "..."}
"""
def decorator(func: Callable) -> Callable:
def wrapped_handler(arguments: Dict[str, Any], context=None) -> ToolResult:
try:
# 1. Validate required params
if required_params:
for param in required_params:
if param not in arguments or arguments[param] is None:
return ToolResult.fail(f"Missing required parameter: {param}")
# 2. Execute handler - pass context only if function accepts it
import inspect
sig = inspect.signature(func)
if 'context' in sig.parameters:
result = func(arguments, context=context)
else:
result = func(arguments)
# 3. Auto-wrap result
return _auto_wrap(result)
except Exception as e:
logger.error(f"[{name}] Unexpected error: {type(e).__name__}: {str(e)}")
logger.debug(traceback.format_exc())
return ToolResult.fail(f"Execution failed: {type(e).__name__}: {str(e)}")
tool_def = ToolDefinition(
name=name,
description=description,
parameters=parameters,
handler=wrapped_handler,
category=category,
required_permission=required_permission
)
registry.register(tool_def)
return func
return decorator
def _auto_wrap(result: Any) -> ToolResult:
"""
Auto-wrap any return value into ToolResult format
Rules:
- ToolResult → use directly
- dict with "error" key → ToolResult.fail()
- dict without "error" → ToolResult.ok()
- other values → ToolResult.ok()
"""
# Already a ToolResult
if isinstance(result, ToolResult):
return result
# Dict with error
if isinstance(result, dict) and "error" in result:
return ToolResult.fail(str(result["error"]))
# Plain dict or other value
return ToolResult.ok(result)
def tool_function(
name: str = None,
description: str = None,
parameters: Dict[str, Any] = None,
category: str = "general",
required_params: Optional[List[str]] = None
):
"""
Alias for tool decorator, providing a more semantic naming
All parameters are the same as tool()
"""
return tool(
name=name,
description=description,
parameters=parameters,
category=category,
required_params=required_params
)