135 lines
4.1 KiB
Python
135 lines
4.1 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
|
|
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
|
|
):
|
|
"""
|
|
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]) -> 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
|
|
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
|
|
)
|
|
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
|
|
)
|