rnow 0.2.4__py3-none-any.whl → 0.3.9__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/cli/commands.py +226 -84
- rnow/cli/test.py +536 -441
- rnow/core/__init__.py +4 -1
- rnow/core/reward.py +34 -3
- rnow/core/tool.py +29 -7
- rnow/models.py +88 -6
- rnow/templates/deepseek-aha/config.yml +1 -1
- rnow/templates/mcp-tavily/config.yml +1 -1
- rnow/templates/rl-single/config.yml +7 -7
- rnow/templates/rl-single/train.jsonl +0 -908
- rnow/templates/rl-tools/config.yml +1 -1
- rnow/templates/tutorial-reward/config.yml +7 -7
- rnow/templates/tutorial-reward/train.jsonl +0 -908
- rnow/templates/tutorial-tool/config.yml +1 -1
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/METADATA +23 -9
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/RECORD +22 -22
- /rnow/templates/rl-tools/{env.py → tools.py} +0 -0
- /rnow/templates/tutorial-tool/{env.py → tools.py} +0 -0
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/WHEEL +0 -0
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/entry_points.txt +0 -0
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {rnow-0.2.4.dist-info → rnow-0.3.9.dist-info}/top_level.txt +0 -0
rnow/cli/commands.py
CHANGED
|
@@ -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
|
|
|
@@ -1032,6 +1089,7 @@ def orgs(ctx, org_id: str | None):
|
|
|
1032
1089
|
"deepseek-aha",
|
|
1033
1090
|
"tutorial-reward",
|
|
1034
1091
|
"tutorial-tool",
|
|
1092
|
+
"web-tasks",
|
|
1035
1093
|
]
|
|
1036
1094
|
),
|
|
1037
1095
|
default="start",
|
|
@@ -1069,6 +1127,7 @@ def init(template: str, name: str):
|
|
|
1069
1127
|
"tutorial-reward": "tutorial-reward",
|
|
1070
1128
|
"tutorial-tool": "tutorial-tool",
|
|
1071
1129
|
"deepseek-aha": "deepseek-aha",
|
|
1130
|
+
"web-tasks": "web-tasks",
|
|
1072
1131
|
"new": "new-project",
|
|
1073
1132
|
"blank": "my-project",
|
|
1074
1133
|
}
|
|
@@ -1099,7 +1158,7 @@ def init(template: str, name: str):
|
|
|
1099
1158
|
"train.jsonl",
|
|
1100
1159
|
"rewards.py",
|
|
1101
1160
|
"requirements.txt",
|
|
1102
|
-
"
|
|
1161
|
+
"tools.py",
|
|
1103
1162
|
"README.md",
|
|
1104
1163
|
}
|
|
1105
1164
|
|
|
@@ -1213,7 +1272,7 @@ def init(template: str, name: str):
|
|
|
1213
1272
|
click.echo(click.style("Next steps:", bold=True))
|
|
1214
1273
|
click.echo(f" 1. Edit {click.style('train.jsonl', underline=True)} with your training data")
|
|
1215
1274
|
click.echo(
|
|
1216
|
-
f" 2. Edit {click.style('rewards.py', underline=True)} and {click.style('
|
|
1275
|
+
f" 2. Edit {click.style('rewards.py', underline=True)} and {click.style('tools.py', underline=True)} with your reward and tool functions"
|
|
1217
1276
|
)
|
|
1218
1277
|
click.echo(f" 3. Run {click.style('rnow run', fg=TEAL_RGB, bold=True)} to start training")
|
|
1219
1278
|
|
|
@@ -1351,6 +1410,15 @@ def _submit_single_run(
|
|
|
1351
1410
|
if jsonl_errors:
|
|
1352
1411
|
raise click.ClickException(f"Invalid train.jsonl: {jsonl_errors[0]}")
|
|
1353
1412
|
|
|
1413
|
+
# Validate sandbox=True functions require docker field in train.jsonl
|
|
1414
|
+
sandbox_errors = validate_sandbox_docker_requirement(
|
|
1415
|
+
train_jsonl_path,
|
|
1416
|
+
rewards_py_path=dir / "rewards.py",
|
|
1417
|
+
tools_py_path=dir / "tools.py",
|
|
1418
|
+
)
|
|
1419
|
+
if sandbox_errors:
|
|
1420
|
+
raise click.ClickException(f"Sandbox validation: {sandbox_errors[0]}")
|
|
1421
|
+
|
|
1354
1422
|
# Validate rewards.py if present
|
|
1355
1423
|
if config.dataset_type == models.DatasetType.RL:
|
|
1356
1424
|
rewards_path = dir / "rewards.py"
|
|
@@ -1378,10 +1446,10 @@ def _submit_single_run(
|
|
|
1378
1446
|
# Collect all tools
|
|
1379
1447
|
all_tools = []
|
|
1380
1448
|
|
|
1381
|
-
# Get tools from
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
all_tools.extend(
|
|
1449
|
+
# Get tools from tools.py
|
|
1450
|
+
tools_path = dir / "tools.py"
|
|
1451
|
+
tools_py_tools = get_tools_from_tools_py(tools_path)
|
|
1452
|
+
all_tools.extend(tools_py_tools)
|
|
1385
1453
|
|
|
1386
1454
|
# Fetch MCP tools
|
|
1387
1455
|
mcp_urls = config.rollout.mcp_url
|
|
@@ -1447,7 +1515,7 @@ def _submit_single_run(
|
|
|
1447
1515
|
)
|
|
1448
1516
|
|
|
1449
1517
|
# Add optional files
|
|
1450
|
-
optional_files = {"
|
|
1518
|
+
optional_files = {"tools.py": dir / "tools.py", "requirements.txt": dir / "requirements.txt"}
|
|
1451
1519
|
for file_name, path in optional_files.items():
|
|
1452
1520
|
if path.exists():
|
|
1453
1521
|
files.append(
|
|
@@ -1457,6 +1525,17 @@ def _submit_single_run(
|
|
|
1457
1525
|
)
|
|
1458
1526
|
)
|
|
1459
1527
|
|
|
1528
|
+
# Add Dockerfile.* files for local/ docker images
|
|
1529
|
+
for dockerfile_path in dir.glob("Dockerfile.*"):
|
|
1530
|
+
file_name = dockerfile_path.name
|
|
1531
|
+
click.echo(f" Found Dockerfile: {file_name}")
|
|
1532
|
+
files.append(
|
|
1533
|
+
(
|
|
1534
|
+
file_name.replace(".", "_"),
|
|
1535
|
+
(file_name, open(dockerfile_path, "rb"), "application/octet-stream"),
|
|
1536
|
+
)
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1460
1539
|
headers = auth.get_auth_headers()
|
|
1461
1540
|
headers.pop("Content-Type", None)
|
|
1462
1541
|
|
|
@@ -1732,6 +1811,36 @@ def run(
|
|
|
1732
1811
|
if not config.organization_id:
|
|
1733
1812
|
config.organization_id = get_active_organization()
|
|
1734
1813
|
|
|
1814
|
+
# Load secrets from .env file if it exists
|
|
1815
|
+
secret_values = {}
|
|
1816
|
+
env_file = dir / ".env"
|
|
1817
|
+
if env_file.exists():
|
|
1818
|
+
try:
|
|
1819
|
+
with open(env_file) as f:
|
|
1820
|
+
for line in f:
|
|
1821
|
+
line = line.strip()
|
|
1822
|
+
# Skip empty lines and comments
|
|
1823
|
+
if not line or line.startswith("#"):
|
|
1824
|
+
continue
|
|
1825
|
+
# Parse KEY=value format
|
|
1826
|
+
if "=" in line:
|
|
1827
|
+
key, _, value = line.partition("=")
|
|
1828
|
+
key = key.strip()
|
|
1829
|
+
value = value.strip()
|
|
1830
|
+
# Remove quotes if present
|
|
1831
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
1832
|
+
value.startswith("'") and value.endswith("'")
|
|
1833
|
+
):
|
|
1834
|
+
value = value[1:-1]
|
|
1835
|
+
secret_values[key] = value
|
|
1836
|
+
|
|
1837
|
+
if secret_values:
|
|
1838
|
+
click.echo(
|
|
1839
|
+
click.style(f"🔐 Loaded {len(secret_values)} secret(s) from .env", dim=True)
|
|
1840
|
+
)
|
|
1841
|
+
except Exception as e:
|
|
1842
|
+
click.echo(click.style(f"⚠️ Warning: Failed to read .env file: {e}", fg="yellow"))
|
|
1843
|
+
|
|
1735
1844
|
# Validate required files (all in the same directory now)
|
|
1736
1845
|
required_files = {
|
|
1737
1846
|
"train.jsonl": dir / "train.jsonl",
|
|
@@ -1807,10 +1916,10 @@ def run(
|
|
|
1807
1916
|
# Collect all tools
|
|
1808
1917
|
all_tools = []
|
|
1809
1918
|
|
|
1810
|
-
# Get tools from
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
all_tools.extend(
|
|
1919
|
+
# Get tools from tools.py
|
|
1920
|
+
tools_path = dir / "tools.py"
|
|
1921
|
+
tools_py_tools = get_tools_from_tools_py(tools_path)
|
|
1922
|
+
all_tools.extend(tools_py_tools)
|
|
1814
1923
|
|
|
1815
1924
|
# Fetch MCP tool schemas (with progress indicator)
|
|
1816
1925
|
mcp_urls = config.rollout.mcp_url if config.rollout else None
|
|
@@ -1828,9 +1937,7 @@ def run(
|
|
|
1828
1937
|
click.echo(f" rnow is running from: {click.style(sys.executable, dim=True)}")
|
|
1829
1938
|
click.echo()
|
|
1830
1939
|
click.echo(" Install it with:")
|
|
1831
|
-
click.echo(
|
|
1832
|
-
click.style(f" {sys.executable} -m pip install fastmcp", fg=TEAL_RGB)
|
|
1833
|
-
)
|
|
1940
|
+
click.echo(click.style(" uv pip install fastmcp", fg=TEAL_RGB))
|
|
1834
1941
|
click.echo()
|
|
1835
1942
|
raise click.ClickException("Missing dependency: fastmcp")
|
|
1836
1943
|
|
|
@@ -1927,19 +2034,19 @@ def run(
|
|
|
1927
2034
|
"Please ensure reward names in train.jsonl match functions in rewards.py"
|
|
1928
2035
|
)
|
|
1929
2036
|
|
|
1930
|
-
# Validate
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
if
|
|
2037
|
+
# Validate tools.py if present (check for docstrings on @tool functions)
|
|
2038
|
+
tools_path = dir / "tools.py"
|
|
2039
|
+
has_tools_py = tools_path.exists() and tools_path.stat().st_size > 0
|
|
2040
|
+
if has_tools_py:
|
|
1934
2041
|
try:
|
|
1935
2042
|
from rnow.core.tool import validate_tools_file
|
|
1936
2043
|
|
|
1937
|
-
errors = validate_tools_file(
|
|
2044
|
+
errors = validate_tools_file(tools_path)
|
|
1938
2045
|
if errors:
|
|
1939
|
-
click.echo(click.style("✗ Invalid
|
|
2046
|
+
click.echo(click.style("✗ Invalid tools.py:", fg="red", bold=True))
|
|
1940
2047
|
for err in errors:
|
|
1941
2048
|
click.echo(f" • {err}")
|
|
1942
|
-
raise click.ClickException("Please fix
|
|
2049
|
+
raise click.ClickException("Please fix tools.py before submitting")
|
|
1943
2050
|
except ImportError:
|
|
1944
2051
|
pass # Skip validation if module not available
|
|
1945
2052
|
|
|
@@ -1950,17 +2057,37 @@ def run(
|
|
|
1950
2057
|
mcp_url = config.rollout.mcp_url
|
|
1951
2058
|
mcp_url_count = len(mcp_url) if isinstance(mcp_url, list) else 1
|
|
1952
2059
|
|
|
2060
|
+
# Validate tool support for the model
|
|
2061
|
+
has_tools = has_tools_py or has_mcp_url
|
|
2062
|
+
if has_tools and not models.supports_tool_calling(model_path):
|
|
2063
|
+
click.echo()
|
|
2064
|
+
click.echo(click.style("✗ Model does not support tool calling", fg="red", bold=True))
|
|
2065
|
+
click.echo()
|
|
2066
|
+
click.echo(f" Model {model_path} does not support tool calling.")
|
|
2067
|
+
if "gpt-oss" in model_path.lower():
|
|
2068
|
+
click.echo(" OpenAI gpt-oss models use a format that doesn't support tools.")
|
|
2069
|
+
else:
|
|
2070
|
+
click.echo(" Base/non-instruct models use a format that doesn't support tools.")
|
|
2071
|
+
click.echo()
|
|
2072
|
+
click.echo(click.style(" Options:", bold=True))
|
|
2073
|
+
click.echo(" 1. Remove tools.py and mcp_url from your project")
|
|
2074
|
+
click.echo(
|
|
2075
|
+
" 2. Use a model that supports tools (e.g., Qwen/Qwen3-8B, meta-llama/Llama-3.1-8B-Instruct)"
|
|
2076
|
+
)
|
|
2077
|
+
click.echo()
|
|
2078
|
+
raise click.ClickException("Model does not support tool calling")
|
|
2079
|
+
|
|
1953
2080
|
# Show tool sources message
|
|
1954
|
-
if
|
|
2081
|
+
if has_tools_py and has_mcp_url:
|
|
1955
2082
|
server_text = f"{mcp_url_count} server(s)" if mcp_url_count > 1 else "1 server"
|
|
1956
2083
|
click.echo(
|
|
1957
|
-
click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text}) and
|
|
2084
|
+
click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text}) and tools.py tools"
|
|
1958
2085
|
)
|
|
1959
2086
|
elif has_mcp_url:
|
|
1960
2087
|
server_text = f"{mcp_url_count} server(s)" if mcp_url_count > 1 else "1 server"
|
|
1961
2088
|
click.echo(click.style("Tools: ", fg=TEAL_RGB) + f"Using MCP ({server_text})")
|
|
1962
|
-
elif
|
|
1963
|
-
click.echo(click.style("Tools: ", fg=TEAL_RGB) + "Using
|
|
2089
|
+
elif has_tools_py:
|
|
2090
|
+
click.echo(click.style("Tools: ", fg=TEAL_RGB) + "Using tools.py tools")
|
|
1964
2091
|
|
|
1965
2092
|
# Start cube spinner early
|
|
1966
2093
|
spinner = CubeSpinner()
|
|
@@ -2004,7 +2131,7 @@ def run(
|
|
|
2004
2131
|
|
|
2005
2132
|
# Add optional files (all in the same directory now)
|
|
2006
2133
|
optional_files = {
|
|
2007
|
-
"
|
|
2134
|
+
"tools.py": dir / "tools.py",
|
|
2008
2135
|
"requirements.txt": dir / "requirements.txt",
|
|
2009
2136
|
}
|
|
2010
2137
|
|
|
@@ -2017,6 +2144,17 @@ def run(
|
|
|
2017
2144
|
)
|
|
2018
2145
|
)
|
|
2019
2146
|
|
|
2147
|
+
# Add Dockerfile.* files for local/ docker images
|
|
2148
|
+
for dockerfile_path in dir.glob("Dockerfile.*"):
|
|
2149
|
+
file_name = dockerfile_path.name
|
|
2150
|
+
click.echo(f" Found Dockerfile: {file_name}")
|
|
2151
|
+
files.append(
|
|
2152
|
+
(
|
|
2153
|
+
file_name.replace(".", "_"),
|
|
2154
|
+
(file_name, open(dockerfile_path, "rb"), "application/octet-stream"),
|
|
2155
|
+
)
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2020
2158
|
# For multipart, we need to omit Content-Type so requests sets the boundary
|
|
2021
2159
|
headers = auth.get_auth_headers()
|
|
2022
2160
|
headers.pop("Content-Type", None)
|
|
@@ -2038,6 +2176,10 @@ def run(
|
|
|
2038
2176
|
if debug:
|
|
2039
2177
|
submit_data["debug"] = "true"
|
|
2040
2178
|
|
|
2179
|
+
# Add secrets if provided (sent as JSON string)
|
|
2180
|
+
if secret_values:
|
|
2181
|
+
submit_data["secrets"] = json.dumps(secret_values)
|
|
2182
|
+
|
|
2041
2183
|
# Start cube spinner if not already running (for small files)
|
|
2042
2184
|
if not spinner.running:
|
|
2043
2185
|
spinner.start()
|