nabla-cli 0.6.4__tar.gz → 0.7.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.
Files changed (35) hide show
  1. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/PKG-INFO +18 -1
  2. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/README.md +17 -0
  3. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/__init__.py +1 -1
  4. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/cli.py +81 -23
  5. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/PKG-INFO +18 -1
  6. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/pyproject.toml +1 -1
  7. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_cli_ux.py +69 -0
  8. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/__main__.py +0 -0
  9. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/branding.py +0 -0
  10. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/config.py +0 -0
  11. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/contract.py +0 -0
  12. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/induce.py +0 -0
  13. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/profile.py +0 -0
  14. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/prompt_recon.py +0 -0
  15. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/recorder.py +0 -0
  16. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/samplegen.py +0 -0
  17. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/scanner.py +0 -0
  18. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/schema_resolver.py +0 -0
  19. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/schemas/site_profile.schema.json +0 -0
  20. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/term.py +0 -0
  21. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla/ui.py +0 -0
  22. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/SOURCES.txt +0 -0
  23. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/dependency_links.txt +0 -0
  24. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/entry_points.txt +0 -0
  25. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/requires.txt +0 -0
  26. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/nabla_cli.egg-info/top_level.txt +0 -0
  27. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/setup.cfg +0 -0
  28. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_config.py +0 -0
  29. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_e2e_profile.py +0 -0
  30. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_induce.py +0 -0
  31. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_prompt_recon.py +0 -0
  32. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_recorder.py +0 -0
  33. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_samplegen.py +0 -0
  34. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_scanner.py +0 -0
  35. {nabla_cli-0.6.4 → nabla_cli-0.7.0}/tests/test_schema_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nabla-cli
3
- Version: 0.6.4
3
+ Version: 0.7.0
4
4
  Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -162,6 +162,23 @@ returns a goal description, implicit constraints, and difficulty
162
162
  dimensions with cited evidence. That's the goal line printed above, and
163
163
  it's what lands in the profile's `goal` block.
164
164
 
165
+ ### `nabla jeremy`
166
+
167
+ An alternative to `build` with the same inputs and site handling: instead
168
+ of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
169
+ a problem generator:
170
+
171
+ ```json
172
+ {
173
+ "goal": "You are generating new problems to test and eventually fine-tune a small student model on the following task: ...",
174
+ "system_prompt": "<the site's own system prompt>",
175
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
176
+ }
177
+ ```
178
+
179
+ The goal text is composed deterministically from the same inferred goal
180
+ and constraints that `build` uses; the examples are the captured pairs.
181
+
165
182
  ## Providers
166
183
 
167
184
  `nabla init` supports three providers. Pick whichever you have a key
@@ -148,6 +148,23 @@ returns a goal description, implicit constraints, and difficulty
148
148
  dimensions with cited evidence. That's the goal line printed above, and
149
149
  it's what lands in the profile's `goal` block.
150
150
 
151
+ ### `nabla jeremy`
152
+
153
+ An alternative to `build` with the same inputs and site handling: instead
154
+ of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
155
+ a problem generator:
156
+
157
+ ```json
158
+ {
159
+ "goal": "You are generating new problems to test and eventually fine-tune a small student model on the following task: ...",
160
+ "system_prompt": "<the site's own system prompt>",
161
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
162
+ }
163
+ ```
164
+
165
+ The goal text is composed deterministically from the same inferred goal
166
+ and constraints that `build` uses; the examples are the captured pairs.
167
+
151
168
  ## Providers
152
169
 
153
170
  `nabla init` supports three providers. Pick whichever you have a key
@@ -1,3 +1,3 @@
1
1
  """nabla — call-site harvester CLI (pre-Phase-0)."""
2
2
 
3
- __version__ = "0.6.4"
3
+ __version__ = "0.7.0"
@@ -132,6 +132,29 @@ def _build_parser() -> argparse.ArgumentParser:
132
132
  p_profile.add_argument("--out", default=None,
133
133
  help="output path (default: <site>.profile.json)")
134
134
 
