rote-cli 0.7.0__tar.gz → 0.9.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.
- {rote_cli-0.7.0 → rote_cli-0.9.0}/PKG-INFO +7 -1
- {rote_cli-0.7.0 → rote_cli-0.9.0}/pyproject.toml +8 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/SKILL.md +13 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/eval-estimates.md +41 -20
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/__init__.py +1 -1
- rote_cli-0.9.0/src/rote/__main__.py +12 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/__init__.py +28 -11
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_common.py +47 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_ts_common.py +573 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/cloudflare.py +146 -15
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/dbos.py +29 -16
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/dbos_ts.py +27 -2
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/inngest.py +27 -2
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/cli.py +403 -66
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/__init__.py +4 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/empirical.py +182 -2
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/estimate.py +20 -6
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/priors.py +13 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/scorecard.py +69 -2
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/sidecar.py +30 -3
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/__init__.py +141 -4
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/__init__.py +22 -0
- rote_cli-0.9.0/src/rote/graduator/drivers/_fs_tools.py +280 -0
- rote_cli-0.9.0/src/rote/graduator/drivers/anthropic_api.py +466 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/claude.py +197 -41
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/drivers/codex.py +13 -2
- rote_cli-0.9.0/src/rote/graduator/drivers/openai_api.py +431 -0
- rote_cli-0.9.0/src/rote/graduator/events.py +255 -0
- rote_cli-0.9.0/src/rote/mcp/__init__.py +48 -0
- rote_cli-0.9.0/src/rote/mcp/_runtime_helper.py +247 -0
- rote_cli-0.9.0/src/rote/mcp/auth.py +166 -0
- rote_cli-0.9.0/src/rote/mcp/registry.py +124 -0
- rote_cli-0.9.0/src/rote/mcp/tokens.py +246 -0
- rote_cli-0.7.0/src/rote/graduator/drivers/anthropic_api.py +0 -423
- {rote_cli-0.7.0 → rote_cli-0.9.0}/.gitignore +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/LICENSE +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/README.md +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/crystallization-heuristics.md +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/ir-schema.md +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/llm-judge-extraction.md +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/skills/rote-graduate/references/node-kinds.md +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/_py_common.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/python.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/adapters/temporal.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/pricing.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/eval/tokens.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/graduator/update.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/ir.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/__init__.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/backends.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/registry.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/serve/server.py +0 -0
- {rote_cli-0.7.0 → rote_cli-0.9.0}/src/rote/skill_source.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rote-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Graduate fuzzy AI skills into deterministic, reliable workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/trevhud/rote
|
|
6
6
|
Project-URL: Repository, https://github.com/trevhud/rote
|
|
@@ -32,11 +32,17 @@ Requires-Dist: dbos>=2.26; extra == 'dev'
|
|
|
32
32
|
Requires-Dist: fastmcp>=3.4.2; extra == 'dev'
|
|
33
33
|
Requires-Dist: httpx>=0.28; extra == 'dev'
|
|
34
34
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
35
|
+
Requires-Dist: openai>=2.45; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
36
37
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
38
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
38
39
|
Requires-Dist: temporalio>=1.7; extra == 'dev'
|
|
39
40
|
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: fastmcp>=3.4.2; extra == 'mcp'
|
|
43
|
+
Requires-Dist: httpx>=0.28; extra == 'mcp'
|
|
44
|
+
Provides-Extra: openai-api
|
|
45
|
+
Requires-Dist: openai>=2.45; extra == 'openai-api'
|
|
40
46
|
Provides-Extra: serve
|
|
41
47
|
Requires-Dist: fastmcp>=3.4.2; extra == 'serve'
|
|
42
48
|
Requires-Dist: httpx>=0.28; extra == 'serve'
|
|
@@ -50,6 +50,9 @@ temporal = [
|
|
|
50
50
|
api = [
|
|
51
51
|
"anthropic>=0.89",
|
|
52
52
|
]
|
|
53
|
+
openai-api = [
|
|
54
|
+
"openai>=2.45",
|
|
55
|
+
]
|
|
53
56
|
dbos = [
|
|
54
57
|
"dbos>=2.26",
|
|
55
58
|
]
|
|
@@ -57,6 +60,10 @@ serve = [
|
|
|
57
60
|
"fastmcp>=3.4.2",
|
|
58
61
|
"httpx>=0.28",
|
|
59
62
|
]
|
|
63
|
+
mcp = [
|
|
64
|
+
"fastmcp>=3.4.2",
|
|
65
|
+
"httpx>=0.28",
|
|
66
|
+
]
|
|
60
67
|
dev = [
|
|
61
68
|
"pytest>=8.0",
|
|
62
69
|
"pytest-asyncio>=0.23",
|
|
@@ -65,6 +72,7 @@ dev = [
|
|
|
65
72
|
"types-pyyaml",
|
|
66
73
|
"temporalio>=1.7",
|
|
67
74
|
"anthropic>=0.89",
|
|
75
|
+
"openai>=2.45",
|
|
68
76
|
"dbos>=2.26",
|
|
69
77
|
"fastmcp>=3.4.2",
|
|
70
78
|
"httpx>=0.28",
|
|
@@ -46,6 +46,19 @@ re-deriving a known-fixed procedure at runtime is waste.
|
|
|
46
46
|
|
|
47
47
|
## Pipeline
|
|
48
48
|
|
|
49
|
+
**Progress markers.** At the START of each Phase N, rewrite
|
|
50
|
+
`progress.ndjson` in the work directory so it holds one JSON line per
|
|
51
|
+
phase entered so far, in order:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
{"phase": 1, "name": "Intake"}
|
|
55
|
+
{"phase": 2, "name": "Node Classification"}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Rewrite the whole file each time (append the new phase's line to the
|
|
59
|
+
ones already there). This file is machine-read for live progress
|
|
60
|
+
display; it is not a deliverable and never affects the IR.
|
|
61
|
+
|
|
49
62
|
### Phase 1: Intake
|
|
50
63
|
|
|
51
64
|
Read the target directory exhaustively:
|
|
@@ -25,6 +25,13 @@ Ground rules:
|
|
|
25
25
|
(agents batch parallel calls). If it says "repeat until you have 25
|
|
26
26
|
qualified contacts", estimate the realistic iteration count from any
|
|
27
27
|
hints (quota sizes, batch sizes, typical yields), not the worst case.
|
|
28
|
+
- **Steps that repeat per item MUST declare `iterations`.** A "process
|
|
29
|
+
each row" step costs its per-row turns *times the row count* — this
|
|
30
|
+
is almost always the dominant term, and flattening it understates the
|
|
31
|
+
whole run by 5–20×. Put the per-iteration turns in `estimated_turns`
|
|
32
|
+
and the realistic item-count range in `iterations` (page sizes,
|
|
33
|
+
pagination labels, batch quotas, and filter windows in the skill text
|
|
34
|
+
are your evidence). The loader multiplies them; never pre-multiply.
|
|
28
35
|
- **Include the overhead the prose hides.** Reading reference files,
|
|
29
36
|
re-checking rubric sections, correcting malformed tool calls — a
|
|
30
37
|
realistic agent spends turns on these. A step whose happy path is 2
|
|
@@ -54,9 +61,14 @@ steps:
|
|
|
54
61
|
phase: "1"
|
|
55
62
|
estimated_turns: {low: 1, high: 2}
|
|
56
63
|
estimated_tool_calls: {low: 4, high: 4}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
- description: "Push each eligible invoice and capture its sent date"
|
|
65
|
+
node_id: process_invoices_loop
|
|
66
|
+
phase: "Processing"
|
|
67
|
+
estimated_turns: {low: 3, high: 6} # PER ROW —
|
|
68
|
+
iterations: {low: 20, high: 90} # — × realistic row count
|
|
69
|
+
totals: # optional — omit if the sum is right;
|
|
70
|
+
low: 88 # must be WHOLE-RUN (post-iteration)
|
|
71
|
+
high: 602 # when any step declares iterations
|
|
60
72
|
notes: >-
|
|
61
73
|
Lead generation dominates: the loop re-queries ZoomInfo with adjusted
|
|
62
74
|
filters until the quota is met; typical campaigns converge in 3-5
|
|
@@ -72,30 +84,39 @@ Field reference:
|
|
|
72
84
|
| `steps[].description` | yes | The step, in the skill's own vocabulary |
|
|
73
85
|
| `steps[].node_id` | no | IR node id, only when the mapping is clean 1:1 |
|
|
74
86
|
| `steps[].phase` | no | Source skill phase, same convention as the IR |
|
|
75
|
-
| `steps[].estimated_turns` | yes | `{low, high}` agent turns |
|
|
87
|
+
| `steps[].estimated_turns` | yes | `{low, high}` agent turns (per iteration when `iterations` is set) |
|
|
76
88
|
| `steps[].estimated_tool_calls` | no | `{low, high}` tool calls, when distinct from turns |
|
|
77
|
-
| `
|
|
89
|
+
| `steps[].iterations` | when the step repeats | `{low, high}` repeats per run (rows, pages, items); whole-run cost = turns × iterations |
|
|
90
|
+
| `totals` | no | Whole-run `{low, high}`; omit to mean "sum the steps (× iterations)" |
|
|
78
91
|
| `notes` | no | The one paragraph a reader needs to trust the numbers |
|
|
79
92
|
|
|
80
93
|
Validated by `rote.eval.sidecar.EvalEstimates` — if you can run Python,
|
|
81
94
|
`load_eval_estimates("eval.yaml")` is the dry check, same pattern as
|
|
82
95
|
`load_pipeline`.
|
|
83
96
|
|
|
84
|
-
## Calibration anchors (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
## Calibration anchors (measured production runs)
|
|
98
|
+
|
|
99
|
+
Whole-run turn counts are regime-dependent — anchor to the skill's
|
|
100
|
+
*shape*, not to a universal range:
|
|
101
|
+
|
|
102
|
+
- **Sequential tool-heavy skills** (BDR-scale: 7 phases, ~20 distinct
|
|
103
|
+
steps, no per-item loop): **30–57 turns** measured. ~13 seconds per
|
|
104
|
+
turn including tool execution.
|
|
105
|
+
- **Per-item loop skills** (browser automation over a table, per-record
|
|
106
|
+
processing): measured production runs landed at **184 and 730 turns**
|
|
107
|
+
— the loop multiplier dominates everything else in the file. If a
|
|
108
|
+
skill says "for each row…", expect hundreds of turns, not tens, and
|
|
109
|
+
express it via `iterations`, never by inflating per-step turns.
|
|
110
|
+
- Per-step anchors: a single fixed API call step: 1–2 turns. A
|
|
111
|
+
batch-and-review step: 2–4. A bounded search loop: 6–15. A genuinely
|
|
112
|
+
open research step: 4–10. One iteration of a per-row browser cycle
|
|
113
|
+
(locate, act, confirm, refresh): 2–6.
|
|
114
|
+
|
|
115
|
+
If your estimate lands wildly outside the anchor for the skill's shape,
|
|
116
|
+
re-examine before writing — either the skill really is bigger/smaller
|
|
117
|
+
(fine, say so in `notes`) or a step estimate is off. A per-item skill
|
|
118
|
+
whose whole-run total sums to under ~100 turns almost certainly has a
|
|
119
|
+
missing `iterations` block.
|
|
99
120
|
|
|
100
121
|
## What NOT to do
|
|
101
122
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""``python -m rote`` — the same CLI as the ``rote`` console script.
|
|
2
|
+
|
|
3
|
+
Exists so generated configs can invoke rote through a known
|
|
4
|
+
interpreter (``sys.executable -m rote``) instead of guessing at PATH —
|
|
5
|
+
the eval harness's ``headersHelper`` wiring depends on this.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from rote.cli import main
|
|
11
|
+
|
|
12
|
+
sys.exit(main())
|
|
@@ -31,7 +31,17 @@ class Adapter(Protocol):
|
|
|
31
31
|
#: Options a factory may receive (forwarded from ``get_adapter``). Factories
|
|
32
32
|
#: swallow unknown keys via ``**options`` — mirrors the driver-registry
|
|
33
33
|
#: convention — so a runtime that doesn't support an option just ignores it.
|
|
34
|
-
#: ``external_backend`` ("mcp" | "api") is understood by the DBOS
|
|
34
|
+
#: ``external_backend`` ("mcp" | "api") is understood by the DBOS, DBOS-TS,
|
|
35
|
+
#: and Inngest adapters.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _validated_backend(options: dict[str, Any]) -> Literal["mcp", "api"] | None:
|
|
39
|
+
backend = options.get("external_backend")
|
|
40
|
+
if backend is None:
|
|
41
|
+
return None
|
|
42
|
+
if backend not in ("mcp", "api"):
|
|
43
|
+
raise ValueError(f"external_backend must be 'mcp' or 'api', got {backend!r}")
|
|
44
|
+
return cast(Literal["mcp", "api"], backend)
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
def _temporal_adapter_factory(**options: Any) -> Adapter:
|
|
@@ -43,32 +53,39 @@ def _temporal_adapter_factory(**options: Any) -> Adapter:
|
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
def _cloudflare_adapter_factory(**options: Any) -> Adapter:
|
|
46
|
-
from rote.adapters.cloudflare import CloudflareAdapter
|
|
56
|
+
from rote.adapters.cloudflare import CloudflareAdapter, CloudflareAdapterConfig
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
backend = _validated_backend(options)
|
|
59
|
+
if backend is None:
|
|
60
|
+
return CloudflareAdapter()
|
|
61
|
+
return CloudflareAdapter(CloudflareAdapterConfig(external_backend=backend))
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
def _dbos_adapter_factory(**options: Any) -> Adapter:
|
|
52
65
|
from rote.adapters.dbos import DbosAdapter, DbosAdapterConfig
|
|
53
66
|
|
|
54
|
-
backend = options
|
|
67
|
+
backend = _validated_backend(options)
|
|
55
68
|
if backend is None:
|
|
56
69
|
return DbosAdapter()
|
|
57
|
-
|
|
58
|
-
raise ValueError(f"external_backend must be 'mcp' or 'api', got {backend!r}")
|
|
59
|
-
return DbosAdapter(DbosAdapterConfig(external_backend=cast(Literal["mcp", "api"], backend)))
|
|
70
|
+
return DbosAdapter(DbosAdapterConfig(external_backend=backend))
|
|
60
71
|
|
|
61
72
|
|
|
62
73
|
def _dbos_ts_adapter_factory(**options: Any) -> Adapter:
|
|
63
|
-
from rote.adapters.dbos_ts import DbosTsAdapter
|
|
74
|
+
from rote.adapters.dbos_ts import DbosTsAdapter, DbosTsAdapterConfig
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
backend = _validated_backend(options)
|
|
77
|
+
if backend is None:
|
|
78
|
+
return DbosTsAdapter()
|
|
79
|
+
return DbosTsAdapter(DbosTsAdapterConfig(external_backend=backend))
|
|
66
80
|
|
|
67
81
|
|
|
68
82
|
def _inngest_adapter_factory(**options: Any) -> Adapter:
|
|
69
|
-
from rote.adapters.inngest import InngestAdapter
|
|
83
|
+
from rote.adapters.inngest import InngestAdapter, InngestAdapterConfig
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
backend = _validated_backend(options)
|
|
86
|
+
if backend is None:
|
|
87
|
+
return InngestAdapter()
|
|
88
|
+
return InngestAdapter(InngestAdapterConfig(external_backend=backend))
|
|
72
89
|
|
|
73
90
|
|
|
74
91
|
def _python_adapter_factory(**options: Any) -> Adapter:
|
|
@@ -194,6 +194,27 @@ class EmitWriter:
|
|
|
194
194
|
)
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
class EmitResult(dict[str, Path]):
|
|
198
|
+
"""An adapter emit mapping that also carries preserved-conflict paths.
|
|
199
|
+
|
|
200
|
+
It *is* the ``label -> written path`` dict every ``emit()`` has always
|
|
201
|
+
returned (the path is the ``.new`` sibling when a user-edited file was
|
|
202
|
+
preserved), so existing consumers that iterate it keep working
|
|
203
|
+
unchanged. The extra :attr:`preserved` attribute exposes the
|
|
204
|
+
:class:`EmitWriter`'s ``rel -> .new path`` map directly, for callers
|
|
205
|
+
(the cloud runner) that want the conflict set without sniffing ``.new``
|
|
206
|
+
suffixes off the values.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
written: dict[str, Path] | None = None,
|
|
212
|
+
preserved: dict[str, Path] | None = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
super().__init__(written or {})
|
|
215
|
+
self.preserved: dict[str, Path] = dict(preserved or {})
|
|
216
|
+
|
|
217
|
+
|
|
197
218
|
# ───────── Case conversion ─────────
|
|
198
219
|
|
|
199
220
|
|
|
@@ -238,6 +259,32 @@ def _pipeline_hash(pipeline: Pipeline) -> str:
|
|
|
238
259
|
return hashlib.sha256(payload.encode()).hexdigest()[:8]
|
|
239
260
|
|
|
240
261
|
|
|
262
|
+
def workflow_class_name(pipeline: Pipeline) -> str:
|
|
263
|
+
"""The generated workflow class name: ``<PascalName>Workflow``.
|
|
264
|
+
|
|
265
|
+
The Cloudflare and Temporal adapters both name their emitted
|
|
266
|
+
workflow class this way; centralizing it here keeps the manifest the
|
|
267
|
+
cloud consumes and the emitted TypeScript from drifting apart.
|
|
268
|
+
"""
|
|
269
|
+
return f"{_to_pascal_case(pipeline.name)}Workflow"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def pipeline_identity(pipeline: Pipeline) -> dict[str, str]:
|
|
273
|
+
"""Stable identity fields for a pipeline, shared by emitters and manifests.
|
|
274
|
+
|
|
275
|
+
Returns ``{name, version, pipeline_hash, class_name}`` — the same four
|
|
276
|
+
values the Cloudflare adapter bakes into the workflow.ts header and
|
|
277
|
+
writes into ``manifest.json`` for the cloud runner. Single source of
|
|
278
|
+
truth so the two can't disagree.
|
|
279
|
+
"""
|
|
280
|
+
return {
|
|
281
|
+
"name": pipeline.name,
|
|
282
|
+
"version": pipeline.version,
|
|
283
|
+
"pipeline_hash": _pipeline_hash(pipeline),
|
|
284
|
+
"class_name": workflow_class_name(pipeline),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
241
288
|
# ───────── Duration strings ─────────
|
|
242
289
|
|
|
243
290
|
_IR_DURATION_RE = re.compile(r"^(\d+(?:\.\d+)?)(ms|s|m|h|d)$")
|