forked from MervinPraison/PraisonAI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecorator.py
More file actions
284 lines (221 loc) · 8.88 KB
/
decorator.py
File metadata and controls
284 lines (221 loc) · 8.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""Tool decorator for converting functions into tools.
This module provides the @tool decorator for easily creating tools from functions.
Usage:
from praisonaiagents import tool
@tool
def search(query: str) -> list:
'''Search the web for information.'''
return [...]
# Or with explicit parameters:
@tool(name="web_search", description="Search the internet")
def search(query: str, max_results: int = 5) -> list:
return [...]
# With injected state:
from praisonaiagents.tools import Injected
@tool
def my_tool(query: str, state: Injected[dict]) -> str:
'''Tool with injected state.'''
return f"session={state.get('session_id')}"
"""
import inspect
import functools
import logging
from typing import Any, Callable, Dict, Optional, Union, get_type_hints
from .base import BaseTool
# Lazy load injected module functions to reduce import time
_injected_module = None
def _get_injected_module():
"""Lazy load the injected module."""
global _injected_module
if _injected_module is None:
from . import injected as _inj
_injected_module = _inj
return _injected_module
def is_injected_type(annotation):
return _get_injected_module().is_injected_type(annotation)
def get_injected_params(func):
return _get_injected_module().get_injected_params(func)
def inject_state_into_kwargs(kwargs, injected_params):
return _get_injected_module().inject_state_into_kwargs(kwargs, injected_params)
def filter_injected_from_schema(schema, func):
return _get_injected_module().filter_injected_from_schema(schema, func)
class FunctionTool(BaseTool):
"""A BaseTool wrapper for plain functions.
Created automatically by the @tool decorator.
Supports Injected[T] parameters for state injection.
"""
def __init__(
self,
func: Callable,
name: Optional[str] = None,
description: Optional[str] = None,
version: str = "1.0.0"
):
self._func = func
self.name = name or func.__name__
self.description = description or func.__doc__ or f"Tool: {self.name}"
self.version = version
# Detect injected parameters
self._injected_params = get_injected_params(func)
# Generate schema from the original function, not run()
self.parameters = self._generate_schema_from_func(func)
# Copy function metadata
functools.update_wrapper(self, func)
# Skip parent's schema generation since we already did it
# Just set defaults that parent would set
if not self.name:
self.name = self.__class__.__name__.lower().replace("tool", "")
if not self.description:
self.description = self.__class__.__doc__ or f"Tool: {self.name}"
@property
def injected_params(self) -> Dict[str, Any]:
"""Get the injected parameters for this tool."""
return self._injected_params
def _generate_schema_from_func(self, func: Callable) -> Dict[str, Any]:
"""Generate JSON Schema from the wrapped function's signature.
Injected parameters are excluded from the schema.
"""
schema = {
"type": "object",
"properties": {},
"required": []
}
try:
sig = inspect.signature(func)
hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
for param_name, param in sig.parameters.items():
if param_name in ('self', 'cls'):
continue
# Skip injected parameters - they don't go in schema
if param_name in self._injected_params:
continue
# Get type hint
param_type = hints.get(param_name, Any)
# Double-check it's not an Injected type
if is_injected_type(param_type):
continue
json_type = BaseTool._python_type_to_json(param_type)
schema["properties"][param_name] = {"type": json_type}
# Check if required (no default value)
if param.default is inspect.Parameter.empty:
schema["required"].append(param_name)
except Exception as e:
logging.debug(f"Could not generate schema for {func.__name__}: {e}")
return schema
def run(self, **kwargs) -> Any:
"""Execute the wrapped function with injected state."""
# Inject state for any Injected parameters
kwargs = inject_state_into_kwargs(kwargs, self._injected_params)
return self._func(**kwargs)
def __call__(self, *args, **kwargs) -> Any:
"""Allow calling with positional args like the original function.
Injects state for Injected parameters.
"""
# Inject state for any Injected parameters
kwargs = inject_state_into_kwargs(kwargs, self._injected_params)
return self._func(*args, **kwargs)
def tool(
func: Optional[Callable] = None,
*,
name: Optional[str] = None,
description: Optional[str] = None,
version: str = "1.0.0"
) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]:
"""Decorator to convert a function into a tool.
Can be used with or without arguments:
@tool
def my_func(x: str) -> str:
'''Does something.'''
return x
@tool(name="custom_name", description="Custom description")
def my_func(x: str) -> str:
return x
Args:
func: The function to wrap (when used without parentheses)
name: Override the tool name (default: function name)
description: Override description (default: function docstring)
version: Tool version (default: "1.0.0")
Returns:
FunctionTool instance that wraps the function
"""
def decorator(fn: Callable) -> FunctionTool:
tool_instance = FunctionTool(
func=fn,
name=name,
description=description,
version=version
)
# Register with global registry if available
try:
from .registry import get_registry
registry = get_registry()
if registry:
registry.register(tool_instance)
except ImportError:
pass # Registry not yet available
return tool_instance
# Handle both @tool and @tool(...) syntax
if func is not None:
# Called without parentheses: @tool
return decorator(func)
else:
# Called with parentheses: @tool(...)
return decorator
def is_tool(obj: Any) -> bool:
"""Check if an object is a tool (BaseTool instance or decorated function)."""
if isinstance(obj, BaseTool):
return True
if isinstance(obj, FunctionTool):
return True
# Check for tools created by other frameworks
if hasattr(obj, 'run') and hasattr(obj, 'name'):
return True
return False
def get_tool_schema(obj: Any) -> Optional[Dict[str, Any]]:
"""Get OpenAI-compatible schema for any tool-like object.
Supports:
- BaseTool instances
- FunctionTool instances
- Plain functions (generates schema from signature)
- LangChain tools
- CrewAI tools
"""
# BaseTool or FunctionTool
if isinstance(obj, BaseTool):
return obj.get_schema()
# Plain callable
if callable(obj):
return _schema_from_function(obj)
return None
def _schema_from_function(func: Callable) -> Dict[str, Any]:
"""Generate OpenAI function schema from a plain function."""
name = getattr(func, '__name__', 'unknown')
description = func.__doc__ or f"Function: {name}"
# Build parameters schema
properties = {}
required = []
try:
sig = inspect.signature(func)
hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
for param_name, param in sig.parameters.items():
if param_name == 'self':
continue
param_type = hints.get(param_name, Any)
json_type = BaseTool._python_type_to_json(param_type)
properties[param_name] = {"type": json_type}
if param.default is inspect.Parameter.empty:
required.append(param_name)
except Exception as e:
logging.debug(f"Could not generate schema for {name}: {e}")
return {
"type": "function",
"function": {
"name": name,
"description": description.strip(),
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
}