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.
Files changed (67) hide show
  1. {rnow-0.3.1/rnow.egg-info → rnow-0.3.12}/PKG-INFO +3 -1
  2. {rnow-0.3.1 → rnow-0.3.12}/pyproject.toml +6 -3
  3. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/commands.py +209 -83
  4. rnow-0.3.12/rnow/cli/test.py +856 -0
  5. {rnow-0.3.1 → rnow-0.3.12}/rnow/core/__init__.py +4 -1
  6. {rnow-0.3.1 → rnow-0.3.12}/rnow/core/reward.py +34 -3
  7. {rnow-0.3.1 → rnow-0.3.12}/rnow/core/tool.py +29 -7
  8. {rnow-0.3.1 → rnow-0.3.12}/rnow/models.py +57 -1
  9. rnow-0.3.12/rnow/templates/finqa/README.md +60 -0
  10. rnow-0.3.12/rnow/templates/finqa/config.yml +23 -0
  11. rnow-0.3.12/rnow/templates/finqa/rewards.py +34 -0
  12. rnow-0.3.12/rnow/templates/finqa/train.jsonl +6251 -0
  13. rnow-0.3.12/rnow/templates/tutorial-reward/requirements.txt +1 -0
  14. {rnow-0.3.1 → rnow-0.3.12/rnow.egg-info}/PKG-INFO +3 -1
  15. {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/SOURCES.txt +7 -2
  16. {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/requires.txt +2 -0
  17. rnow-0.3.1/rnow/cli/test.py +0 -712
  18. {rnow-0.3.1 → rnow-0.3.12}/LICENSE +0 -0
  19. {rnow-0.3.1 → rnow-0.3.12}/README.md +0 -0
  20. {rnow-0.3.1 → rnow-0.3.12}/rnow/__init__.py +0 -0
  21. {rnow-0.3.1 → rnow-0.3.12}/rnow/__main__.py +0 -0
  22. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/__init__.py +0 -0
  23. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/auth.py +0 -0
  24. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/blob.py +0 -0
  25. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/common.py +0 -0
  26. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/cube.py +0 -0
  27. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/main.py +0 -0
  28. {rnow-0.3.1 → rnow-0.3.12}/rnow/cli/token_count.py +0 -0
  29. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/config.yml +0 -0
  30. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/rewards.py +0 -0
  31. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/deepseek-aha/train.jsonl +0 -0
  32. {rnow-0.3.1/rnow/templates/rl-single → rnow-0.3.12/rnow/templates/finqa}/requirements.txt +0 -0
  33. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/config.yml +0 -0
  34. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/requirements.txt +0 -0
  35. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/rewards.py +0 -0
  36. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/mcp-tavily/train.jsonl +0 -0
  37. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/config.yml +0 -0
  38. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/requirements.txt +0 -0
  39. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/rewards.py +0 -0
  40. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/new/train.jsonl +0 -0
  41. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/config.yml +0 -0
  42. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/requirements.txt +0 -0
  43. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/rewards.py +0 -0
  44. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-nextjs/train.jsonl +0 -0
  45. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/config.yml +0 -0
  46. {rnow-0.3.1/rnow/templates/tutorial-reward → rnow-0.3.12/rnow/templates/rl-single}/requirements.txt +0 -0
  47. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/rewards.py +0 -0
  48. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-single/train.jsonl +0 -0
  49. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/config.yml +0 -0
  50. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/requirements.txt +0 -0
  51. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/rewards.py +0 -0
  52. /rnow-0.3.1/rnow/templates/rl-tools/env.py → /rnow-0.3.12/rnow/templates/rl-tools/tools.py +0 -0
  53. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/rl-tools/train.jsonl +0 -0
  54. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/sft/config.yml +0 -0
  55. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/sft/train.jsonl +0 -0
  56. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/config.yml +0 -0
  57. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/rewards.py +0 -0
  58. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-reward/train.jsonl +0 -0
  59. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/config.yml +0 -0
  60. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/requirements.txt +0 -0
  61. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/rewards.py +0 -0
  62. /rnow-0.3.1/rnow/templates/tutorial-tool/env.py → /rnow-0.3.12/rnow/templates/tutorial-tool/tools.py +0 -0
  63. {rnow-0.3.1 → rnow-0.3.12}/rnow/templates/tutorial-tool/train.jsonl +0 -0
  64. {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/dependency_links.txt +0 -0
  65. {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/entry_points.txt +0 -0
  66. {rnow-0.3.1 → rnow-0.3.12}/rnow.egg-info/top_level.txt +0 -0
  67. {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.1
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.1"
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 = ["tinker-cookbook>=0.1.0"]
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 get_tools_from_env_py(env_path: Path) -> list[dict]:
224
+ def get_tools_from_tools_py(tools_path: Path) -> list[dict]:
225
225
  """
226
- Extract tool definitions from env.py as structured data.
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 env_path.exists():
231
+ if not tools_path.exists():
232
232
  return []
233
233
 
234
234
  try:
235
- source = env_path.read_text()
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 by sampling first N lines.
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
- # Check it's a dict
409
- if not isinstance(record, dict):
410
- errors.append(
411
- f"Line {line_num}: Expected JSON object, got {type(record).__name__}"
412
- )
413
- continue
414
-
415
- # Check for required 'messages' field
416
- if "messages" not in record:
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
- "env.py",
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('env.py', underline=True)} with your reward and tool functions"
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 env.py
1382
- env_path = dir / "env.py"
1383
- env_tools = get_tools_from_env_py(env_path)
1384
- all_tools.extend(env_tools)
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 = {"env.py": dir / "env.py", "requirements.txt": dir / "requirements.txt"}
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 env.py
1811
- env_path = dir / "env.py"
1812
- env_tools = get_tools_from_env_py(env_path)
1813
- all_tools.extend(env_tools)
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 env.py if present (check for docstrings on @tool functions)
1929
- env_path = dir / "env.py"
1930
- has_env_py = env_path.exists() and env_path.stat().st_size > 0
1931
- if has_env_py:
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(env_path)
2046
+ errors = validate_tools_file(tools_path)
1936
2047
  if errors:
1937
- click.echo(click.style("✗ Invalid env.py:", fg="red", bold=True))
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 env.py before submitting")
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 = has_env_py or has_mcp_url
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 env.py and mcp_url from your project")
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 has_env_py and has_mcp_url:
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 env.py tools"
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 has_env_py:
1981
- click.echo(click.style("Tools: ", fg=TEAL_RGB) + "Using env.py tools")
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
- "env.py": dir / "env.py",
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()