135
+ p_jeremy = sub.add_parser(
136
+ "jeremy",
137
+ help="assemble a seed spec (goal + system_prompt + problem/solution examples)",
138
+ formatter_class=argparse.RawDescriptionHelpFormatter,
139
+ epilog=(
140
+ "an alternative to `build`: same inputs, but emits <site>.jeremy.json —\n"
141
+ "a generator-framed goal, the site's system prompt, and the captured\n"
142
+ "pairs as {problem, solution} reference examples.\n\n"
143
+ "examples:\n"
144
+ " nabla jeremy auto: pick site, find <site>.captured.jsonl\n"
145
+ " nabla jeremy path/to/repo --site extract_invoice --captured captured.jsonl\n"
146
+ ),
147
+ )
148
+ p_jeremy.add_argument("repo", nargs="?", default=".", help="path to target repo (default: current directory)")
149
+ p_jeremy.add_argument(
150
+ "--site",
151
+ help="site symbol (auto-picked when omitted and the repo has exactly one supported call site)",
152
+ )
153
+ p_jeremy.add_argument("--captured", "--examples", dest="examples",
154
+ help="captured JSONL from `nabla capture` (default: <site>.captured.jsonl)")
155
+ p_jeremy.add_argument("--out", default=None,
156
+ help="output path (default: <site>.jeremy.json)")
157
+
135
158
  return parser
136
159
 
137
160
 
@@ -193,6 +216,7 @@ def main(argv: list[str] | None = None) -> int:
193
216
  "record": _cmd_record, # legacy alias
194
217
  "build": _cmd_profile,
195
218
  "profile": _cmd_profile, # legacy alias
219
+ "jeremy": lambda a: _cmd_profile(a, mode="jeremy"),
196
220
  }
197
221
  try:
198
222
  return commands[args.command](args)
@@ -778,21 +802,23 @@ def _find_captured_file(site) -> Path | None:
778
802
  )
779
803
 
780
804
 
781
- def _cmd_profile(args) -> int:
805
+ def _cmd_profile(args, mode: str = "profile") -> int:
782
806
  from .ui import rail, rail_end, rail_start
783
807
 
784
- rail_start(_out, "nabla build")
808
+ command = "jeremy" if mode == "jeremy" else "build"
809
+ noun = "seed spec(s)" if mode == "jeremy" else "profile(s)"
810
+ rail_start(_out, f"nabla {command}")
785
811
  sites = scan(args.repo)
786
812
 
787
- # Multi-site: no --site/--captured, several supported sites — build a
788
- # profile for every site that has captured pairs on disk.
813
+ # Multi-site: no --site/--captured, several supported sites — build the
814
+ # artifact for every site that has captured pairs on disk.
789
815
  if args.site is None and args.examples is None:
790
816
  supported = [s for s in sites
791
817
  if is_supported(resolve_schema(s, args.repo)[1], s.flags)]
792
818
  if len(supported) > 1:
793
819
  with_captures = [s for s in supported if _find_captured_file(s) is not None]
794
820
  if not with_captures:
