blitz-cli 0.1.0__tar.gz → 0.4.0__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.
- blitz_cli-0.4.0/.github/workflows/homebrew.yml +54 -0
- blitz_cli-0.4.0/LICENSE +21 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/PKG-INFO +8 -11
- blitz_cli-0.4.0/README.md +22 -0
- blitz_cli-0.4.0/blitz_cli/__init__.py +27 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/_client.py +30 -29
- blitz_cli-0.4.0/blitz_cli/_scaffold.py +103 -0
- blitz_cli-0.4.0/blitz_cli/_scan/__init__.py +65 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_classify.py +28 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_js.py +98 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_models.py +126 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_python.py +308 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_report.py +196 -0
- blitz_cli-0.4.0/blitz_cli/_scan/_walk.py +68 -0
- blitz_cli-0.4.0/blitz_cli/_skill.py +59 -0
- blitz_cli-0.4.0/blitz_cli/cli.py +487 -0
- blitz_cli-0.4.0/blitz_cli/skills/__init__.py +0 -0
- blitz_cli-0.4.0/blitz_cli/skills/configure-training/SKILL.md +166 -0
- blitz_cli-0.4.0/blitz_cli/skills/onboard-to-blitz/SKILL.md +146 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/templates/Dockerfile.tmpl +1 -2
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/templates/Makefile.tmpl +5 -7
- blitz_cli-0.4.0/blitz_cli/templates/README.md.tmpl +38 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/templates/dockerignore.tmpl +2 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/templates/requirements.txt.tmpl +1 -1
- blitz_cli-0.4.0/blitz_cli/templates/train.py.tmpl +216 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/pyproject.toml +2 -1
- blitz_cli-0.4.0/tests/test_scaffold.py +89 -0
- blitz_cli-0.4.0/tests/test_scan.py +308 -0
- blitz_cli-0.1.0/README.md +0 -27
- blitz_cli-0.1.0/blitz_cli/__init__.py +0 -11
- blitz_cli-0.1.0/blitz_cli/_scaffold.py +0 -111
- blitz_cli-0.1.0/blitz_cli/cli.py +0 -128
- blitz_cli-0.1.0/blitz_cli/templates/README.md.tmpl +0 -36
- blitz_cli-0.1.0/blitz_cli/templates/eval.py.tmpl +0 -84
- blitz_cli-0.1.0/blitz_cli/templates/train.py.tmpl +0 -89
- blitz_cli-0.1.0/tests/test_scaffold.py +0 -80
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/.gitignore +0 -0
- {blitz_cli-0.1.0 → blitz_cli-0.4.0}/blitz_cli/templates/__init__.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Update Homebrew tap
|
|
2
|
+
|
|
3
|
+
# On every release tag, point the sparepartslabs/homebrew-tap formula at the new
|
|
4
|
+
# PyPI sdist. Runs alongside publish.yml (same v* trigger) and waits for the
|
|
5
|
+
# sdist to appear on PyPI before reading its canonical URL + sha256.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- "v*"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
bump:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Resolve version
|
|
16
|
+
id: v
|
|
17
|
+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
18
|
+
|
|
19
|
+
- name: Fetch sdist url + sha256 from PyPI (wait for publish)
|
|
20
|
+
id: pypi
|
|
21
|
+
run: |
|
|
22
|
+
VERSION="${{ steps.v.outputs.version }}"
|
|
23
|
+
for i in $(seq 1 30); do
|
|
24
|
+
JSON=$(curl -fsSL "https://pypi.org/pypi/blitz-cli/${VERSION}/json" || true)
|
|
25
|
+
URL=$(printf '%s' "$JSON" | jq -r '.urls[]? | select(.packagetype=="sdist") | .url')
|
|
26
|
+
SHA=$(printf '%s' "$JSON" | jq -r '.urls[]? | select(.packagetype=="sdist") | .digests.sha256')
|
|
27
|
+
if [ -n "$URL" ] && [ "$URL" != "null" ]; then break; fi
|
|
28
|
+
echo "sdist for ${VERSION} not on PyPI yet, retrying ($i)..."
|
|
29
|
+
sleep 10
|
|
30
|
+
done
|
|
31
|
+
if [ -z "$URL" ] || [ "$URL" = "null" ]; then
|
|
32
|
+
echo "::error::blitz-cli ${VERSION} sdist not found on PyPI"; exit 1
|
|
33
|
+
fi
|
|
34
|
+
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
|
35
|
+
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
36
|
+
|
|
37
|
+
- name: Checkout tap
|
|
38
|
+
uses: actions/checkout@v4
|
|
39
|
+
with:
|
|
40
|
+
repository: sparepartslabs/homebrew-tap
|
|
41
|
+
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
42
|
+
|
|
43
|
+
- name: Update formula
|
|
44
|
+
run: |
|
|
45
|
+
F=Formula/blitz-cli.rb
|
|
46
|
+
sed -i -E "s#^ url \".*\"# url \"${{ steps.pypi.outputs.url }}\"#" "$F"
|
|
47
|
+
sed -i -E "s#^ sha256 \".*\"# sha256 \"${{ steps.pypi.outputs.sha }}\"#" "$F"
|
|
48
|
+
git config user.name "github-actions[bot]"
|
|
49
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
50
|
+
if git diff --quiet; then
|
|
51
|
+
echo "formula already up to date"; exit 0
|
|
52
|
+
fi
|
|
53
|
+
git commit -am "blitz-cli ${{ steps.v.outputs.version }}"
|
|
54
|
+
git push
|
blitz_cli-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spare Parts Labs
|
|
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.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Developer CLI for Blitz: pull a workflow's training data + base-model recommendation and scaffold a runnable QLoRA training container.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
5
7
|
Requires-Python: >=3.9
|
|
6
8
|
Description-Content-Type: text/markdown
|
|
7
9
|
|
|
8
10
|
# blitz-cli
|
|
9
11
|
|
|
10
12
|
Developer CLI for [Blitz](https://github.com/sparepartslabs/blitz-sdk-py). Pull a
|
|
11
|
-
workflow's captured traces as a fine-tuning dataset
|
|
12
|
-
|
|
13
|
-
close the loop by grading the trained student against the held-out eval set.
|
|
13
|
+
workflow's captured traces as a fine-tuning dataset, and scaffold a
|
|
14
|
+
self-contained, runnable QLoRA training container.
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
pip install blitz-cli
|
|
@@ -19,16 +20,12 @@ export BLITZ_API_KEY=blz_... # a read-scoped project key (mint one in the dash
|
|
|
19
20
|
blitz scaffold -p proj_abc -w mechanic-assistant -o ./train
|
|
20
21
|
|
|
21
22
|
cd train
|
|
22
|
-
make build && make train
|
|
23
|
+
make build && make train
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
`blitz scaffold` writes `./train` with `data/dataset.jsonl
|
|
26
|
-
`
|
|
27
|
-
`train.py` + `eval.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
26
|
+
`blitz scaffold` writes `./train` with `data/dataset.jsonl` and a `Dockerfile` +
|
|
27
|
+
`train.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
28
28
|
|
|
29
29
|
This package is intentionally dependency-free (stdlib + the Blitz HTTP API). The
|
|
30
30
|
heavy ML stack (torch / transformers / trl / peft) is pinned only in the
|
|
31
31
|
*generated* project's `requirements.txt`, installed inside the training image.
|
|
32
|
-
|
|
33
|
-
The complementary tracing SDK (`pip install blitz-sdk`, `import blitz`) lives in
|
|
34
|
-
[`../blitz-sdk`](../blitz-sdk).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# blitz-cli
|
|
2
|
+
|
|
3
|
+
Developer CLI for [Blitz](https://github.com/sparepartslabs/blitz-sdk-py). Pull a
|
|
4
|
+
workflow's captured traces as a fine-tuning dataset, and scaffold a
|
|
5
|
+
self-contained, runnable QLoRA training container.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install blitz-cli
|
|
9
|
+
|
|
10
|
+
export BLITZ_API_KEY=blz_... # a read-scoped project key (mint one in the dashboard)
|
|
11
|
+
blitz scaffold -p proj_abc -w mechanic-assistant -o ./train
|
|
12
|
+
|
|
13
|
+
cd train
|
|
14
|
+
make build && make train
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`blitz scaffold` writes `./train` with `data/dataset.jsonl` and a `Dockerfile` +
|
|
18
|
+
`train.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
19
|
+
|
|
20
|
+
This package is intentionally dependency-free (stdlib + the Blitz HTTP API). The
|
|
21
|
+
heavy ML stack (torch / transformers / trl / peft) is pinned only in the
|
|
22
|
+
*generated* project's `requirements.txt`, installed inside the training image.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""blitz-cli — onboard a codebase to Blitz and scaffold a QLoRA trainer.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
|
|
5
|
+
- ``blitz init -p <project>`` — write a repo-local ``.env`` (BLITZ_PROJECT /
|
|
6
|
+
BLITZ_ENDPOINT) and validate your ingest key.
|
|
7
|
+
|
|
8
|
+
- ``blitz scan [path]`` — statically walk a codebase and report each AI/LLM
|
|
9
|
+
call's Blitz instrumentation posture (unsupported provider / no blitz.init() /
|
|
10
|
+
traced-but-unnamed / instrumented), with copy-paste fixes. Offline, no API key.
|
|
11
|
+
|
|
12
|
+
- ``blitz verify [--wait]`` — confirm sample traces have landed in Blitz,
|
|
13
|
+
authenticated by the ingest key (``BLITZ_API_KEY``).
|
|
14
|
+
|
|
15
|
+
- ``blitz skill install`` — drop the Blitz Claude Code skills (``onboard-to-blitz``,
|
|
16
|
+
``configure-training``) into the repo's ``.claude/skills`` so Claude can run the
|
|
17
|
+
onboarding and training playbooks.
|
|
18
|
+
|
|
19
|
+
- ``blitz scaffold -p <project> -w <workflow> -o ./train`` — download a
|
|
20
|
+
workflow's SFT dataset, then emit a self-contained, runnable training project
|
|
21
|
+
(Dockerfile + train.py). Authenticates with a read-scoped Blitz API key
|
|
22
|
+
(``BLITZ_API_KEY``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__all__ = ["__version__"]
|
|
26
|
+
|
|
27
|
+
__version__ = "0.4.0"
|
|
@@ -26,8 +26,9 @@ class BlitzAPIError(RuntimeError):
|
|
|
26
26
|
def _hint(status: int, detail: str) -> str:
|
|
27
27
|
if status in (401, 403):
|
|
28
28
|
return (
|
|
29
|
-
f"{detail} (HTTP {status}). Check BLITZ_API_KEY — it must be a "
|
|
30
|
-
"
|
|
29
|
+
f"{detail} (HTTP {status}). Check BLITZ_API_KEY — it must be a valid "
|
|
30
|
+
"key for this project (an ingest key for tracing/verify, a read key "
|
|
31
|
+
"for training data). Create one in the dashboard."
|
|
31
32
|
)
|
|
32
33
|
if status == 404:
|
|
33
34
|
return (
|
|
@@ -71,6 +72,12 @@ class BlitzClient:
|
|
|
71
72
|
except Exception: # noqa: BLE001
|
|
72
73
|
pass
|
|
73
74
|
raise BlitzAPIError(exc.code, _hint(exc.code, str(detail))) from None
|
|
75
|
+
except urllib.error.URLError as exc:
|
|
76
|
+
raise BlitzAPIError(
|
|
77
|
+
0,
|
|
78
|
+
f"could not reach the Blitz API at {self._base} ({exc.reason}). "
|
|
79
|
+
"Check BLITZ_ENDPOINT and your network.",
|
|
80
|
+
) from None
|
|
74
81
|
|
|
75
82
|
def _get_json(self, path: str, query: Optional[dict] = None) -> dict:
|
|
76
83
|
req = urllib.request.Request(self._url(path, query), headers=self._headers)
|
|
@@ -85,30 +92,34 @@ class BlitzClient:
|
|
|
85
92
|
if line:
|
|
86
93
|
yield json.loads(line)
|
|
87
94
|
|
|
88
|
-
def
|
|
95
|
+
def _send_json(self, path: str, body: dict, method: str) -> dict:
|
|
89
96
|
data = json.dumps(body).encode("utf-8")
|
|
90
97
|
req = urllib.request.Request(
|
|
91
98
|
self._url(path),
|
|
92
99
|
data=data,
|
|
93
|
-
method=
|
|
100
|
+
method=method,
|
|
94
101
|
headers={**self._headers, "content-type": "application/json"},
|
|
95
102
|
)
|
|
96
103
|
with self._open(req) as resp:
|
|
97
104
|
return json.loads(resp.read().decode("utf-8"))
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
def _post_json(self, path: str, body: dict) -> dict:
|
|
107
|
+
return self._send_json(path, body, "POST")
|
|
108
|
+
|
|
109
|
+
def _put_json(self, path: str, body: dict) -> dict:
|
|
110
|
+
return self._send_json(path, body, "PUT")
|
|
111
|
+
|
|
112
|
+
# -- public API (one method per endpoint the CLI needs) -----------------
|
|
100
113
|
|
|
101
114
|
def _p(self, suffix: str) -> str:
|
|
102
115
|
return f"/blitz/projects/{self._project}{suffix}"
|
|
103
116
|
|
|
104
|
-
def
|
|
105
|
-
return self._get_json(self._p("/
|
|
106
|
-
|
|
107
|
-
def recommended_tools(self, workflow: str) -> dict:
|
|
108
|
-
return self._get_json(self._p("/recommended-tools"), {"workflow": workflow})
|
|
117
|
+
def get_training_config(self, workflow: str) -> dict:
|
|
118
|
+
return self._get_json(self._p("/training-config"), {"workflow": workflow})
|
|
109
119
|
|
|
110
|
-
def
|
|
111
|
-
|
|
120
|
+
def set_training_config(self, config: dict) -> dict:
|
|
121
|
+
# config is the full flat config (includes `workflow`); the server keys on it.
|
|
122
|
+
return self._put_json(self._p("/training-config"), config)
|
|
112
123
|
|
|
113
124
|
def download_dataset(
|
|
114
125
|
self, workflow: Optional[str] = None, include_synthetic: bool = True
|
|
@@ -118,21 +129,11 @@ class BlitzClient:
|
|
|
118
129
|
{"workflow": workflow, "include_synthetic": include_synthetic},
|
|
119
130
|
)
|
|
120
131
|
|
|
121
|
-
def
|
|
122
|
-
return self.
|
|
123
|
-
|
|
124
|
-
def submit_eval(
|
|
125
|
-
self, workflow: str, predictions: list, grader: str = "auto"
|
|
126
|
-
) -> dict:
|
|
127
|
-
return self._post_json(
|
|
128
|
-
self._p("/eval"),
|
|
129
|
-
{
|
|
130
|
-
"workflow": workflow,
|
|
131
|
-
"candidate": "supplied",
|
|
132
|
-
"grader": grader,
|
|
133
|
-
"predictions": predictions,
|
|
134
|
-
},
|
|
135
|
-
)
|
|
132
|
+
def training_readiness(self, workflow: str) -> dict:
|
|
133
|
+
return self._get_json(self._p("/training-readiness"), {"workflow": workflow})
|
|
136
134
|
|
|
137
|
-
def
|
|
138
|
-
|
|
135
|
+
def ingest_status(self, workflow: Optional[str] = None) -> dict:
|
|
136
|
+
"""Trace summary for `blitz verify`, authed by the ingest key. This is
|
|
137
|
+
the one read the ingest scope can do (path is not project-scoped; the
|
|
138
|
+
key resolves the project server-side)."""
|
|
139
|
+
return self._get_json("/blitz/v1/ingest-status", {"workflow": workflow})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Render a runnable QLoRA training project + derive a training config.
|
|
2
|
+
|
|
3
|
+
Templates live in ``blitz_cli/templates`` and ship as package data. ``train.py``
|
|
4
|
+
and ``requirements.txt`` are copied verbatim (they read ``config.json`` at
|
|
5
|
+
runtime, so they need no substitution and stay valid Python regardless of the
|
|
6
|
+
chosen base model); ``Dockerfile``/``Makefile``/``README`` get ``$var`` substitution
|
|
7
|
+
via string.Template (avoids the brace-escaping pain of str.format on code).
|
|
8
|
+
|
|
9
|
+
The config itself is no longer written into the project at scaffold time. The
|
|
10
|
+
onboarding skill assembles it (via ``build_config``) and pushes it to Blitz with
|
|
11
|
+
``blitz config set``; the training container fetches it at run start and writes its
|
|
12
|
+
own ``config.json``. ``build_config`` stays here as the shared deriver that turns a
|
|
13
|
+
base-model choice into a full config with sensible, size-scaled hyperparameters.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from importlib import resources
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from string import Template
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clamp_seq_len(raw: int) -> int:
|
|
24
|
+
"""Clamp a raw sequence length (p95_input + max_output) to the trainer's
|
|
25
|
+
supported range. Defaults to 2048 when nothing usable is supplied."""
|
|
26
|
+
return max(1024, min(8192, int(raw or 0) or 2048))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_config(
|
|
30
|
+
workflow: str,
|
|
31
|
+
base_model_hf: str,
|
|
32
|
+
params_b: float = 7.0,
|
|
33
|
+
license: str = "",
|
|
34
|
+
seq_len: int = 2048,
|
|
35
|
+
) -> dict:
|
|
36
|
+
"""Derive a full QLoRA training config from a base-model choice.
|
|
37
|
+
|
|
38
|
+
Hyperparameters scale with model size to keep a single 24GB-class card viable
|
|
39
|
+
(effective batch ~16-32). ``seq_len`` is clamped to the trainer's range.
|
|
40
|
+
"""
|
|
41
|
+
params_b = float(params_b or 7)
|
|
42
|
+
|
|
43
|
+
if params_b <= 3:
|
|
44
|
+
lora_r, batch, accum, lr = 16, 4, 4, 2e-4
|
|
45
|
+
elif params_b <= 9:
|
|
46
|
+
lora_r, batch, accum, lr = 16, 2, 8, 2e-4
|
|
47
|
+
else: # 14B and up
|
|
48
|
+
lora_r, batch, accum, lr = 32, 1, 16, 1e-4
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"workflow": workflow,
|
|
52
|
+
"base_model_hf": base_model_hf,
|
|
53
|
+
"params_b": params_b,
|
|
54
|
+
"license": license or "",
|
|
55
|
+
"seq_len": clamp_seq_len(seq_len),
|
|
56
|
+
"lora_r": lora_r,
|
|
57
|
+
"lora_alpha": lora_r * 2,
|
|
58
|
+
"lora_dropout": 0.05,
|
|
59
|
+
"epochs": 3,
|
|
60
|
+
"lr": lr,
|
|
61
|
+
"per_device_batch_size": batch,
|
|
62
|
+
"grad_accum": accum,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _template(name: str) -> str:
|
|
67
|
+
return resources.files("blitz_cli.templates").joinpath(name).read_text(
|
|
68
|
+
encoding="utf-8"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write(path: Path, content: str) -> None:
|
|
73
|
+
path.write_text(content, encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render(out_dir: Path, project: str, workflow: str, endpoint: str) -> None:
|
|
77
|
+
"""Write the training project skeleton into out_dir.
|
|
78
|
+
|
|
79
|
+
No config.json is written: the container fetches the config from Blitz at run
|
|
80
|
+
start (see the templates). Only project/workflow/endpoint are substituted, all
|
|
81
|
+
into the Dockerfile/Makefile/README wiring.
|
|
82
|
+
"""
|
|
83
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
# Verbatim (read config.json at runtime) — keep as real source files.
|
|
86
|
+
for name in ("train.py", "requirements.txt", "dockerignore"):
|
|
87
|
+
dest = ".dockerignore" if name == "dockerignore" else name
|
|
88
|
+
_write(out_dir / dest, _template(name + ".tmpl"))
|
|
89
|
+
|
|
90
|
+
subs = {"project": project, "workflow": workflow, "endpoint": endpoint}
|
|
91
|
+
for name in ("Dockerfile", "Makefile", "README.md"):
|
|
92
|
+
_write(out_dir / name, Template(_template(name + ".tmpl")).safe_substitute(subs))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def gated_license_warning(license: str) -> str | None:
|
|
96
|
+
"""A warning string if a base model's license needs HF acceptance, else None."""
|
|
97
|
+
lic = (license or "").lower()
|
|
98
|
+
if lic.startswith("llama") or lic.startswith("gemma"):
|
|
99
|
+
return (
|
|
100
|
+
f"This base model is gated ({lic}). Accept its license on Hugging Face "
|
|
101
|
+
"and pass HF_TOKEN into the container (see README) before training."
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Codebase scan: find AI/LLM calls and report their Blitz instrumentation
|
|
2
|
+
posture. Public surface: ``scan_path``, ``render_report``, ``dumps_json``."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Set
|
|
8
|
+
|
|
9
|
+
from ._classify import classify
|
|
10
|
+
from ._js import scan_js
|
|
11
|
+
from ._models import ProjectSignals, ScanResult, Tier
|
|
12
|
+
from ._python import scan_python
|
|
13
|
+
from ._report import dumps_json, render_report
|
|
14
|
+
from ._walk import iter_source_files
|
|
15
|
+
|
|
16
|
+
__all__ = ["scan_path", "render_report", "dumps_json", "ScanResult", "Tier"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def scan_path(root: Path, langs: Optional[Set[str]] = None) -> ScanResult:
|
|
20
|
+
"""Walk ``root``, detect LLM calls in each file, then classify every site
|
|
21
|
+
against tree-wide signals (whether the project calls ``blitz.init()``)."""
|
|
22
|
+
langs = langs or {"python", "js"}
|
|
23
|
+
root = root.resolve()
|
|
24
|
+
base = root if root.is_dir() else root.parent
|
|
25
|
+
|
|
26
|
+
result = ScanResult(root=str(root))
|
|
27
|
+
signals = ProjectSignals()
|
|
28
|
+
|
|
29
|
+
for path, lang in iter_source_files(root, langs):
|
|
30
|
+
try:
|
|
31
|
+
source = path.read_text(encoding="utf-8", errors="replace")
|
|
32
|
+
except OSError:
|
|
33
|
+
continue
|
|
34
|
+
rel = _relpath(path, base)
|
|
35
|
+
try:
|
|
36
|
+
scan = scan_python(path, source, rel) if lang == "python" else scan_js(
|
|
37
|
+
path, source, rel
|
|
38
|
+
)
|
|
39
|
+
except SyntaxError:
|
|
40
|
+
result.skipped.append(rel)
|
|
41
|
+
continue
|
|
42
|
+
signals.files_scanned += 1
|
|
43
|
+
if scan.has_init:
|
|
44
|
+
if lang == "python":
|
|
45
|
+
signals.has_python_init = True
|
|
46
|
+
else:
|
|
47
|
+
signals.has_js_init = True
|
|
48
|
+
result.sites.extend(scan.sites)
|
|
49
|
+
|
|
50
|
+
# Classification needs the tree-wide init signal, so it runs after the walk.
|
|
51
|
+
result.signals = signals
|
|
52
|
+
for site in result.sites:
|
|
53
|
+
classify(site, signals)
|
|
54
|
+
|
|
55
|
+
# Stable ordering: worst tier first, then path/line.
|
|
56
|
+
order = {Tier.UNSUPPORTED: 0, Tier.NO_INIT: 1, Tier.UNNAMED: 2, Tier.INSTRUMENTED: 3}
|
|
57
|
+
result.sites.sort(key=lambda s: (order.get(s.tier, 9), s.path, s.line))
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _relpath(path: Path, base: Path) -> str:
|
|
62
|
+
try:
|
|
63
|
+
return path.resolve().relative_to(base).as_posix()
|
|
64
|
+
except ValueError:
|
|
65
|
+
return path.as_posix()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The 4-tier decision tree. Pure and language-agnostic: both detectors funnel
|
|
2
|
+
their ``CallSite`` drafts through here once project-wide signals are known."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from ._models import CallSite, Confidence, ProjectSignals, Provider, SUPPORTED, Tier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def classify(site: CallSite, signals: ProjectSignals) -> None:
|
|
10
|
+
"""Set ``site.tier`` (and adjust confidence) in place."""
|
|
11
|
+
if site.in_untraced:
|
|
12
|
+
# Intentional suppression (blitz-platform meta-calls). Not a miss; we
|
|
13
|
+
# still label it INSTRUMENTED-equivalent so it drops out of misses().
|
|
14
|
+
site.tier = Tier.INSTRUMENTED
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
if site.provider not in SUPPORTED:
|
|
18
|
+
site.tier = Tier.UNSUPPORTED
|
|
19
|
+
elif not signals.has_init(site.lang):
|
|
20
|
+
site.tier = Tier.NO_INIT
|
|
21
|
+
elif not site.in_named_workflow:
|
|
22
|
+
site.tier = Tier.UNNAMED
|
|
23
|
+
# A decorated caller further up the stack may already be tracing this
|
|
24
|
+
# helper at runtime — we can't see that lexically, so soften confidence.
|
|
25
|
+
if site.confidence is Confidence.HIGH:
|
|
26
|
+
site.confidence = Confidence.MEDIUM
|
|
27
|
+
else:
|
|
28
|
+
site.tier = Tier.INSTRUMENTED
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""JS/TS detector — honest regex heuristic.
|
|
2
|
+
|
|
3
|
+
There's no stdlib JS parser and blitz-cli must stay dependency-free, so this is
|
|
4
|
+
line-oriented ``re`` matching, not real parsing. Consequences, stated loudly:
|
|
5
|
+
no scope resolution, no reliable client-var/decorator tracking, and — because a
|
|
6
|
+
JS Blitz SDK's ``init`` can't be verified here — supported-provider JS calls are
|
|
7
|
+
reported as NO_INIT at LOW confidence rather than pretending they're traced.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from ._models import CallSite, Confidence, FileScan, Provider
|
|
17
|
+
|
|
18
|
+
# import specifier substring -> provider (used to know what a file touches)
|
|
19
|
+
_IMPORT_PROVIDER = [
|
|
20
|
+
("@anthropic-ai/sdk", Provider.ANTHROPIC),
|
|
21
|
+
("@google/generative-ai", Provider.GEMINI),
|
|
22
|
+
("@google/genai", Provider.GEMINI),
|
|
23
|
+
("openai", Provider.OPENAI),
|
|
24
|
+
("@langchain", Provider.LANGCHAIN),
|
|
25
|
+
("langchain", Provider.LANGCHAIN),
|
|
26
|
+
("litellm", Provider.LITELLM),
|
|
27
|
+
("cohere-ai", Provider.COHERE),
|
|
28
|
+
("@mistralai", Provider.MISTRAL),
|
|
29
|
+
("@aws-sdk/client-bedrock", Provider.BEDROCK),
|
|
30
|
+
("ollama", Provider.OLLAMA),
|
|
31
|
+
("groq-sdk", Provider.GROQ),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# call-chain regex -> provider it identifies
|
|
35
|
+
_CALL_PATTERNS = [
|
|
36
|
+
(re.compile(r"\.messages\.create\s*\("), Provider.ANTHROPIC),
|
|
37
|
+
(re.compile(r"\.messages\.stream\s*\("), Provider.ANTHROPIC),
|
|
38
|
+
(re.compile(r"\.chat\.completions\.create\s*\("), Provider.OPENAI),
|
|
39
|
+
(re.compile(r"\.responses\.create\s*\("), Provider.OPENAI),
|
|
40
|
+
(re.compile(r"\.generateContent\s*\("), Provider.GEMINI),
|
|
41
|
+
(re.compile(r"\.generateContentStream\s*\("), Provider.GEMINI),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
_INIT_RE = re.compile(r"blitz\.init\s*\(")
|
|
45
|
+
_WORKFLOW_RE = re.compile(r"blitz\.workflow\s*\(")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def scan_js(path: Path, source: str, rel_path: str) -> FileScan:
|
|
49
|
+
lines = source.splitlines()
|
|
50
|
+
imported: List[Provider] = []
|
|
51
|
+
has_init = False
|
|
52
|
+
for ln in lines:
|
|
53
|
+
if _INIT_RE.search(ln):
|
|
54
|
+
has_init = True
|
|
55
|
+
for spec, prov in _IMPORT_PROVIDER:
|
|
56
|
+
if ("import" in ln or "require" in ln) and spec in ln and prov not in imported:
|
|
57
|
+
imported.append(prov)
|
|
58
|
+
|
|
59
|
+
sites: List[CallSite] = []
|
|
60
|
+
for i, ln in enumerate(lines, start=1):
|
|
61
|
+
for pat, prov in _CALL_PATTERNS:
|
|
62
|
+
m = pat.search(ln)
|
|
63
|
+
if not m:
|
|
64
|
+
continue
|
|
65
|
+
# Prefer a provider actually imported in this file; a bare
|
|
66
|
+
# `.responses.create` in a groq/azure file still reads as its shape.
|
|
67
|
+
provider = prov
|
|
68
|
+
sites.append(
|
|
69
|
+
CallSite(
|
|
70
|
+
path=rel_path,
|
|
71
|
+
line=i,
|
|
72
|
+
provider=provider,
|
|
73
|
+
lang="js",
|
|
74
|
+
call_expr=m.group(0).rstrip("(").strip(),
|
|
75
|
+
enclosing_func=_nearest_func(lines, i - 1),
|
|
76
|
+
in_named_workflow=bool(_WORKFLOW_RE.search(ln)),
|
|
77
|
+
in_untraced=False,
|
|
78
|
+
confidence=Confidence.LOW,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return FileScan(sites=sites, has_init=has_init)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_FUNC_RES = [
|
|
85
|
+
re.compile(r"function\s+([A-Za-z0-9_$]+)"),
|
|
86
|
+
re.compile(r"(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\("),
|
|
87
|
+
re.compile(r"([A-Za-z0-9_$]+)\s*\([^)]*\)\s*\{"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _nearest_func(lines: List[str], idx: int) -> Optional[str]:
|
|
92
|
+
"""Best-effort: the nearest preceding function-ish declaration name."""
|
|
93
|
+
for j in range(idx, max(-1, idx - 60), -1):
|
|
94
|
+
for rx in _FUNC_RES:
|
|
95
|
+
m = rx.search(lines[j])
|
|
96
|
+
if m:
|
|
97
|
+
return m.group(1)
|
|
98
|
+
return None
|