rnow 0.1.9__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 (57) hide show
  1. rnow-0.1.9/LICENSE +21 -0
  2. rnow-0.1.9/PKG-INFO +129 -0
  3. rnow-0.1.9/README.md +105 -0
  4. rnow-0.1.9/pyproject.toml +75 -0
  5. rnow-0.1.9/rnow/__init__.py +5 -0
  6. rnow-0.1.9/rnow/__main__.py +7 -0
  7. rnow-0.1.9/rnow/cli/__init__.py +6 -0
  8. rnow-0.1.9/rnow/cli/auth.py +67 -0
  9. rnow-0.1.9/rnow/cli/blob.py +98 -0
  10. rnow-0.1.9/rnow/cli/commands.py +1497 -0
  11. rnow-0.1.9/rnow/cli/common.py +28 -0
  12. rnow-0.1.9/rnow/cli/cube.py +255 -0
  13. rnow-0.1.9/rnow/cli/main.py +46 -0
  14. rnow-0.1.9/rnow/cli/test.py +688 -0
  15. rnow-0.1.9/rnow/core/__init__.py +33 -0
  16. rnow-0.1.9/rnow/core/reward.py +294 -0
  17. rnow-0.1.9/rnow/core/tool.py +495 -0
  18. rnow-0.1.9/rnow/models.py +248 -0
  19. rnow-0.1.9/rnow/templates/deepseek-aha/config.yml +25 -0
  20. rnow-0.1.9/rnow/templates/deepseek-aha/rewards.py +38 -0
  21. rnow-0.1.9/rnow/templates/deepseek-aha/train.jsonl +1000 -0
  22. rnow-0.1.9/rnow/templates/mcp-tavily/config.yml +27 -0
  23. rnow-0.1.9/rnow/templates/mcp-tavily/rewards.py +14 -0
  24. rnow-0.1.9/rnow/templates/mcp-tavily/train.jsonl +5 -0
  25. rnow-0.1.9/rnow/templates/new/config.yml +25 -0
  26. rnow-0.1.9/rnow/templates/new/requirements.txt +4 -0
  27. rnow-0.1.9/rnow/templates/new/rewards.py +0 -0
  28. rnow-0.1.9/rnow/templates/new/train.jsonl +0 -0
  29. rnow-0.1.9/rnow/templates/rl-nextjs/config.yml +26 -0
  30. rnow-0.1.9/rnow/templates/rl-nextjs/requirements.txt +3 -0
  31. rnow-0.1.9/rnow/templates/rl-nextjs/rewards.py +328 -0
  32. rnow-0.1.9/rnow/templates/rl-nextjs/train.jsonl +10 -0
  33. rnow-0.1.9/rnow/templates/rl-single/config.yml +26 -0
  34. rnow-0.1.9/rnow/templates/rl-single/rewards.py +18 -0
  35. rnow-0.1.9/rnow/templates/rl-single/train.jsonl +1000 -0
  36. rnow-0.1.9/rnow/templates/rl-tools/config.yml +26 -0
  37. rnow-0.1.9/rnow/templates/rl-tools/env.py +44 -0
  38. rnow-0.1.9/rnow/templates/rl-tools/requirements.txt +1 -0
  39. rnow-0.1.9/rnow/templates/rl-tools/rewards.py +14 -0
  40. rnow-0.1.9/rnow/templates/rl-tools/train.jsonl +5 -0
  41. rnow-0.1.9/rnow/templates/sft/config.yml +19 -0
  42. rnow-0.1.9/rnow/templates/sft/train.jsonl +3 -0
  43. rnow-0.1.9/rnow/templates/tutorial-reward/config.yml +25 -0
  44. rnow-0.1.9/rnow/templates/tutorial-reward/rewards.py +6 -0
  45. rnow-0.1.9/rnow/templates/tutorial-reward/train.jsonl +1200 -0
  46. rnow-0.1.9/rnow/templates/tutorial-tool/config.yml +26 -0
  47. rnow-0.1.9/rnow/templates/tutorial-tool/env.py +7 -0
  48. rnow-0.1.9/rnow/templates/tutorial-tool/requirements.txt +2 -0
  49. rnow-0.1.9/rnow/templates/tutorial-tool/rewards.py +20 -0
  50. rnow-0.1.9/rnow/templates/tutorial-tool/train.jsonl +100 -0
  51. rnow-0.1.9/rnow.egg-info/PKG-INFO +129 -0
  52. rnow-0.1.9/rnow.egg-info/SOURCES.txt +55 -0
  53. rnow-0.1.9/rnow.egg-info/dependency_links.txt +1 -0
  54. rnow-0.1.9/rnow.egg-info/entry_points.txt +2 -0
  55. rnow-0.1.9/rnow.egg-info/requires.txt +18 -0
  56. rnow-0.1.9/rnow.egg-info/top_level.txt +1 -0
  57. rnow-0.1.9/setup.cfg +4 -0