795
- print(f"nabla build: no captured pairs found for any of the "
821
+ print(f"nabla {command}: no captured pairs found for any of the "
796
822
  f"{len(supported)} supported sites. Run first: nabla capture {args.repo}",
797
823
  file=sys.stderr)
798
824
  return 2
@@ -801,11 +827,11 @@ def _cmd_profile(args) -> int:
801
827
  for i, site in enumerate(with_captures):
802
828
  if i:
803
829
  rail(_out)
804
- rc = _build_one(args, site) or rc
830
+ rc = _build_one(args, site, mode=mode) or rc
805
831
  if skipped:
806
832
  rail(_out, f"skipped (no captured pairs): {', '.join(skipped)}", style="yellow")
807
833
  rail(_out)
808
- rail_end(_out, f"done — built {len(with_captures)} profile(s)", style="green")
834
+ rail_end(_out, f"done — built {len(with_captures)} {noun}", style="green")
809
835
  return rc
810
836
 
811
837
  site, err = _resolve_site(args, sites)
@@ -813,16 +839,32 @@ def _cmd_profile(args) -> int:
813
839
  return err
814
840
  if args.site:
815
841
  rail(_out, f"site {site.symbol} · {site.file}")
816
- rc = _build_one(args, site, final_rail=True)
842
+ rc = _build_one(args, site, final_rail=True, mode=mode)
817
843
  return rc
818
844
 
819
845
 
820
- def _build_one(args, site, final_rail: bool = False) -> int:
846
+ def _compose_seed_goal(goal) -> str:
847
+ """Generator-framed goal text for the seed spec, composed in pure Python
848
+ from the induced GoalBlock — no extra LLM call, no invented facts."""
849
+ parts = [
850
+ "You are generating new problems to test and eventually fine-tune a "
851
+ "small student model on the following task: " + goal.description.rstrip(".") + "."
852
+ ]
853
+ if goal.constraints:
854
+ parts.append("Every generated problem's solution must satisfy: "
855
+ + " ".join(c.rstrip(".") + "." for c in goal.constraints))
856
+ parts.append("Match the style, difficulty range, and output format of the "
857
+ "reference examples below.")
858
+ return " ".join(parts)
859
+
860
+
861
+ def _build_one(args, site, final_rail: bool = False, mode: str = "profile") -> int:
821
862
  from .induce import induce
822
863
  from .prompt_recon import reconstruct
823
864
  from .schema_resolver import resolve_schema as _resolve
824
865
  from .ui import rail, rail_end, rail_ok
825
866
 
867
+ command = "jeremy" if mode == "jeremy" else "build"
826
868
  examples_path = args.examples
827
869
  if examples_path is None:
828
870
  found = _find_captured_file(site) or (
@@ -831,7 +873,7 @@ def _build_one(args, site, final_rail: bool = False) -> int:
831
873
  capture_cmd = f"nabla capture {args.repo}"
832
874
  if args.site:
833
875
  capture_cmd += f" --site {site.symbol}"
834
- print(f"nabla build: no captured pairs found ({site.symbol}.captured.jsonl). "
876
+ print(f"nabla {command}: no captured pairs found ({site.symbol}.captured.jsonl). "
835
877
  f"Run first: {capture_cmd}", file=sys.stderr)
836
878
  return 2
837
879
  examples_path = str(found)
@@ -857,21 +899,37 @@ def _build_one(args, site, final_rail: bool = False) -> int:
857
899
  else:
858
900
  goal = induce(template, schema, examples)
859
901
 
860
- profile = build_profile(site, args.repo, examples, goal, recorded_from=recorded_from)
861
- out = (args.out if final_rail else None) or f"{site.symbol}.profile.json"
862
- Path(out).write_text(json.dumps(profile, indent=2), encoding="utf-8")
863
- supported = profile["classification"]["supported"]
864
- rail(_out)
865
- rail_ok(_out, f"wrote {out}")
866
- rail(_out, f"site {site.symbol} · {profile['site_id'][:16]}…")
867
- rail(_out, f"supported {supported}")
868
- rail(_out, f"examples {len(examples)} ({recorded_from})")
869
- rail(_out, f"goal {goal.description[:90]}")
870
- dims = ", ".join(d["name"] for d in goal.difficulty_dimensions[:4])
871
- rail(_out, f"difficulty {dims}")
902
+ if mode == "jeremy":
903
+ seed = {
904
+ "goal": _compose_seed_goal(goal),
905
+ "system_prompt": template.system_prompt or "",
906
+ "examples": [
907
+ {"problem": e.prompt, "solution": e.completion} for e in examples
908
+ ],
909
+ }
910
+ out = (args.out if final_rail else None) or f"{site.symbol}.jeremy.json"
911
+ Path(out).write_text(json.dumps(seed, indent=2), encoding="utf-8")
912
+ rail(_out)
913
+ rail_ok(_out, f"wrote {out}")
914
+ rail(_out, f"site {site.symbol}")
915
+ rail(_out, f"examples {len(examples)} ({recorded_from})")
916
+ rail(_out, f"goal {seed['goal'][:90]}")
917
+ else:
918
+ profile = build_profile(site, args.repo, examples, goal, recorded_from=recorded_from)
919
+ out = (args.out if final_rail else None) or f"{site.symbol}.profile.json"
920
+ Path(out).write_text(json.dumps(profile, indent=2), encoding="utf-8")
921
+ supported = profile["classification"]["supported"]
922
+ rail(_out)
923
+ rail_ok(_out, f"wrote {out}")
924
+ rail(_out, f"site {site.symbol} · {profile['site_id'][:16]}…")
925
+ rail(_out, f"supported {supported}")
926
+ rail(_out, f"examples {len(examples)} ({recorded_from})")
927
+ rail(_out, f"goal {goal.description[:90]}")
928
+ dims = ", ".join(d["name"] for d in goal.difficulty_dimensions[:4])
929
+ rail(_out, f"difficulty {dims}")
872
930
  if len(examples) < 20:
873
931
  rail(_out, f"only {len(examples)} examples (< 20) — enough for a look, but a "
874
- "handoff-quality profile wants 20+ real-traffic pairs", style="yellow")
932
+ "handoff-quality artifact wants 20+ real-traffic pairs", style="yellow")
875
933
  if final_rail:
876
934
  rail(_out)
877
935
  rail_end(_out, f"done — {out} is the handoff artifact", style="green")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nabla-cli
3
- Version: 0.6.4
3
+ Version: 0.7.0
4
4
  Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -162,6 +162,23 @@ returns a goal description, implicit constraints, and difficulty
162
162
  dimensions with cited evidence. That's the goal line printed above, and
163
163
  it's what lands in the profile's `goal` block.
164
164
 
165
+ ### `nabla jeremy`
166
+
167
+ An alternative to `build` with the same inputs and site handling: instead
168
+ of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
169
+ a problem generator:
170
+
171
+ ```json
172
+ {
173
+ "goal": "You are generating new problems to test and eventually fine-tune a small student model on the following task: ...",
174
+ "system_prompt": "<the site's own system prompt>",
175
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
176
+ }
177
+ ```
178
+
179
+ The goal text is composed deterministically from the same inferred goal
180
+ and constraints that `build` uses; the examples are the captured pairs.
181
+
165
182
  ## Providers
166
183
 
167
184
  `nabla init` supports three providers. Pick whichever you have a key
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nabla-cli"
3
- version = "0.6.4"
3
+ version = "0.7.0"
4
4
  description = "Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -517,3 +517,72 @@ def test_scan_skips_build_and_dist_dirs(tmp_path, monkeypatch, capsys):
517
517
  out = capsys.readouterr().out
518
518
  assert "1 call site found" in out
519
519
  assert "copy_of_site" not in out
520
+
521
+
522
+ # ---------------------------------------------------------------------------
523
+ # nabla jeremy — seed-spec sibling of build
524
+ # ---------------------------------------------------------------------------
525
+
526
+ def _fake_goal(*a, **k):
527
+ from nabla.contract import GoalBlock
528
+
529
+ return GoalBlock(
530
+ description="Solve a grade-school word problem step by step.",
531
+ constraints=["final answer is a bare number", "no invented constants"],
532
+ difficulty_dimensions=[{"name": "d", "levels": ["a", "b"], "evidence": "e"}],
533
+ )
534
+
535
+
536
+ def test_jeremy_single_site_writes_seed_spec(tmp_path, monkeypatch, capsys):
537
+ repo = _make_single_site_repo(tmp_path)
538
+ monkeypatch.chdir(tmp_path)
539
+ pair = {"input_slots": {"ocr_text": "r"}, "prompt": "A farmer has 6 crates...",
540
+ "completion": "The answer is 57.",
541
+ "tokens": {"in": 5, "out": 5}, "latency_ms": 10, "_source_kind": "samples"}
542
+ (tmp_path / "extract_receipt.captured.jsonl").write_text(
543
+ json.dumps(pair) + "\n", encoding="utf-8")
544
+ monkeypatch.setattr("nabla.induce.induce", _fake_goal)
545
+
546
+ rc = main(["jeremy", str(repo)])
547
+
548
+ assert rc == 0
549
+ seed = json.loads((tmp_path / "extract_receipt.jeremy.json").read_text(encoding="utf-8"))
550
+ assert set(seed) == {"goal", "system_prompt", "examples"}
551
+ assert seed["goal"].startswith("You are generating new problems")
552
+ assert "grade-school word problem" in seed["goal"]
553
+ assert "final answer is a bare number" in seed["goal"]
554
+ assert "Extract the receipt fields" in seed["system_prompt"]
555
+ assert seed["examples"] == [
556
+ {"problem": "A farmer has 6 crates...", "solution": "The answer is 57."}
557
+ ]
558
+ assert "extract_receipt.jeremy.json" in capsys.readouterr().out
559
+
560
+
561
+ def test_jeremy_multi_site(tmp_path, monkeypatch, capsys):
562
+ repo = _make_two_site_repo(tmp_path)
563
+ monkeypatch.chdir(tmp_path)
564
+ pair = {"input_slots": {}, "prompt": "p", "completion": "s",
565
+ "tokens": {"in": 1, "out": 1}, "latency_ms": 1, "_source_kind": "samples"}
566
+ for symbol in ("extract_receipt", "extract_menu"):
567
+ (tmp_path / f"{symbol}.captured.jsonl").write_text(
568
+ json.dumps(pair) + "\n", encoding="utf-8")
569
+ monkeypatch.setattr("nabla.induce.induce", _fake_goal)
570
+
571
+ rc = main(["jeremy", str(repo)])
572
+
573
+ assert rc == 0
574
+ assert (tmp_path / "extract_receipt.jeremy.json").exists()
575
+ assert (tmp_path / "extract_menu.jeremy.json").exists()
576
+ assert "built 2 seed spec(s)" in capsys.readouterr().out
577
+
578
+
579
+ def test_jeremy_without_captures_points_at_capture(tmp_path, monkeypatch, capsys):
580
+ repo = _make_single_site_repo(tmp_path)
581
+ monkeypatch.chdir(tmp_path)
582
+
583
+ rc = main(["jeremy", str(repo)])
584
+
585
+ assert rc == 2
586
+ err = capsys.readouterr().err
587
+ assert "nabla jeremy" in err
588
+ assert "nabla capture" in err
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes