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.
Files changed (56) hide show
  1. rnow/__init__.py +5 -0
  2. rnow/__main__.py +7 -0
  3. rnow/cli/__init__.py +6 -0
  4. rnow/cli/auth.py +67 -0
  5. rnow/cli/blob.py +98 -0
  6. rnow/cli/commands.py +2311 -0
  7. rnow/cli/common.py +28 -0
  8. rnow/cli/cube.py +255 -0
  9. rnow/cli/main.py +49 -0
  10. rnow/cli/test.py +728 -0
  11. rnow/cli/token_count.py +295 -0
  12. rnow/core/__init__.py +33 -0
  13. rnow/core/reward.py +333 -0
  14. rnow/core/tool.py +494 -0
  15. rnow/models.py +295 -0
  16. rnow/templates/deepseek-aha/config.yml +26 -0
  17. rnow/templates/deepseek-aha/rewards.py +36 -0
  18. rnow/templates/deepseek-aha/train.jsonl +1000 -0
  19. rnow/templates/mcp-tavily/config.yml +29 -0
  20. rnow/templates/mcp-tavily/requirements.txt +1 -0
  21. rnow/templates/mcp-tavily/rewards.py +25 -0
  22. rnow/templates/mcp-tavily/train.jsonl +500 -0
  23. rnow/templates/new/config.yml +26 -0
  24. rnow/templates/new/requirements.txt +1 -0
  25. rnow/templates/new/rewards.py +0 -0
  26. rnow/templates/new/train.jsonl +0 -0
  27. rnow/templates/rl-nextjs/config.yml +27 -0
  28. rnow/templates/rl-nextjs/requirements.txt +2 -0
  29. rnow/templates/rl-nextjs/rewards.py +446 -0
  30. rnow/templates/rl-nextjs/train.jsonl +1000 -0
  31. rnow/templates/rl-single/config.yml +27 -0
  32. rnow/templates/rl-single/requirements.txt +1 -0
  33. rnow/templates/rl-single/rewards.py +14 -0
  34. rnow/templates/rl-single/train.jsonl +1000 -0
  35. rnow/templates/rl-tools/config.yml +27 -0
  36. rnow/templates/rl-tools/env.py +38 -0
  37. rnow/templates/rl-tools/requirements.txt +3 -0
  38. rnow/templates/rl-tools/rewards.py +25 -0
  39. rnow/templates/rl-tools/train.jsonl +500 -0
  40. rnow/templates/sft/config.yml +20 -0
  41. rnow/templates/sft/train.jsonl +100 -0
  42. rnow/templates/tutorial-reward/config.yml +27 -0
  43. rnow/templates/tutorial-reward/requirements.txt +1 -0
  44. rnow/templates/tutorial-reward/rewards.py +15 -0
  45. rnow/templates/tutorial-reward/train.jsonl +1000 -0
  46. rnow/templates/tutorial-tool/config.yml +27 -0
  47. rnow/templates/tutorial-tool/env.py +7 -0
  48. rnow/templates/tutorial-tool/requirements.txt +3 -0
  49. rnow/templates/tutorial-tool/rewards.py +7 -0
  50. rnow/templates/tutorial-tool/train.jsonl +1266 -0
  51. rnow-0.2.4.dist-info/METADATA +135 -0
  52. rnow-0.2.4.dist-info/RECORD +56 -0
  53. rnow-0.2.4.dist-info/WHEEL +5 -0
  54. rnow-0.2.4.dist-info/entry_points.txt +2 -0
  55. rnow-0.2.4.dist-info/licenses/LICENSE +21 -0
  56. 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