nabla-cli 0.6.4__tar.gz → 0.7.1__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.1}/PKG-INFO +19 -1
  2. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/README.md +18 -0
  3. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/__init__.py +1 -1
  4. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/cli.py +76 -23
  5. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/PKG-INFO +19 -1
  6. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/pyproject.toml +1 -1
  7. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_cli_ux.py +68 -0
  8. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/__main__.py +0 -0
  9. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/branding.py +0 -0
  10. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/config.py +0 -0
  11. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/contract.py +0 -0
  12. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/induce.py +0 -0
  13. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/profile.py +0 -0
  14. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/prompt_recon.py +0 -0
  15. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/recorder.py +0 -0
  16. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/samplegen.py +0 -0
  17. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/scanner.py +0 -0
  18. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/schema_resolver.py +0 -0
  19. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/schemas/site_profile.schema.json +0 -0
  20. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/term.py +0 -0
  21. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/ui.py +0 -0
  22. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/SOURCES.txt +0 -0
  23. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/dependency_links.txt +0 -0
  24. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/entry_points.txt +0 -0
  25. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/requires.txt +0 -0
  26. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/top_level.txt +0 -0
  27. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/setup.cfg +0 -0
  28. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_config.py +0 -0
  29. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_e2e_profile.py +0 -0
  30. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_induce.py +0 -0
  31. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_prompt_recon.py +0 -0
  32. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_recorder.py +0 -0
  33. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_samplegen.py +0 -0
  34. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_scanner.py +0 -0
  35. {nabla_cli-0.6.4 → nabla_cli-0.7.1}/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.1
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,24 @@ 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": "<build's inferred goal description and constraints, joined into one string>",
174
+ "system_prompt": "<the site's own system prompt>",
175
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
176
+ }
177
+ ```
178
+
179
+ Same pipeline as `build` (captured pairs in, one goal-inference call) —
180
+ only the output shape differs; nothing is added that `build`'s own
181
+ output doesn't contain.
182
+
165
183
  ## Providers
166
184
 
167
185
  `nabla init` supports three providers. Pick whichever you have a key
@@ -148,6 +148,24 @@ 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": "<build's inferred goal description and constraints, joined into one string>",
160
+ "system_prompt": "<the site's own system prompt>",
161
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
162
+ }
163
+ ```
164
+
165
+ Same pipeline as `build` (captured pairs in, one goal-inference call) —
166
+ only the output shape differs; nothing is added that `build`'s own
167
+ output doesn't contain.
168
+
151
169
  ## Providers
152
170
 
153
171
  `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.1"
@@ -132,6 +132,30 @@ 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 pipeline and inputs, but emits\n"
141
+ "<site>.jeremy.json — build's goal rearranged into one string, the\n"
142
+ "site's system prompt as its own field, and the captured pairs as\n"
143
+ "{problem, solution} reference examples.\n\n"
144
+ "examples:\n"
145
+ " nabla jeremy auto: pick site, find <site>.captured.jsonl\n"
146
+ " nabla jeremy path/to/repo --site extract_invoice --captured captured.jsonl\n"
147
+ ),
148
+ )
149
+ p_jeremy.add_argument("repo", nargs="?", default=".", help="path to target repo (default: current directory)")
150
+ p_jeremy.add_argument(
151
+ "--site",
152
+ help="site symbol (auto-picked when omitted and the repo has exactly one supported call site)",
153
+ )
154
+ p_jeremy.add_argument("--captured", "--examples", dest="examples",
155
+ help="captured JSONL from `nabla capture` (default: <site>.captured.jsonl)")
156
+ p_jeremy.add_argument("--out", default=None,
157
+ help="output path (default: <site>.jeremy.json)")
158
+
135
159
  return parser
136
160
 
137
161
 
@@ -193,6 +217,7 @@ def main(argv: list[str] | None = None) -> int:
193
217
  "record": _cmd_record, # legacy alias
194
218
  "build": _cmd_profile,
195
219
  "profile": _cmd_profile, # legacy alias
220
+ "jeremy": lambda a: _cmd_profile(a, mode="jeremy"),
196
221
  }
197
222
  try:
198
223
  return commands[args.command](args)
@@ -778,21 +803,23 @@ def _find_captured_file(site) -> Path | None:
778
803
  )
779
804
 
780
805
 
781
- def _cmd_profile(args) -> int:
806
+ def _cmd_profile(args, mode: str = "profile") -> int:
782
807
  from .ui import rail, rail_end, rail_start
783
808
 
784
- rail_start(_out, "nabla build")
809
+ command = "jeremy" if mode == "jeremy" else "build"
810
+ noun = "seed spec(s)" if mode == "jeremy" else "profile(s)"
811
+ rail_start(_out, f"nabla {command}")
785
812
  sites = scan(args.repo)
786
813
 
787
- # Multi-site: no --site/--captured, several supported sites — build a
788
- # profile for every site that has captured pairs on disk.
814
+ # Multi-site: no --site/--captured, several supported sites — build the
815
+ # artifact for every site that has captured pairs on disk.
789
816
  if args.site is None and args.examples is None:
790
817
  supported = [s for s in sites
791
818
  if is_supported(resolve_schema(s, args.repo)[1], s.flags)]
792
819
  if len(supported) > 1:
793
820
  with_captures = [s for s in supported if _find_captured_file(s) is not None]
794
821
  if not with_captures:
795
- print(f"nabla build: no captured pairs found for any of the "
822
+ print(f"nabla {command}: no captured pairs found for any of the "
796
823
  f"{len(supported)} supported sites. Run first: nabla capture {args.repo}",
797
824
  file=sys.stderr)
798
825
  return 2
@@ -801,11 +828,11 @@ def _cmd_profile(args) -> int:
801
828
  for i, site in enumerate(with_captures):
802
829
  if i:
803
830
  rail(_out)
804
- rc = _build_one(args, site) or rc
831
+ rc = _build_one(args, site, mode=mode) or rc
805
832
  if skipped:
806
833
  rail(_out, f"skipped (no captured pairs): {', '.join(skipped)}", style="yellow")
807
834
  rail(_out)
808
- rail_end(_out, f"done — built {len(with_captures)} profile(s)", style="green")
835
+ rail_end(_out, f"done — built {len(with_captures)} {noun}", style="green")
809
836
  return rc
810
837
 
811
838
  site, err = _resolve_site(args, sites)
@@ -813,16 +840,26 @@ def _cmd_profile(args) -> int:
813
840
  return err
814
841
  if args.site:
815
842
  rail(_out, f"site {site.symbol} · {site.file}")
816
- rc = _build_one(args, site, final_rail=True)
843
+ rc = _build_one(args, site, final_rail=True, mode=mode)
817
844
  return rc
818
845
 
819
846
 
820
- def _build_one(args, site, final_rail: bool = False) -> int:
847
+ def _compose_seed_goal(goal) -> str:
848
+ """The seed spec's goal string: build's induced goal, rearranged into
849
+ prose — description followed by the constraints. Nothing is added that
850
+ build's own output doesn't contain."""
851
+ parts = [goal.description.rstrip(".") + "."]
852
+ parts += [c.rstrip(".") + "." for c in goal.constraints]
853
+ return " ".join(parts)
854
+
855
+
856
+ def _build_one(args, site, final_rail: bool = False, mode: str = "profile") -> int:
821
857
  from .induce import induce
822
858
  from .prompt_recon import reconstruct
823
859
  from .schema_resolver import resolve_schema as _resolve
824
860
  from .ui import rail, rail_end, rail_ok
825
861
 
862
+ command = "jeremy" if mode == "jeremy" else "build"
826
863
  examples_path = args.examples
827
864
  if examples_path is None:
828
865
  found = _find_captured_file(site) or (
@@ -831,7 +868,7 @@ def _build_one(args, site, final_rail: bool = False) -> int:
831
868
  capture_cmd = f"nabla capture {args.repo}"
832
869
  if args.site:
833
870
  capture_cmd += f" --site {site.symbol}"
834
- print(f"nabla build: no captured pairs found ({site.symbol}.captured.jsonl). "
871
+ print(f"nabla {command}: no captured pairs found ({site.symbol}.captured.jsonl). "
835
872
  f"Run first: {capture_cmd}", file=sys.stderr)
836
873
  return 2
837
874
  examples_path = str(found)
@@ -857,21 +894,37 @@ def _build_one(args, site, final_rail: bool = False) -> int:
857
894
  else:
858
895
  goal = induce(template, schema, examples)
859
896
 
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}")
897
+ if mode == "jeremy":
898
+ seed = {
899
+ "goal": _compose_seed_goal(goal),
900
+ "system_prompt": template.system_prompt or "",
901
+ "examples": [
902
+ {"problem": e.prompt, "solution": e.completion} for e in examples
903
+ ],
904
+ }
905
+ out = (args.out if final_rail else None) or f"{site.symbol}.jeremy.json"
906
+ Path(out).write_text(json.dumps(seed, indent=2), encoding="utf-8")
907
+ rail(_out)
908
+ rail_ok(_out, f"wrote {out}")
909
+ rail(_out, f"site {site.symbol}")
910
+ rail(_out, f"examples {len(examples)} ({recorded_from})")
911
+ rail(_out, f"goal {seed['goal'][:90]}")
912
+ else:
913
+ profile = build_profile(site, args.repo, examples, goal, recorded_from=recorded_from)
914
+ out = (args.out if final_rail else None) or f"{site.symbol}.profile.json"
915
+ Path(out).write_text(json.dumps(profile, indent=2), encoding="utf-8")
916
+ supported = profile["classification"]["supported"]
917
+ rail(_out)
918
+ rail_ok(_out, f"wrote {out}")
919
+ rail(_out, f"site {site.symbol} · {profile['site_id'][:16]}…")
920
+ rail(_out, f"supported {supported}")
921
+ rail(_out, f"examples {len(examples)} ({recorded_from})")
922
+ rail(_out, f"goal {goal.description[:90]}")
923
+ dims = ", ".join(d["name"] for d in goal.difficulty_dimensions[:4])
924
+ rail(_out, f"difficulty {dims}")
872
925
  if len(examples) < 20:
873
926
  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")
927
+ "handoff-quality artifact wants 20+ real-traffic pairs", style="yellow")
875
928
  if final_rail:
876
929
  rail(_out)
877
930
  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.1
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,24 @@ 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": "<build's inferred goal description and constraints, joined into one string>",
174
+ "system_prompt": "<the site's own system prompt>",
175
+ "examples": [ {"problem": "...", "solution": "..."}, ... ]
176
+ }
177
+ ```
178
+
179
+ Same pipeline as `build` (captured pairs in, one goal-inference call) —
180
+ only the output shape differs; nothing is added that `build`'s own
181
+ output doesn't contain.
182
+
165
183
  ## Providers
