rnow 0.3.1__tar.gz → 0.3.12__tar.gz
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-0.3.1/rnow.egg-info → rnow-0.3.12}/PKG-INFO +3 -1
- {rnow-0.3.1 → rnow-0.3.12}/pyproject.toml +6 -3
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/commands.py +209 -83
- rnow-0.3.12/rnow/cli/test.py +856 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/core/__init__.py +4 -1
- {rnow-0.3.1 → rnow-0.3.12}/rnow/core/reward.py +34 -3
- {rnow-0.3.1 → rnow-0.3.12}/rnow/core/tool.py +29 -7
- {rnow-0.3.1 → rnow-0.3.12}/rnow/models.py +57 -1
- rnow-0.3.12/rnow/templates/finqa/README.md +60 -0
- rnow-0.3.12/rnow/templates/finqa/config.yml +23 -0
- rnow-0.3.12/rnow/templates/finqa/rewards.py +34 -0
- rnow-0.3.12/rnow/templates/finqa/train.jsonl +6251 -0
- rnow-0.3.12/rnow/templates/tutorial-reward/requirements.txt +1 -0
- {rnow-0.3.1 → rnow-0.3.12/rnow.egg-info}/PKG-INFO +3 -1
- {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/SOURCES.txt +7 -2
- {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/requires.txt +2 -0
- rnow-0.3.1/rnow/cli/test.py +0 -712
- {rnow-0.3.1 → rnow-0.3.12}/LICENSE +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/README.md +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/__init__.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/__main__.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/__init__.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/auth.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/blob.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/common.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/cube.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/main.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/token_count.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/train.jsonl +0 -0
- {rnow-0.3.1/rnow/templates/rl-single → rnow-0.3.12/rnow/templates/finqa}/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/config.yml +0 -0
- {rnow-0.3.1/rnow/templates/tutorial-reward → rnow-0.3.12/rnow/templates/rl-single}/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/rewards.py +0 -0
- /rnow-0.3.1/rnow/templates/rl-tools/env.py → /rnow-0.3.12/rnow/templates/rl-tools/tools.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/sft/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/sft/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/rewards.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/config.yml +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/requirements.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/rewards.py +0 -0
- /rnow-0.3.1/rnow/templates/tutorial-tool/env.py → /rnow-0.3.12/rnow/templates/tutorial-tool/tools.py +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/train.jsonl +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/dependency_links.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/entry_points.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/top_level.txt +0 -0
- {rnow-0.3.1 → rnow-0.3.12}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rnow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: ReinforceNow CLI - Reinforcement Learning platform command-line interface
|
|
5
5
|
Requires-Python: <3.15,>=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -16,6 +16,7 @@ Requires-Dist: tokenizers>=0.15.0
|
|
|
16
16
|
Requires-Dist: openai-harmony>=0.0.8
|
|
17
17
|
Provides-Extra: test
|
|
18
18
|
Requires-Dist: tinker-cookbook>=0.1.0; extra == "test"
|
|
19
|
+
Requires-Dist: transformers>=4.40.0; extra == "test"
|
|
19
20
|
Provides-Extra: api
|
|
20
21
|
Requires-Dist: fastapi>=0.68.0; extra == "api"
|
|
21
22
|
Requires-Dist: uvicorn>=0.15.0; extra == "api"
|
|
@@ -23,6 +24,7 @@ Provides-Extra: mcp
|
|
|
23
24
|
Requires-Dist: fastmcp>=0.1.0; extra == "mcp"
|
|
24
25
|
Provides-Extra: all
|
|
25
26
|
Requires-Dist: tinker-cookbook>=0.1.0; extra == "all"
|
|
27
|
+
Requires-Dist: transformers>=4.40.0; extra == "all"
|
|
26
28
|
Requires-Dist: fastapi>=0.68.0; extra == "all"
|
|
27
29
|
Requires-Dist: uvicorn>=0.15.0; extra == "all"
|
|
28
30
|
Requires-Dist: fastmcp>=0.1.0; extra == "all"
|
|
@@ -11,7 +11,7 @@ rnow = ["templates/**/*"]
|
|
|
11
11
|
|
|
12
12
|
[project]
|
|
13
13
|
name = "rnow"
|
|
14
|
-
version = "0.3.
|
|
14
|
+
version = "0.3.12"
|
|
15
15
|
description = "ReinforceNow CLI - Reinforcement Learning platform command-line interface"
|
|
16
16
|
readme = "README.md"
|
|
17
17
|
requires-python = ">=3.10,<3.15"
|
|
@@ -31,13 +31,16 @@ dependencies = [
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
# Local testing with ML inference (requires torch)
|
|
34
|
-
test = [
|
|
34
|
+
test = [
|
|
35
|
+
"tinker-cookbook>=0.1.0",
|
|
36
|
+
"transformers>=4.40.0", # Ensure modern version (2.x is incompatible)
|
|
37
|
+
]
|
|
35
38
|
# API server mode
|
|
36
39
|
api = ["fastapi>=0.68.0", "uvicorn>=0.15.0"]
|
|
37
40
|
# MCP server support (for fetching tool schemas)
|
|
38
41
|
mcp = ["fastmcp>=0.1.0"]
|
|
39
42
|
# All optional features
|
|
40
|
-
all = ["tinker-cookbook>=0.1.0", "fastapi>=0.68.0", "uvicorn>=0.15.0", "fastmcp>=0.1.0"]
|
|
43
|
+
all = ["tinker-cookbook>=0.1.0", "transformers>=4.40.0", "fastapi>=0.68.0", "uvicorn>=0.15.0", "fastmcp>=0.1.0"]
|
|
41
44
|
|
|
42
45
|
[project.scripts]
|
|
43
46
|
rnow = "rnow.cli.main:main"
|
|
@@ -221,18 +221,18 @@ def validate_max_tokens_for_context(
|
|
|
221
221
|
return None, available
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
def
|
|
224
|
+
def get_tools_from_tools_py(tools_path: Path) -> list[dict]:
|
|
225
225
|
"""
|
|
226
|
-
Extract tool definitions from
|
|
226
|
+
Extract tool definitions from tools.py as structured data.
|
|
227
227
|
Returns list of tool dicts with name, description, and schema.
|
|
228
228
|
"""
|
|
229
229
|
import ast
|
|
230
230
|
|
|
231
|
-
if not
|
|
231
|
+
if not tools_path.exists():
|
|
232
232
|
return []
|
|
233
233
|
|
|
234
234
|
try:
|
|
235
|
-
source =
|
|
235
|
+
source = tools_path.read_text()
|
|
236
236
|
tree = ast.parse(source)
|
|
237
237
|
except (SyntaxError, OSError):
|
|
238
238
|
return []
|
|
@@ -377,25 +377,124 @@ def fetch_mcp_tool_schemas(
|
|
|
377
377
|
return all_tools, None
|
|
378
378
|
|
|
379
379
|
|
|
380
|
+
def get_sandbox_names_from_file(filepath: Path, decorator_name: str) -> set[str]:
|
|
381
|
+
"""
|
|
382
|
+
Extract function names with sandbox=True from a file.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
filepath: Path to rewards.py or tools.py
|
|
386
|
+
decorator_name: "reward" or "tool"
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Set of function names that have sandbox=True
|
|
390
|
+
"""
|
|
391
|
+
import ast
|
|
392
|
+
|
|
393
|
+
names = set()
|
|
394
|
+
if not filepath.exists():
|
|
395
|
+
return names
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
source = filepath.read_text()
|
|
399
|
+
tree = ast.parse(source, filename=str(filepath))
|
|
400
|
+
except SyntaxError:
|
|
401
|
+
return names
|
|
402
|
+
|
|
403
|
+
for node in ast.walk(tree):
|
|
404
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
405
|
+
for decorator in node.decorator_list:
|
|
406
|
+
# Check for @decorator_name(sandbox=True)
|
|
407
|
+
if (
|
|
408
|
+
isinstance(decorator, ast.Call)
|
|
409
|
+
and isinstance(decorator.func, ast.Name)
|
|
410
|
+
and decorator.func.id == decorator_name
|
|
411
|
+
):
|
|
412
|
+
for kw in decorator.keywords:
|
|
413
|
+
if kw.arg == "sandbox":
|
|
414
|
+
# Check if value is True
|
|
415
|
+
if isinstance(kw.value, ast.Constant) and kw.value.value is True:
|
|
416
|
+
names.add(node.name)
|
|
417
|
+
elif isinstance(kw.value, ast.NameConstant) and kw.value.value is True:
|
|
418
|
+
names.add(node.name) # Python 3.7 compat
|
|
419
|
+
return names
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def validate_sandbox_docker_requirement(
|
|
423
|
+
train_jsonl_path: Path, rewards_py_path: Path, tools_py_path: Path
|
|
424
|
+
) -> list[str]:
|
|
425
|
+
"""
|
|
426
|
+
Validate that entries using sandbox=True tools/rewards have a docker field.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
List of error messages (empty if valid)
|
|
430
|
+
"""
|
|
431
|
+
errors = []
|
|
432
|
+
|
|
433
|
+
# Get sandbox function names
|
|
434
|
+
sandbox_rewards = get_sandbox_names_from_file(rewards_py_path, "reward")
|
|
435
|
+
sandbox_tools = get_sandbox_names_from_file(tools_py_path, "tool")
|
|
436
|
+
|
|
437
|
+
if not sandbox_rewards and not sandbox_tools:
|
|
438
|
+
return [] # No sandbox functions, nothing to validate
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
with open(train_jsonl_path, encoding="utf-8") as f:
|
|
442
|
+
for line_num, line in enumerate(f, start=1):
|
|
443
|
+
stripped = line.strip()
|
|
444
|
+
if not stripped:
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
record = json.loads(stripped)
|
|
449
|
+
except json.JSONDecodeError:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
# Check if entry references sandbox rewards/tools
|
|
453
|
+
entry_rewards = set(record.get("rewards", []))
|
|
454
|
+
entry_tools = set(record.get("tools", []))
|
|
455
|
+
|
|
456
|
+
uses_sandbox_reward = bool(sandbox_rewards & entry_rewards)
|
|
457
|
+
uses_sandbox_tool = bool(sandbox_tools & entry_tools)
|
|
458
|
+
|
|
459
|
+
if (uses_sandbox_reward or uses_sandbox_tool) and not record.get("docker"):
|
|
460
|
+
used = []
|
|
461
|
+
if uses_sandbox_reward:
|
|
462
|
+
used.extend(f"reward:{r}" for r in sandbox_rewards & entry_rewards)
|
|
463
|
+
if uses_sandbox_tool:
|
|
464
|
+
used.extend(f"tool:{t}" for t in sandbox_tools & entry_tools)
|
|
465
|
+
errors.append(
|
|
466
|
+
f"Line {line_num}: Uses sandbox functions ({', '.join(used)}) but missing 'docker' field"
|
|
467
|
+
)
|
|
468
|
+
if len(errors) >= 5:
|
|
469
|
+
errors.append("... (stopping after 5 errors)")
|
|
470
|
+
return errors
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
errors.append(f"Failed to validate sandbox requirements: {e}")
|
|
474
|
+
|
|
475
|
+
return errors
|
|
476
|
+
|
|
477
|
+
|
|
380
478
|
def validate_train_jsonl(
|
|
381
479
|
path: Path, dataset_type: models.DatasetType, sample_size: int = 50
|
|
382
480
|
) -> list[str]:
|
|
383
481
|
"""
|
|
384
|
-
Validate train.jsonl format
|
|
482
|
+
Validate train.jsonl format using Pydantic models.
|
|
385
483
|
Returns a list of error messages (empty if valid).
|
|
386
484
|
"""
|
|
485
|
+
from pydantic import ValidationError
|
|
486
|
+
|
|
387
487
|
errors = []
|
|
488
|
+
EntryModel = models.TrainEntryRL if dataset_type == models.DatasetType.RL else models.TrainEntry
|
|
388
489
|
|
|
389
490
|
try:
|
|
390
491
|
with open(path, encoding="utf-8") as f:
|
|
391
492
|
lines_checked = 0
|
|
392
493
|
for line_num, line in enumerate(f, start=1):
|
|
393
|
-
# Skip empty lines
|
|
394
494
|
stripped = line.strip()
|
|
395
495
|
if not stripped:
|
|
396
496
|
continue
|
|
397
497
|
|
|
398
|
-
# Try to parse as JSON
|
|
399
498
|
try:
|
|
400
499
|
record = json.loads(stripped)
|
|
401
500
|
except json.JSONDecodeError as e:
|
|
@@ -405,63 +504,21 @@ def validate_train_jsonl(
|
|
|
405
504
|
return errors
|
|
406
505
|
continue
|
|
407
506
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
errors.append(f"Line {line_num}: Missing required 'messages' field")
|
|
418
|
-
continue
|
|
419
|
-
|
|
420
|
-
messages = record["messages"]
|
|
421
|
-
if not isinstance(messages, list):
|
|
422
|
-
errors.append(f"Line {line_num}: 'messages' must be a list")
|
|
423
|
-
continue
|
|
424
|
-
|
|
425
|
-
if len(messages) == 0:
|
|
426
|
-
errors.append(f"Line {line_num}: 'messages' list is empty")
|
|
507
|
+
try:
|
|
508
|
+
EntryModel.model_validate(record)
|
|
509
|
+
except ValidationError as e:
|
|
510
|
+
for err in e.errors():
|
|
511
|
+
loc = ".".join(str(x) for x in err["loc"])
|
|
512
|
+
errors.append(f"Line {line_num}: {loc} - {err['msg']}")
|
|
513
|
+
if len(errors) >= 5:
|
|
514
|
+
errors.append("... (stopping after 5 errors)")
|
|
515
|
+
return errors
|
|
427
516
|
continue
|
|
428
517
|
|
|
429
|
-
# Check each message has role and content
|
|
430
|
-
for msg_idx, msg in enumerate(messages):
|
|
431
|
-
if not isinstance(msg, dict):
|
|
432
|
-
errors.append(f"Line {line_num}: Message {msg_idx + 1} must be an object")
|
|
433
|
-
break
|
|
434
|
-
if "role" not in msg:
|
|
435
|
-
errors.append(f"Line {line_num}: Message {msg_idx + 1} missing 'role'")
|
|
436
|
-
break
|
|
437
|
-
if "content" not in msg:
|
|
438
|
-
errors.append(f"Line {line_num}: Message {msg_idx + 1} missing 'content'")
|
|
439
|
-
break
|
|
440
|
-
if msg["role"] not in ("system", "user", "assistant"):
|
|
441
|
-
errors.append(
|
|
442
|
-
f"Line {line_num}: Message {msg_idx + 1} has invalid role '{msg['role']}' (expected: system, user, assistant)"
|
|
443
|
-
)
|
|
444
|
-
break
|
|
445
|
-
|
|
446
|
-
# For RL, check for rewards field
|
|
447
|
-
if dataset_type == models.DatasetType.RL and "rewards" not in record:
|
|
448
|
-
errors.append(
|
|
449
|
-
f"Line {line_num}: Missing required 'rewards' field for RL dataset"
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
# Validate optional 'tools' field if present
|
|
453
|
-
if "tools" in record:
|
|
454
|
-
tools = record["tools"]
|
|
455
|
-
if not isinstance(tools, list):
|
|
456
|
-
errors.append(f"Line {line_num}: 'tools' must be a list of tool names")
|
|
457
|
-
elif not all(isinstance(t, str) for t in tools):
|
|
458
|
-
errors.append(f"Line {line_num}: 'tools' must contain only strings")
|
|
459
|
-
|
|
460
518
|
lines_checked += 1
|
|
461
519
|
if lines_checked >= sample_size:
|
|
462
520
|
break
|
|
463
521
|
|
|
464
|
-
# Check if file was effectively empty (only whitespace)
|
|
465
522
|
if lines_checked == 0:
|
|
466
523
|
errors.append("File contains no valid JSON lines")
|
|
467
524
|
|
|
@@ -1030,8 +1087,10 @@ def orgs(ctx, org_id: str | None):
|
|
|
1030
1087
|
"rl-tools",
|
|
1031
1088
|
"mcp-tavily",
|
|
1032
1089
|
"deepseek-aha",
|
|
1090
|
+
"dcf-sec",
|
|
1033
1091
|
"tutorial-reward",
|
|
1034
1092
|
"tutorial-tool",
|
|
1093
|
+
"web-tasks",
|
|
1035
1094
|
]
|
|
1036
1095
|
),
|
|
1037
1096
|
default="start",
|
|
@@ -1069,6 +1128,8 @@ def init(template: str, name: str):
|
|
|
1069
1128
|
"tutorial-reward": "tutorial-reward",
|
|
1070
1129
|
"tutorial-tool": "tutorial-tool",
|
|
1071
1130
|
"deepseek-aha": "deepseek-aha",
|
|
1131
|
+
"dcf-sec": "dcf-sec-filings",
|
|
1132
|
+
"web-tasks": "web-tasks",
|
|
1072
1133
|
"new": "new-project",
|
|
1073
1134
|
"blank": "my-project",
|
|
1074
1135
|
}
|
|
@@ -1099,7 +1160,7 @@ def init(template: str, name: str):
|
|
|
1099
1160
|
"train.jsonl",
|
|
1100
1161
|
"rewards.py",
|
|
1101
1162
|
"requirements.txt",
|
|
1102
|
-
"
|
|
1163
|
+
"tools.py",
|
|
1103
1164
|
"README.md",
|
|
1104
1165
|
}
|
|
1105
1166
|
|
|
@@ -1213,7 +1274,7 @@ def init(template: str, name: str):
|
|
|
1213
1274
|
click.echo(click.style("Next steps:", bold=True))
|
|
1214
1275
|
click.echo(f" 1. Edit {click.style('train.jsonl', underline=True)} with your training data")
|
|
1215
1276
|
click.echo(
|
|
1216
|
-
f" 2. Edit {click.style('rewards.py', underline=True)} and {click.style('
|
|
1277
|
+
f" 2. Edit {click.style('rewards.py', underline=True)} and {click.style('tools.py', underline=True)} with your reward and tool functions"
|
|
1217
1278
|
)
|
|
1218
1279
|
click.echo(f" 3. Run {click.style('rnow run', fg=TEAL_RGB, bold=True)} to start training")
|
|
1219
1280
|
|
|
@@ -1351,6 +1412,15 @@ def _submit_single_run(
|
|
|
1351
1412
|
if jsonl_errors:
|
|
1352
1413
|
raise click.ClickException(f"Invalid train.jsonl: {jsonl_errors[0]}")
|
|
1353
1414
|
|
|
1415
|
+
# Validate sandbox=True functions require docker field in train.jsonl
|
|
1416
|
+
sandbox_errors = validate_sandbox_docker_requirement(
|
|
1417
|
+
train_jsonl_path,
|
|
1418
|
+
rewards_py_path=dir / "rewards.py",
|
|
1419
|
+
tools_py_path=dir / "tools.py",
|
|
1420
|
+
)
|
|
1421
|
+
if sandbox_errors:
|
|
1422
|
+
raise click.ClickException(f"Sandbox validation: {sandbox_errors[0]}")
|
|
1423
|
+
|
|
1354
1424
|
# Validate rewards.py if present
|
|
1355
1425
|
if config.dataset_type == models.DatasetType.RL:
|
|
1356
1426
|
rewards_path = dir / "rewards.py"
|
|
@@ -1378,10 +1448,10 @@ def _submit_single_run(
|
|
|
1378
1448
|
# Collect all tools
|
|
1379
1449
|
all_tools = []
|
|
1380
1450
|
|
|
1381
|
-
# Get tools from
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
all_tools.extend(
|
|
1451
|
+
# Get tools from tools.py
|
|
1452
|
+
tools_path = dir / "tools.py"
|
|
1453
|
+
tools_py_tools = get_tools_from_tools_py(tools_path)
|
|
1454
|
+
all_tools.extend(tools_py_tools)
|
|
1385
1455
|
|
|
1386
1456
|
# Fetch MCP tools
|
|
1387
1457
|
mcp_urls = config.rollout.mcp_url
|
|
@@ -1447,7 +1517,7 @@ def _submit_single_run(
|
|
|
1447
1517
|
)
|
|
1448
1518
|
|
|
1449
1519
|
# Add optional files
|
|
1450
|
-
optional_files = {"
|
|
1520
|
+
optional_files = {"tools.py": dir / "tools.py", "requirements.txt": dir / "requirements.txt"}
|
|
1451
1521
|
for file_name, path in optional_files.items():
|
|
1452
1522
|
if path.exists():
|
|
1453
1523
|
files.append(
|
|
@@ -1457,6 +1527,17 @@ def _submit_single_run(
|
|
|
1457
1527
|
)
|
|
1458
1528
|
)
|
|
1459
1529
|
|
|
1530
|
+
# Add Dockerfile.* files for local/ docker images
|
|
1531
|
+
for dockerfile_path in dir.glob("Dockerfile.*"):
|
|
1532
|
+
file_name = dockerfile_path.name
|
|
1533
|
+
click.echo(f" Found Dockerfile: {file_name}")
|
|
1534
|
+
files.append(
|
|
1535
|
+
(
|
|
1536
|
+
file_name.replace(".", "_"),
|
|
1537
|
+
(file_name, open(dockerfile_path, "rb"), "application/octet-stream"),
|
|
1538
|
+
)
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1460
1541
|
headers = auth.get_auth_headers()
|
|
1461
1542
|
headers.pop("Content-Type", None)
|
|
1462
1543
|
|
|
@@ -1732,6 +1813,36 @@ def run(
|
|
|
1732
1813
|
if not config.organization_id:
|
|
1733
1814
|
config.organization_id = get_active_organization()
|
|
1734
1815
|
|
|
1816
|
+
# Load secrets from .env file if it exists
|
|
1817
|
+
secret_values = {}
|
|
1818
|
+
env_file = dir / ".env"
|
|
1819
|
+
if env_file.exists():
|
|
1820
|
+
try:
|
|
1821
|
+
with open(env_file) as f:
|
|
1822
|
+
for line in f:
|
|
1823
|
+
line = line.strip()
|
|
1824
|
+
# Skip empty lines and comments
|
|
1825
|
+
if not line or line.startswith("#"):
|
|
1826
|
+
continue
|
|
1827
|
+
# Parse KEY=value format
|
|
1828
|
+
if "=" in line:
|
|
1829
|
+
key, _, value = line.partition("=")
|
|
1830
|
+
key = key.strip()
|
|
1831
|
+
value = value.strip()
|
|
1832
|
+
# Remove quotes if present
|
|
1833
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
1834
|
+
value.startswith("'") and value.endswith("'")
|
|
1835
|
+
):
|
|
1836
|
+
value = value[1:-1]
|
|
1837
|
+
secret_values[key] = value
|
|
1838
|
+
|
|
1839
|
+
if secret_values:
|
|
1840
|
+
click.echo(
|
|
1841
|
+
click.style(f"🔐 Loaded {len(secret_values)} secret(s) from .env", dim=True)
|
|
1842
|
+
)
|
|
1843
|
+
except Exception as e:
|
|
1844
|
+
click.echo(click.style(f"⚠️ Warning: Failed to read .env file: {e}", fg="yellow"))
|
|
1845
|
+
|
|
1735
1846
|
# Validate required files (all in the same directory now)
|
|
1736
1847
|
required_files = {
|
|
1737
1848
|
"train.jsonl": dir / "train.jsonl",
|
|
@@ -1807,10 +1918,10 @@ def run(
|
|
|
1807
1918
|
# Collect all tools
|
|
1808
1919
|
all_tools = []
|
|
1809
1920
|
|
|
1810
|
-
# Get tools from
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
all_tools.extend(
|
|
1921
|
+
# Get tools from tools.py
|
|
1922
|
+
tools_path = dir / "tools.py"
|
|
1923
|
+
tools_py_tools = get_tools_from_tools_py(tools_path)
|
|
1924
|
+
all_tools.extend(tools_py_tools)
|
|
1814
1925
|
|
|
1815
1926
|
# Fetch MCP tool schemas (with progress indicator)
|
|
1816
1927
|
mcp_urls = config.rollout.mcp_url if config.rollout else None
|
|
@@ -1925,19 +2036,19 @@ def run(
|
|
|
1925
2036
|
"Please ensure reward names in train.jsonl match functions in rewards.py"
|
|
1926
2037
|
)
|
|
1927
2038
|
|
|
1928
|
-
# Validate
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
if
|
|
2039
|
+
# Validate tools.py if present (check for docstrings on @tool functions)
|
|
2040
|
+
tools_path = dir / "tools.py"
|
|
2041
|
+
has_tools_py = tools_path.exists() and tools_path.stat().st_size > 0
|
|
2042
|
+
if has_tools_py:
|
|
1932
2043
|
try:
|
|
1933
2044
|
from rnow.core.tool import validate_tools_file
|
|
1934
2045
|
|
|
1935
|
-
errors = validate_tools_file(
|
|
2046
|
+
errors = validate_tools_file(tools_path)
|
|
1936
2047
|
if errors:
|
|
1937
|
-
click.echo(click.style("✗ Invalid
|
|
2048
|
+
click.echo(click.style("✗ Invalid tools.py:", fg="red", bold=True))
|
|
1938
2049
|
for err in errors:
|
|
1939
2050
|
click.echo(f" • {err}")
|
|
1940
|
-
raise click.ClickException("Please fix
|
|
2051
|
+
raise click.ClickException("Please fix tools.py before submitting")
|
|
1941
2052
|
except ImportError:
|
|
1942
2053
|
pass # Skip validation if module not available
|
|
1943
2054
|
|
|
@@ -1949,7 +2060,7 @@ def run(
|
|
|
1949
2060
|
mcp_url_count = len(mcp_url) if isinstance(mcp_url, list) else 1
|
|
1950
2061
|
|
|
1951
2062
|
# Validate tool support for the model
|
|
1952
|
-
has_tools =
|
|
2063
|
+
has_tools = has_tools_py or has_mcp_url
|
|
1953
2064
|
if has_tools and not models.supports_tool_calling(model_path):
|
|
1954
2065
|
click.echo()
|
|
1955
2066
|
click.echo(click.style("✗ Model does not support tool calling", fg="red", bold=True))
|
|
@@ -1961,7 +2072,7 @@ def run(
|
|
|
1961
2072
|
click.echo(" Base/non-instruct models use a format that doesn't support tools.")
|
|
1962
2073
|
click.echo()
|
|
1963
2074
|
click.echo(click.style(" Options:", bold=True))
|
|
1964
|
-
click.echo(" 1. Remove
|
|
2075
|
+
click.echo(" 1. Remove tools.py and mcp_url from your project")
|
|
1965
2076
|
click.echo(
|
|
1966
2077
|
" 2. Use a model that supports tools (e.g., Qwen/Qwen3-8B, meta-llama/Llama-3.1-8B-Instruct)"
|
|
1967
2078
|
)
|
|
@@ -1969,16 +2080,16 @@ def run(
|
|
|
1969
2080
|
raise click.ClickException("Model does not support tool calling")
|
|
1970
2081
|
|
|
1971
2082
|
# Show tool sources message
|
|
1972
|
-
if
|
|
2083
|
+
if has_tools_py and has_mcp_url:
|
|
1973
2084
|
server_text = f"{mcp_url_count} server(s)" if mcp_url_count > 1 else "1 server"
|
|
1974
2085
|
click.echo(
|
|
1975
|
-
click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text}) and
|
|
2086
|
+
click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text}) and tools.py tools"
|
|
1976
2087
|
)
|
|
1977
2088
|
elif has_mcp_url:
|
|
1978
2089
|
server_text = f"{mcp_url_count} server(s)" if mcp_url_count > 1 else "1 server"
|
|
1979
2090
|
click.echo(click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text})")
|
|
1980
|
-
elif
|
|
1981
|
-
click.echo(click.style("Tools: ", fg=TEAL_RGB) + "Using
|
|
2091
|
+
elif has_tools_py:
|
|
2092
|
+
click.echo(click.style("Tools: ", fg=TEAL_RGB) + "Using tools.py tools")
|
|
1982
2093
|
|
|
1983
2094
|
# Start cube spinner early
|
|
1984
2095
|
spinner = CubeSpinner()
|
|
@@ -2022,7 +2133,7 @@ def run(
|
|
|
2022
2133
|
|
|
2023
2134
|
# Add optional files (all in the same directory now)
|
|
2024
2135
|
optional_files = {
|
|
2025
|
-
"
|
|
2136
|
+
"tools.py": dir / "tools.py",
|
|
2026
2137
|
"requirements.txt": dir / "requirements.txt",
|
|
2027
2138
|
}
|
|
2028
2139
|
|
|
@@ -2035,6 +2146,17 @@ def run(
|
|
|
2035
2146
|
)
|
|
2036
2147
|
)
|
|
2037
2148
|
|
|
2149
|
+
# Add Dockerfile.* files for local/ docker images
|
|
2150
|
+
for dockerfile_path in dir.glob("Dockerfile.*"):
|
|
2151
|
+
file_name = dockerfile_path.name
|
|
2152
|
+
click.echo(f" Found Dockerfile: {file_name}")
|
|
2153
|
+
files.append(
|
|
2154
|
+
(
|
|
2155
|
+
file_name.replace(".", "_"),
|
|
2156
|
+
(file_name, open(dockerfile_path, "rb"), "application/octet-stream"),
|
|
2157
|
+
)
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2038
2160
|
# For multipart, we need to omit Content-Type so requests sets the boundary
|
|
2039
2161
|
headers = auth.get_auth_headers()
|
|
2040
2162
|
headers.pop("Content-Type", None)
|
|
@@ -2056,6 +2178,10 @@ def run(
|
|
|
2056
2178
|
if debug:
|
|
2057
2179
|
submit_data["debug"] = "true"
|
|
2058
2180
|
|
|
2181
|
+
# Add secrets if provided (sent as JSON string)
|
|
2182
|
+
if secret_values:
|
|
2183
|
+
submit_data["secrets"] = json.dumps(secret_values)
|
|
2184
|
+
|
|
2059
2185
|
# Start cube spinner if not already running (for small files)
|
|
2060
2186
|
if not spinner.running:
|
|
2061
2187
|
spinner.start()
|