rnow-0.1.9/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ReinforceNow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rnow-0.1.9/PKG-INFO ADDED
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: rnow
3
+ Version: 0.1.9
4
+ Summary: ReinforceNow CLI - Reinforcement Learning platform command-line interface
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: click>=8.0.0
9
+ Requires-Dist: requests>=2.25.0
10
+ Requires-Dist: httpx>=0.24.0
11
+ Requires-Dist: pydantic>=2.0.0
12
+ Requires-Dist: pyyaml>=5.4.0
13
+ Requires-Dist: packaging>=21.0
14
+ Provides-Extra: test
15
+ Requires-Dist: tinker-cookbook>=0.1.0; extra == "test"
16
+ Provides-Extra: api
17
+ Requires-Dist: fastapi>=0.68.0; extra == "api"
18
+ Requires-Dist: uvicorn>=0.15.0; extra == "api"
19
+ Provides-Extra: all
20
+ Requires-Dist: tinker-cookbook>=0.1.0; extra == "all"
21
+ Requires-Dist: fastapi>=0.68.0; extra == "all"
22
+ Requires-Dist: uvicorn>=0.15.0; extra == "all"
23
+ Dynamic: license-file
24
+
25
+ <div align="center">
26
+ <img
27
+ alt="ReinforceNow CLI"
28
+ src="./assets/header.png"
29
+ width="100%"
30
+ >
31
+ <br><br>
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/rnow?color=blue)](https://pypi.org/project/rnow/)
34
+ [![Docs](https://img.shields.io/badge/docs-reinforcenow.ai-blue)](https://reinforcenow.ai/docs)
35
+ [![Follow on X](https://img.shields.io/badge/Follow_on_X-@reinforcenow-black?labelColor=white)](https://x.com/reinforcenowai)
36
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
37
+
38
+ </div>
39
+
40
+ # Documentation
41
+
42
+ See the [documentation](https://www.reinforcenow.ai/docs/getting-started/quickstart) for a technical overview of the platform and [train your first agent](https://www.reinforcenow.ai/docs/getting-started/first-agent)
43
+
44
+ # Quick Start
45
+
46
+ ### 1. Install
47
+
48
+ ```bash
49
+ pip install rnow
50
+ ```
51
+
52
+ ### 2. Authenticate
53
+
54
+ ```bash
55
+ rnow login
56
+ ```
57
+
58
+ ### 3. Create & Run Your First Project
59
+
60
+ ```bash
61
+ rnow init --template rl-single
62
+ rnow run
63
+ ```
64
+
65
+ That's it! Your training run will start on ReinforceNow's infrastructure. Monitor progress in the [dashboard](https://reinforcenow.ai/home).
66
+
67
+ ![ReinforceNow Graph](./assets/reinforcenow-graph.png)
68
+
69
+ # Core Concepts
70
+
71
+ Go from raw data to a reliable AI agent in production. ReinforceNow gives you the flexibility to define:
72
+
73
+ ### 1. Reward Functions
74
+
75
+ Define how your model should be evaluated using the `@reward` decorator:
76
+
77
+ ```python
78
+ from rnow.core import reward, RewardArgs
79
+
80
+ @reward
81
+ async def accuracy(args: RewardArgs, messages: list) -> float:
82
+ """Check if the model's answer matches ground truth."""
83
+ response = messages[-1]["content"]
84
+ expected = args.metadata["answer"]
85
+ return 1.0 if expected in response else 0.0
86
+ ```
87
+
88
+ → [Write your first reward function](https://www.reinforcenow.ai/docs/getting-started/first-reward)
89
+
90
+ ### 2. Tools (for Agents)
91
+
92
+ Give your model the ability to call functions during training:
93
+
94
+ ```python
95
+ from rnow.core import tool
96
+
97
+ @tool
98
+ def search(query: str, max_results: int = 5) -> dict:
99
+ """Search the web for information."""
100
+ # Your implementation here
101
+ return {"results": [...]}
102
+ ```
103
+
104
+ → [Train an agent with custom tools](https://www.reinforcenow.ai/docs/getting-started/first-agent)
105
+
106
+ ### 3. Training Data
107
+
108
+ Create a `train.jsonl` file with your prompts and reward assignments:
109
+
110
+ ```json
111
+ {"messages": [{"role": "user", "content": "Balance the equation: Fe + O2 → Fe2O3"}], "rewards": ["accuracy"], "metadata": {"answer": "4Fe + 3O2 → 2Fe2O3"}}
112
+ {"messages": [{"role": "user", "content": "Balance the equation: H2 + O2 → H2O"}], "rewards": ["accuracy"], "metadata": {"answer": "2H2 + O2 → 2H2O"}}
113
+ {"messages": [{"role": "user", "content": "Balance the equation: N2 + H2 → NH3"}], "rewards": ["accuracy"], "metadata": {"answer": "N2 + 3H2 → 2NH3"}}
114
+ ```
115
+
116
+ → [Learn about training data format](https://www.reinforcenow.ai/docs/cli-reference/train-data)
117
+
118
+ # Contributing
119
+
120
+ We welcome contributions! ❤️ Please open an issue to discuss your ideas before submitting a PR.
121
+
122
+ <br>
123
+ <div align="center">
124
+ <img
125
+ alt="ReinforceNow"
126
+ src="./assets/footer.png"
127
+ width="100%"
128
+ >
129
+ </div>
rnow-0.1.9/README.md ADDED
@@ -0,0 +1,105 @@
1
+ <div align="center">
2
+ <img
3
+ alt="ReinforceNow CLI"
4
+ src="./assets/header.png"
5
+ width="100%"
6
+ >
7
+ <br><br>
8
+
9
+ [![PyPI version](https://img.shields.io/pypi/v/rnow?color=blue)](https://pypi.org/project/rnow/)
10
+ [![Docs](https://img.shields.io/badge/docs-reinforcenow.ai-blue)](https://reinforcenow.ai/docs)
11
+ [![Follow on X](https://img.shields.io/badge/Follow_on_X-@reinforcenow-black?labelColor=white)](https://x.com/reinforcenowai)
12
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
13
+
14
+ </div>
15
+
16
+ # Documentation
17
+
18
+ See the [documentation](https://www.reinforcenow.ai/docs/getting-started/quickstart) for a technical overview of the platform and [train your first agent](https://www.reinforcenow.ai/docs/getting-started/first-agent)
19
+
20
+ # Quick Start
21
+
22
+ ### 1. Install
23
+
24
+ ```bash
25
+ pip install rnow
26
+ ```
27
+
28
+ ### 2. Authenticate
29
+
30
+ ```bash
31
+ rnow login
32
+ ```
33
+
34
+ ### 3. Create & Run Your First Project
35
+
36
+ ```bash
37
+ rnow init --template rl-single
38
+ rnow run
39
+ ```
40
+
41
+ That's it! Your training run will start on ReinforceNow's infrastructure. Monitor progress in the [dashboard](https://reinforcenow.ai/home).
42
+
43
+ ![ReinforceNow Graph](./assets/reinforcenow-graph.png)
44
+
45
+ # Core Concepts
46
+
47
+ Go from raw data to a reliable AI agent in production. ReinforceNow gives you the flexibility to define:
48
+
49
+ ### 1. Reward Functions
50
+
51
+ Define how your model should be evaluated using the `@reward` decorator:
52
+
53
+ ```python
54
+ from rnow.core import reward, RewardArgs
55
+
56
+ @reward
57
+ async def accuracy(args: RewardArgs, messages: list) -> float:
58
+ """Check if the model's answer matches ground truth."""
59
+ response = messages[-1]["content"]
60
+ expected = args.metadata["answer"]
61
+ return 1.0 if expected in response else 0.0
62
+ ```
63
+
64
+ → [Write your first reward function](https://www.reinforcenow.ai/docs/getting-started/first-reward)
65
+
66
+ ### 2. Tools (for Agents)
67
+
68
+ Give your model the ability to call functions during training:
69
+
70
+ ```python
71
+ from rnow.core import tool
72
+
73
+ @tool
74
+ def search(query: str, max_results: int = 5) -> dict:
75
+ """Search the web for information."""
76
+ # Your implementation here
77
+ return {"results": [...]}
78
+ ```
79
+
80
+ → [Train an agent with custom tools](https://www.reinforcenow.ai/docs/getting-started/first-agent)
81
+
82
+ ### 3. Training Data
83
+
84
+ Create a `train.jsonl` file with your prompts and reward assignments:
85
+
86
+ ```json
87
+ {"messages": [{"role": "user", "content": "Balance the equation: Fe + O2 → Fe2O3"}], "rewards": ["accuracy"], "metadata": {"answer": "4Fe + 3O2 → 2Fe2O3"}}
88
+ {"messages": [{"role": "user", "content": "Balance the equation: H2 + O2 → H2O"}], "rewards": ["accuracy"], "metadata": {"answer": "2H2 + O2 → 2H2O"}}
89
+ {"messages": [{"role": "user", "content": "Balance the equation: N2 + H2 → NH3"}], "rewards": ["accuracy"], "metadata": {"answer": "N2 + 3H2 → 2NH3"}}
90
+ ```
91
+
92
+ → [Learn about training data format](https://www.reinforcenow.ai/docs/cli-reference/train-data)
93
+
94
+ # Contributing
95
+
96
+ We welcome contributions! ❤️ Please open an issue to discuss your ideas before submitting a PR.
97
+
98
+ <br>
99
+ <div align="center">
100
+ <img
101
+ alt="ReinforceNow"
102
+ src="./assets/footer.png"
103
+ width="100%"
104
+ >
105
+ </div>
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where = ["."]
7
+ include = ["rnow*"]
8
+
9
+ [tool.setuptools.package-data]
10
+ rnow = ["templates/*", "templates/start/*", "templates/new/*", "templates/**/*"]
11
+
12
+ [project]
13
+ name = "rnow"
14
+ version = "0.1.9"
15
+ description = "ReinforceNow CLI - Reinforcement Learning platform command-line interface"
16
+ readme = "README.md"
17
+ requires-python = ">=3.10"
18
+ dependencies = [
19
+ # Core CLI deps - lightweight, cross-platform
20
+ "click>=8.0.0",
21
+ "requests>=2.25.0",
22
+ "httpx>=0.24.0",
23
+ "pydantic>=2.0.0",
24
+ "pyyaml>=5.4.0",
25
+ "packaging>=21.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ # Local testing with ML inference (requires torch)
30
+ test = ["tinker-cookbook>=0.1.0"]
31
+ # API server mode
32
+ api = ["fastapi>=0.68.0", "uvicorn>=0.15.0"]
33
+ # All optional features
34
+ all = ["tinker-cookbook>=0.1.0", "fastapi>=0.68.0", "uvicorn>=0.15.0"]
35
+
36
+ [project.scripts]
37
+ rnow = "rnow.cli.main:main"
38
+
39
+ # =============================================================================
40
+ # Ruff configuration (linting + formatting)
41
+ # =============================================================================
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py310"
45
+
46
+ [tool.ruff.lint]
47
+ select = [
48
+ "E", # pycodestyle errors
49
+ "F", # pyflakes
50
+ "I", # isort
51
+ "W", # pycodestyle warnings
52
+ "UP", # pyupgrade
53
+ "B", # flake8-bugbear
54
+ "SIM", # flake8-simplify
55
+ ]
56
+ ignore = [
57
+ "E501", # line too long (formatter handles)
58
+ "E402", # imports not at top
59
+ "E722", # bare except
60
+ "B008", # function call in default arg
61
+ "B904", # raise from err
62
+ "SIM115", # context manager for open
63
+ ]
64
+
65
+ [tool.ruff.lint.isort]
66
+ known-first-party = ["rnow"]
67
+
68
+ # =============================================================================
69
+ # Mypy configuration (type checking)
70
+ # =============================================================================
71
+ [tool.mypy]
72
+ python_version = "3.10"
73
+ warn_return_any = true
74
+ warn_unused_ignores = true
75
+ ignore_missing_imports = true
@@ -0,0 +1,5 @@
1
+ """
2
+ ReinforceNow CLI - Command-line interface for ReinforceNow RLHF platform.
3
+ """
4
+
5
+ __version__ = "0.8.2"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python
2
+ """Entry point for running rnow as a module."""
3
+
4
+ from rnow.cli.main import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,6 @@
1
+ # reinforcenow/cli/__init__.py
2
+ # CLI package exports
3
+
4
+ from rnow.cli.main import cli
5
+
6
+ __all__ = ["cli"]
@@ -0,0 +1,67 @@
1
+ # reinforcenow/cli/auth.py
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ # Simple home directory paths
9
+ DATA_DIR = Path.home() / ".reinforcenow"
10
+ CREDS_FILE = DATA_DIR / "credentials.json"
11
+ CONFIG_FILE = DATA_DIR / "config.json"
12
+
13
+
14
+ def is_authenticated() -> bool:
15
+ """Check if authenticated."""
16
+ try:
17
+ with open(CREDS_FILE) as f:
18
+ return "api_key" in json.load(f)
19
+ except (FileNotFoundError, json.JSONDecodeError, KeyError):
20
+ return False
21
+
22
+
23
+ def get_auth_headers() -> dict[str, str]:
24
+ """Get auth headers."""
25
+ try:
26
+ with open(CREDS_FILE) as f:
27
+ creds = json.load(f)
28
+ return {
29
+ "Content-Type": "application/json",
30
+ "Authorization": f"Bearer {creds['api_key']}",
31
+ }
32
+ except (FileNotFoundError, json.JSONDecodeError, KeyError):
33
+ raise click.ClickException("Not authenticated. Run 'reinforcenow login'")
34
+
35
+
36
+ def get_active_org_from_config() -> str | None:
37
+ """Get active organization."""
38
+ try:
39
+ with open(CONFIG_FILE) as f:
40
+ return json.load(f).get("active_organization_id")
41
+ except (FileNotFoundError, json.JSONDecodeError):
42
+ return None
43
+
44
+
45
+ def set_active_organization(org_id: str) -> None:
46
+ """Set active organization."""
47
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
48
+
49
+ try:
50
+ with open(CONFIG_FILE) as f:
51
+ config = json.load(f)
52
+ except (FileNotFoundError, json.JSONDecodeError):
53
+ config = {}
54
+
55
+ config["active_organization_id"] = org_id
56
+
57
+ with open(CONFIG_FILE, "w") as f:
58
+ json.dump(config, f, indent=2)
59
+
60
+
61
+ def logout() -> None:
62
+ """Remove credentials."""
63
+ if CREDS_FILE.exists():
64
+ CREDS_FILE.unlink()
65
+ click.echo("✓ Logged out")
66
+ else:
67
+ click.echo("Not logged in")
@@ -0,0 +1,98 @@
1
+ # reinforcenow/cli/blob.py
2
+ """Vercel Blob upload support for large files."""
3
+
4
+ from pathlib import Path
5
+
6
+ import requests
7
+
8
+ from rnow.cli import auth
9
+
10
+ # Size threshold for blob uploads (4MB to stay under 4.5MB limit)
11
+ MAX_INLINE_BYTES = 4 * 1024 * 1024
12
+
13
+ BLOB_API_URL = "https://blob.vercel-storage.com"
14
+ BLOB_API_VERSION = "7"
15
+
16
+
17
+ def request_blob_client_token(base_url: str, pathname: str) -> str:
18
+ """
19
+ Request a client upload token from the backend.
20
+ This token allows direct upload to Vercel Blob.
21
+ """
22
+ headers = auth.get_auth_headers()
23
+ headers["Content-Type"] = "application/json"
24
+
25
+ payload = {
26
+ "type": "blob.generate-client-token",
27
+ "payload": {
28
+ "pathname": pathname,
29
+ "callbackUrl": f"{base_url}/dataset/upload",
30
+ },
31
+ }
32
+
33
+ resp = requests.post(
34
+ f"{base_url}/dataset/upload",
35
+ headers=headers,
36
+ json=payload,
37
+ timeout=30,
38
+ )
39
+ resp.raise_for_status()
40
+ data = resp.json()
41
+
42
+ if data.get("type") != "blob.generate-client-token":
43
+ raise RuntimeError(f"Unexpected response from blob token endpoint: {data}")
44
+
45
+ client_token = data.get("clientToken")
46
+ if not client_token:
47
+ raise RuntimeError("No clientToken returned from blob token endpoint")
48
+
49
+ return client_token
50
+
51
+
52
+ def upload_file_to_blob(base_url: str, local_path: Path, blob_pathname: str) -> dict:
53
+ """
54
+ Upload a file directly to Vercel Blob using a client token.
55
+ Returns the blob JSON (contains url, pathname, etc).
56
+ """
57
+ client_token = request_blob_client_token(base_url, blob_pathname)
58
+
59
+ url = f"{BLOB_API_URL}/{blob_pathname.lstrip('/')}"
60
+ headers = {
61
+ "Authorization": f"Bearer {client_token}",
62
+ "x-api-version": BLOB_API_VERSION,
63
+ "x-content-type": "application/jsonl",
64
+ }
65
+
66
+ with open(local_path, "rb") as f:
67
+ resp = requests.put(url, headers=headers, data=f, timeout=300)
68
+
69
+ resp.raise_for_status()
70
+ return resp.json()
71
+
72
+
73
+ def maybe_upload_to_blob(
74
+ base_url: str,
75
+ file_path: Path,
76
+ dataset_id: str,
77
+ ) -> tuple[str | None, dict | None]:
78
+ """
79
+ Check if file needs blob upload and handle it.
80
+
81
+ Returns:
82
+ (inline_contents, blob_info)
83
+ - If small: inline_contents is file content, blob_info is None
84
+ - If large: inline_contents is None, blob_info has url/pathname
85
+ """
86
+ size = file_path.stat().st_size
87
+
88
+ if size <= MAX_INLINE_BYTES:
89
+ # Small file - return contents for inline upload
90
+ return None, None
91
+
92
+ # Large file - upload to blob
93
+ import uuid
94
+
95
+ blob_pathname = f"datasets/{dataset_id}/{uuid.uuid4().hex[:8]}-{file_path.name}"
96
+
97
+ blob = upload_file_to_blob(base_url, file_path, blob_pathname)
98
+ return None, blob