166
184
 
167
185
  `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.1"
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,71 @@ 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("Solve a grade-school word problem")
552
+ assert "final answer is a bare number" in seed["goal"]
553
+ assert "Extract the receipt fields" in seed["system_prompt"]
554
+ assert seed["examples"] == [
555
+ {"problem": "A farmer has 6 crates...", "solution": "The answer is 57."}
556
+ ]
557
+ assert "extract_receipt.jeremy.json" in capsys.readouterr().out
558
+
559
+
560
+ def test_jeremy_multi_site(tmp_path, monkeypatch, capsys):
561
+ repo = _make_two_site_repo(tmp_path)
562
+ monkeypatch.chdir(tmp_path)
563
+ pair = {"input_slots": {}, "prompt": "p", "completion": "s",
564
+ "tokens": {"in": 1, "out": 1}, "latency_ms": 1, "_source_kind": "samples"}
565
+ for symbol in ("extract_receipt", "extract_menu"):
566
+ (tmp_path / f"{symbol}.captured.jsonl").write_text(
567
+ json.dumps(pair) + "\n", encoding="utf-8")
568
+ monkeypatch.setattr("nabla.induce.induce", _fake_goal)
569
+
570
+ rc = main(["jeremy", str(repo)])
571
+
572
+ assert rc == 0
573
+ assert (tmp_path / "extract_receipt.jeremy.json").exists()
574
+ assert (tmp_path / "extract_menu.jeremy.json").exists()
575
+ assert "built 2 seed spec(s)" in capsys.readouterr().out
576
+
577
+
578
+ def test_jeremy_without_captures_points_at_capture(tmp_path, monkeypatch, capsys):
579
+ repo = _make_single_site_repo(tmp_path)
580
+ monkeypatch.chdir(tmp_path)
581
+
582
+ rc = main(["jeremy", str(repo)])
583
+
584
+ assert rc == 2
585
+ err = capsys.readouterr().err
586
+ assert "nabla jeremy" in err
587
+ 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