rnow 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rnow/__init__.py +5 -0
- rnow/__main__.py +7 -0
- rnow/cli/__init__.py +6 -0
- rnow/cli/auth.py +67 -0
- rnow/cli/blob.py +98 -0
- rnow/cli/commands.py +2311 -0
- rnow/cli/common.py +28 -0
- rnow/cli/cube.py +255 -0
- rnow/cli/main.py +49 -0
- rnow/cli/test.py +728 -0
- rnow/cli/token_count.py +295 -0
- rnow/core/__init__.py +33 -0
- rnow/core/reward.py +333 -0
- rnow/core/tool.py +494 -0
- rnow/models.py +295 -0
- rnow/templates/deepseek-aha/config.yml +26 -0
- rnow/templates/deepseek-aha/rewards.py +36 -0
- rnow/templates/deepseek-aha/train.jsonl +1000 -0
- rnow/templates/mcp-tavily/config.yml +29 -0
- rnow/templates/mcp-tavily/requirements.txt +1 -0
- rnow/templates/mcp-tavily/rewards.py +25 -0
- rnow/templates/mcp-tavily/train.jsonl +500 -0
- rnow/templates/new/config.yml +26 -0
- rnow/templates/new/requirements.txt +1 -0
- rnow/templates/new/rewards.py +0 -0
- rnow/templates/new/train.jsonl +0 -0
- rnow/templates/rl-nextjs/config.yml +27 -0
- rnow/templates/rl-nextjs/requirements.txt +2 -0
- rnow/templates/rl-nextjs/rewards.py +446 -0
- rnow/templates/rl-nextjs/train.jsonl +1000 -0
- rnow/templates/rl-single/config.yml +27 -0
- rnow/templates/rl-single/requirements.txt +1 -0
- rnow/templates/rl-single/rewards.py +14 -0
- rnow/templates/rl-single/train.jsonl +1000 -0
- rnow/templates/rl-tools/config.yml +27 -0
- rnow/templates/rl-tools/env.py +38 -0
- rnow/templates/rl-tools/requirements.txt +3 -0
- rnow/templates/rl-tools/rewards.py +25 -0
- rnow/templates/rl-tools/train.jsonl +500 -0
- rnow/templates/sft/config.yml +20 -0
- rnow/templates/sft/train.jsonl +100 -0
- rnow/templates/tutorial-reward/config.yml +27 -0
- rnow/templates/tutorial-reward/requirements.txt +1 -0
- rnow/templates/tutorial-reward/rewards.py +15 -0
- rnow/templates/tutorial-reward/train.jsonl +1000 -0
- rnow/templates/tutorial-tool/config.yml +27 -0
- rnow/templates/tutorial-tool/env.py +7 -0
- rnow/templates/tutorial-tool/requirements.txt +3 -0
- rnow/templates/tutorial-tool/rewards.py +7 -0
- rnow/templates/tutorial-tool/train.jsonl +1266 -0
- rnow-0.2.4.dist-info/METADATA +135 -0
- rnow-0.2.4.dist-info/RECORD +56 -0
- rnow-0.2.4.dist-info/WHEEL +5 -0
- rnow-0.2.4.dist-info/entry_points.txt +2 -0
- rnow-0.2.4.dist-info/licenses/LICENSE +21 -0
- rnow-0.2.4.dist-info/top_level.txt +1 -0
rnow/core/tool.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool entry point for ReinforceNow with robust validation.
|
|
3
|
+
|
|
4
|
+
Validates at decorator-time:
|
|
5
|
+
- Function has docstring or description
|
|
6
|
+
- No *args/**kwargs in signature
|
|
7
|
+
- All parameters have type hints
|
|
8
|
+
- Return type is declared and JSON-serializable
|
|
9
|
+
- Supports Optional[T], list[T], dict[str, T], Literal[...], Union types
|
|
10
|
+
|
|
11
|
+
Validates at runtime:
|
|
12
|
+
- Arguments match schema (required keys, no extra keys, type coercion)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import (
|
|
18
|
+
Any,
|
|
19
|
+
Union,
|
|
20
|
+
get_args,
|
|
21
|
+
get_origin,
|
|
22
|
+
get_type_hints,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Global registry for tool functions
|
|
26
|
+
TOOL_REGISTRY: dict[str, Callable] = {}
|
|
27
|
+
|
|
28
|
+
# Types that are JSON-serializable
|
|
29
|
+
JSON_SERIALIZABLE_TYPES = (str, int, float, bool, list, dict, type(None))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def clear_tool_registry() -> None:
|
|
33
|
+
"""Clear the tool registry (useful for testing multiple projects)."""
|
|
34
|
+
TOOL_REGISTRY.clear()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _map_type_to_json_schema(py_type: Any) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Map a Python type annotation to a JSON Schema fragment.
|
|
40
|
+
|
|
41
|
+
Supports:
|
|
42
|
+
- Primitives: str, int, float, bool
|
|
43
|
+
- Collections: list, List[T], dict, Dict[str, T]
|
|
44
|
+
- Optional: Optional[T], T | None, Union[T, None]
|
|
45
|
+
- Literal: Literal["foo", "bar"]
|
|
46
|
+
- Any: defaults to string
|
|
47
|
+
"""
|
|
48
|
+
origin = get_origin(py_type) or py_type
|
|
49
|
+
args = get_args(py_type)
|
|
50
|
+
|
|
51
|
+
# Simple primitives
|
|
52
|
+
if origin is str:
|
|
53
|
+
return {"type": "string"}
|
|
54
|
+
if origin is int:
|
|
55
|
+
return {"type": "integer"}
|
|
56
|
+
if origin is float:
|
|
57
|
+
return {"type": "number"}
|
|
58
|
+
if origin is bool:
|
|
59
|
+
return {"type": "boolean"}
|
|
60
|
+
if origin is type(None):
|
|
61
|
+
return {"type": "null"}
|
|
62
|
+
|
|
63
|
+
# List[T] / list[T]
|
|
64
|
+
if origin in (list, list):
|
|
65
|
+
if args:
|
|
66
|
+
return {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": _map_type_to_json_schema(args[0]),
|
|
69
|
+
}
|
|
70
|
+
return {"type": "array"}
|
|
71
|
+
|
|
72
|
+
# Dict[K, V] / dict[K, V]
|
|
73
|
+
if origin is dict:
|
|
74
|
+
return {"type": "object"}
|
|
75
|
+
|
|
76
|
+
# Literal["foo", "bar"] -> enum
|
|
77
|
+
try:
|
|
78
|
+
from typing import Literal
|
|
79
|
+
|
|
80
|
+
if origin is Literal:
|
|
81
|
+
return {"type": "string", "enum": list(args)}
|
|
82
|
+
except ImportError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Optional[T] / Union[T, None] / T | None
|
|
86
|
+
if origin is Union:
|
|
87
|
+
# Filter out None types
|
|
88
|
+
non_none_args = [a for a in args if a is not type(None)]
|
|
89
|
+
has_none = len(non_none_args) < len(args)
|
|
90
|
+
|
|
91
|
+
if len(non_none_args) == 1:
|
|
92
|
+
# Optional[T] case
|
|
93
|
+
schema = _map_type_to_json_schema(non_none_args[0])
|
|
94
|
+
if has_none and "type" in schema:
|
|
95
|
+
# Allow null
|
|
96
|
+
current_type = schema["type"]
|
|
97
|
+
if isinstance(current_type, list):
|
|
98
|
+
schema["type"] = current_type + ["null"]
|
|
99
|
+
else:
|
|
100
|
+
schema["type"] = [current_type, "null"]
|
|
101
|
+
return schema
|
|
102
|
+
else:
|
|
103
|
+
# Union[A, B, C] case - use anyOf
|
|
104
|
+
schemas = [_map_type_to_json_schema(a) for a in non_none_args]
|
|
105
|
+
if has_none:
|
|
106
|
+
schemas.append({"type": "null"})
|
|
107
|
+
return {"anyOf": schemas}
|
|
108
|
+
|
|
109
|
+
# Any or unknown type - default to string
|
|
110
|
+
return {"type": "string"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _is_json_serializable_type(py_type: Any) -> bool:
|
|
114
|
+
"""Check if a type annotation represents a JSON-serializable type."""
|
|
115
|
+
origin = get_origin(py_type) or py_type
|
|
116
|
+
args = get_args(py_type)
|
|
117
|
+
|
|
118
|
+
# Direct JSON types
|
|
119
|
+
if origin in JSON_SERIALIZABLE_TYPES:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# List/dict with JSON-serializable contents
|
|
123
|
+
if origin in (list, list):
|
|
124
|
+
if args:
|
|
125
|
+
return _is_json_serializable_type(args[0])
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
if origin is dict:
|
|
129
|
+
if args and len(args) >= 2:
|
|
130
|
+
return _is_json_serializable_type(args[1])
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
# Optional/Union
|
|
134
|
+
if origin is Union:
|
|
135
|
+
return all(_is_json_serializable_type(a) for a in args)
|
|
136
|
+
|
|
137
|
+
# Literal
|
|
138
|
+
try:
|
|
139
|
+
from typing import Literal
|
|
140
|
+
|
|
141
|
+
if origin is Literal:
|
|
142
|
+
return True
|
|
143
|
+
except ImportError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Any
|
|
147
|
+
return py_type is Any
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _infer_schema(func: Callable) -> dict[str, Any]:
|
|
151
|
+
"""
|
|
152
|
+
Infer JSON schema from function signature with full type support.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
TypeError: If parameters are missing type hints or use *args/**kwargs
|
|
156
|
+
"""
|
|
157
|
+
sig = inspect.signature(func)
|
|
158
|
+
hints = get_type_hints(func)
|
|
159
|
+
|
|
160
|
+
# Disallow *args/**kwargs - tools must have explicit parameters
|
|
161
|
+
for param in sig.parameters.values():
|
|
162
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
163
|
+
raise TypeError(
|
|
164
|
+
f"Tool '{func.__name__}' cannot use *args. "
|
|
165
|
+
"Define explicit, typed parameters instead."
|
|
166
|
+
)
|
|
167
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
168
|
+
raise TypeError(
|
|
169
|
+
f"Tool '{func.__name__}' cannot use **kwargs. "
|
|
170
|
+
"Define explicit, typed parameters instead."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
properties: dict[str, Any] = {}
|
|
174
|
+
required: list[str] = []
|
|
175
|
+
|
|
176
|
+
for param_name, param in sig.parameters.items():
|
|
177
|
+
if param_name in ("self", "cls"):
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Require type hint for every parameter
|
|
181
|
+
if param_name not in hints:
|
|
182
|
+
raise TypeError(
|
|
183
|
+
f"Missing type hint for parameter '{param_name}' in tool '{func.__name__}'. "
|
|
184
|
+
"All parameters must have type annotations."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
param_type = hints[param_name]
|
|
188
|
+
properties[param_name] = _map_type_to_json_schema(param_type)
|
|
189
|
+
|
|
190
|
+
# Mark as required if no default value
|
|
191
|
+
if param.default is inspect.Parameter.empty:
|
|
192
|
+
required.append(param_name)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"properties": properties,
|
|
197
|
+
"required": required,
|
|
198
|
+
"additionalProperties": False,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _validate_return_type(func: Callable) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Validate that the function has a return type annotation that is JSON-serializable.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
TypeError: If return type is missing or not JSON-serializable
|
|
208
|
+
"""
|
|
209
|
+
sig = inspect.signature(func)
|
|
210
|
+
hints = get_type_hints(func)
|
|
211
|
+
|
|
212
|
+
# Check return annotation exists
|
|
213
|
+
if sig.return_annotation is inspect.Signature.empty and "return" not in hints:
|
|
214
|
+
raise TypeError(
|
|
215
|
+
f"Tool '{func.__name__}' must declare a return type annotation. "
|
|
216
|
+
"Add -> ReturnType to the function signature."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return_type = hints.get("return", sig.return_annotation)
|
|
220
|
+
|
|
221
|
+
# Check return type is JSON-serializable
|
|
222
|
+
if not _is_json_serializable_type(return_type):
|
|
223
|
+
raise TypeError(
|
|
224
|
+
f"Tool '{func.__name__}' return type '{return_type}' is not JSON-serializable. "
|
|
225
|
+
"Use dict, list, str, int, float, bool, or None."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def validate_tool_args(
|
|
230
|
+
tool_name: str, schema: dict[str, Any], args: dict[str, Any]
|
|
231
|
+
) -> tuple[bool, str | None]:
|
|
232
|
+
"""
|
|
233
|
+
Validate tool arguments against the schema.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
tool_name: Name of the tool (for error messages)
|
|
237
|
+
schema: The tool's JSON schema
|
|
238
|
+
args: Arguments to validate
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of (is_valid, error_message)
|
|
242
|
+
"""
|
|
243
|
+
required = schema.get("required", [])
|
|
244
|
+
props = schema.get("properties", {})
|
|
245
|
+
allow_additional = schema.get("additionalProperties", True)
|
|
246
|
+
|
|
247
|
+
# Check for missing required arguments
|
|
248
|
+
for key in required:
|
|
249
|
+
if key not in args:
|
|
250
|
+
return False, f"Tool '{tool_name}': missing required argument '{key}'."
|
|
251
|
+
|
|
252
|
+
# Check for unexpected arguments
|
|
253
|
+
if not allow_additional:
|
|
254
|
+
for key in args:
|
|
255
|
+
if key not in props:
|
|
256
|
+
return False, f"Tool '{tool_name}': unexpected argument '{key}'."
|
|
257
|
+
|
|
258
|
+
# Type validation with coercion attempts
|
|
259
|
+
for key, value in args.items():
|
|
260
|
+
if key not in props:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
prop_schema = props[key]
|
|
264
|
+
expected_type = prop_schema.get("type")
|
|
265
|
+
|
|
266
|
+
if expected_type is None:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Handle array types (can be ["string", "null"])
|
|
270
|
+
expected_types = expected_type if isinstance(expected_type, list) else [expected_type]
|
|
271
|
+
|
|
272
|
+
# Check if value matches any expected type
|
|
273
|
+
valid = False
|
|
274
|
+
for exp_type in expected_types:
|
|
275
|
+
if (
|
|
276
|
+
exp_type == "string"
|
|
277
|
+
and isinstance(value, str)
|
|
278
|
+
or exp_type == "integer"
|
|
279
|
+
and isinstance(value, int)
|
|
280
|
+
and not isinstance(value, bool)
|
|
281
|
+
or exp_type == "number"
|
|
282
|
+
and isinstance(value, int | float)
|
|
283
|
+
and not isinstance(value, bool)
|
|
284
|
+
or exp_type == "boolean"
|
|
285
|
+
and isinstance(value, bool)
|
|
286
|
+
or exp_type == "array"
|
|
287
|
+
and isinstance(value, list)
|
|
288
|
+
or exp_type == "object"
|
|
289
|
+
and isinstance(value, dict)
|
|
290
|
+
or exp_type == "null"
|
|
291
|
+
and value is None
|
|
292
|
+
):
|
|
293
|
+
valid = True
|
|
294
|
+
|
|
295
|
+
if not valid:
|
|
296
|
+
# Attempt type coercion for common cases
|
|
297
|
+
coerced, coerced_value = _try_coerce(value, expected_types)
|
|
298
|
+
if coerced:
|
|
299
|
+
args[key] = coerced_value
|
|
300
|
+
else:
|
|
301
|
+
return False, (
|
|
302
|
+
f"Tool '{tool_name}': argument '{key}' expected type "
|
|
303
|
+
f"{expected_types}, got {type(value).__name__}."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return True, None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _try_coerce(value: Any, expected_types: list[str]) -> tuple[bool, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Try to coerce a value to one of the expected types.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Tuple of (success, coerced_value)
|
|
315
|
+
"""
|
|
316
|
+
for exp_type in expected_types:
|
|
317
|
+
try:
|
|
318
|
+
if exp_type == "integer" and isinstance(value, str):
|
|
319
|
+
return True, int(value)
|
|
320
|
+
if exp_type == "number" and isinstance(value, str):
|
|
321
|
+
return True, float(value)
|
|
322
|
+
if exp_type == "boolean" and isinstance(value, str):
|
|
323
|
+
if value.lower() in ("true", "1", "yes"):
|
|
324
|
+
return True, True
|
|
325
|
+
if value.lower() in ("false", "0", "no"):
|
|
326
|
+
return True, False
|
|
327
|
+
if exp_type == "string" and not isinstance(value, str):
|
|
328
|
+
return True, str(value)
|
|
329
|
+
except (ValueError, TypeError):
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
return False, value
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def tool(fn: Callable = None) -> Callable:
|
|
336
|
+
"""
|
|
337
|
+
Decorator to register tool functions with robust validation.
|
|
338
|
+
|
|
339
|
+
Validates at decorator-time:
|
|
340
|
+
- Function has a non-empty docstring
|
|
341
|
+
- No *args/**kwargs in signature
|
|
342
|
+
- All parameters have type hints
|
|
343
|
+
- Return type is declared and JSON-serializable
|
|
344
|
+
|
|
345
|
+
Both sync and async functions are supported. Execution strategy
|
|
346
|
+
is determined automatically at runtime.
|
|
347
|
+
|
|
348
|
+
Usage:
|
|
349
|
+
@tool
|
|
350
|
+
def web_search(query: str) -> dict:
|
|
351
|
+
'''Search the web.'''
|
|
352
|
+
return requests.get(...).json()
|
|
353
|
+
|
|
354
|
+
@tool
|
|
355
|
+
def calculator(expr: str) -> float:
|
|
356
|
+
'''Evaluate math expression.'''
|
|
357
|
+
return eval(expr)
|
|
358
|
+
|
|
359
|
+
Supported parameter types:
|
|
360
|
+
- Primitives: str, int, float, bool
|
|
361
|
+
- Collections: list, List[T], dict, Dict[str, T]
|
|
362
|
+
- Optional: Optional[T], T | None
|
|
363
|
+
- Literal: Literal["option1", "option2"]
|
|
364
|
+
- Union: Union[str, int]
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def decorator(func: Callable) -> Callable:
|
|
368
|
+
# 1. Validate docstring (must be non-empty)
|
|
369
|
+
doc = (func.__doc__ or "").strip()
|
|
370
|
+
if not doc:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"Tool '{func.__name__}' must have a non-empty docstring. "
|
|
373
|
+
"Add a docstring to the function."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# 2. Validate return type
|
|
377
|
+
try:
|
|
378
|
+
_validate_return_type(func)
|
|
379
|
+
except TypeError as e:
|
|
380
|
+
raise TypeError(f"Tool registration failed: {e}") from e
|
|
381
|
+
|
|
382
|
+
# 3. Infer and validate schema (checks type hints, no *args/**kwargs)
|
|
383
|
+
try:
|
|
384
|
+
schema = _infer_schema(func)
|
|
385
|
+
except TypeError as e:
|
|
386
|
+
raise TypeError(f"Tool registration failed: {e}") from e
|
|
387
|
+
|
|
388
|
+
# 4. Warn if overwriting existing tool
|
|
389
|
+
if func.__name__ in TOOL_REGISTRY:
|
|
390
|
+
import warnings
|
|
391
|
+
|
|
392
|
+
warnings.warn(
|
|
393
|
+
f"Tool '{func.__name__}' is being overwritten in the registry.",
|
|
394
|
+
UserWarning,
|
|
395
|
+
stacklevel=2,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# 5. Attach metadata and register
|
|
399
|
+
func._is_tool = True
|
|
400
|
+
func._tool_name = func.__name__
|
|
401
|
+
func._schema = schema
|
|
402
|
+
func._description = doc # Already validated and stripped above
|
|
403
|
+
|
|
404
|
+
TOOL_REGISTRY[func._tool_name] = func
|
|
405
|
+
|
|
406
|
+
return func
|
|
407
|
+
|
|
408
|
+
# Support both @tool and @tool()
|
|
409
|
+
return decorator(fn) if fn else decorator
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def validate_tools_file(filepath) -> list:
|
|
413
|
+
"""
|
|
414
|
+
Validate an env.py file without executing it.
|
|
415
|
+
|
|
416
|
+
Parses the AST to find @tool decorated functions and checks:
|
|
417
|
+
- Function has a non-empty docstring
|
|
418
|
+
- No *args/**kwargs
|
|
419
|
+
- Has type annotations for all parameters
|
|
420
|
+
- Has a return type annotation
|
|
421
|
+
|
|
422
|
+
Both sync and async functions are supported.
|
|
423
|
+
|
|
424
|
+
Returns a list of error messages (empty if valid).
|
|
425
|
+
"""
|
|
426
|
+
import ast
|
|
427
|
+
from pathlib import Path
|
|
428
|
+
|
|
429
|
+
errors = []
|
|
430
|
+
filepath = Path(filepath)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
source = filepath.read_text()
|
|
434
|
+
tree = ast.parse(source, filename=str(filepath))
|
|
435
|
+
except SyntaxError as e:
|
|
436
|
+
return [f"Syntax error in {filepath.name}: {e}"]
|
|
437
|
+
|
|
438
|
+
for node in ast.walk(tree):
|
|
439
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
440
|
+
# Check if function has @tool decorator
|
|
441
|
+
is_tool = False
|
|
442
|
+
for decorator in node.decorator_list:
|
|
443
|
+
if (
|
|
444
|
+
isinstance(decorator, ast.Name)
|
|
445
|
+
and decorator.id == "tool"
|
|
446
|
+
or (
|
|
447
|
+
isinstance(decorator, ast.Call)
|
|
448
|
+
and isinstance(decorator.func, ast.Name)
|
|
449
|
+
and decorator.func.id == "tool"
|
|
450
|
+
)
|
|
451
|
+
):
|
|
452
|
+
is_tool = True
|
|
453
|
+
|
|
454
|
+
if is_tool:
|
|
455
|
+
# Both async and sync functions are allowed
|
|
456
|
+
|
|
457
|
+
# Check for docstring using ast.get_docstring (canonical way)
|
|
458
|
+
doc = ast.get_docstring(node)
|
|
459
|
+
if not (doc or "").strip():
|
|
460
|
+
errors.append(
|
|
461
|
+
f"Tool '{node.name}' must have a non-empty docstring. "
|
|
462
|
+
"Add a docstring to describe what the tool does and its arguments."
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Check for *args/**kwargs (not allowed in tools)
|
|
466
|
+
if node.args.vararg:
|
|
467
|
+
errors.append(
|
|
468
|
+
f"Tool '{node.name}' cannot use *{node.args.vararg.arg}. "
|
|
469
|
+
"Define explicit, typed parameters instead."
|
|
470
|
+
)
|
|
471
|
+
if node.args.kwarg:
|
|
472
|
+
errors.append(
|
|
473
|
+
f"Tool '{node.name}' cannot use **{node.args.kwarg.arg}. "
|
|
474
|
+
"Define explicit, typed parameters instead."
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Check return type annotation
|
|
478
|
+
if node.returns is None:
|
|
479
|
+
errors.append(
|
|
480
|
+
f"Tool '{node.name}' must have a return type annotation. "
|
|
481
|
+
"Add '-> ReturnType' to the function signature."
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Check parameter type annotations (args + kwonlyargs, skip 'self' and 'cls')
|
|
485
|
+
all_args = list(node.args.args) + list(node.args.kwonlyargs)
|
|
486
|
+
for arg in all_args:
|
|
487
|
+
if arg.arg in ("self", "cls"):
|
|
488
|
+
continue
|
|
489
|
+
if arg.annotation is None:
|
|
490
|
+
errors.append(
|
|
491
|
+
f"Tool '{node.name}': parameter '{arg.arg}' must have a type annotation."
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
return errors
|