runapprentice 0.1.0__tar.gz → 0.2.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.
- {runapprentice-0.1.0 → runapprentice-0.2.0}/PKG-INFO +47 -7
- {runapprentice-0.1.0 → runapprentice-0.2.0}/README.md +42 -6
- {runapprentice-0.1.0 → runapprentice-0.2.0}/pyproject.toml +6 -1
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/__init__.py +4 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/_capture.py +10 -17
- runapprentice-0.2.0/src/runapprentice/cli.py +447 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/client.py +114 -12
- runapprentice-0.2.0/src/runapprentice/local.py +612 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/models.py +19 -0
- runapprentice-0.2.0/tests/test_cli.py +352 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_client.py +153 -2
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_dispatcher.py +23 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_failopen.py +1 -1
- runapprentice-0.2.0/tests/test_local.py +387 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/.gitignore +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/LICENSE +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/langchain.py +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/openai.py +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/src/runapprentice/py.typed +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_capture.py +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_langchain_prompts.py +0 -0
- {runapprentice-0.1.0 → runapprentice-0.2.0}/tests/test_openai.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runapprentice
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Cut LLM cost without losing quality: capture traffic, build golden datasets, and optimize prompts.
|
|
5
5
|
Project-URL: Homepage, https://runapprentice.com
|
|
6
6
|
Project-URL: Documentation, https://docs.runapprentice.com
|
|
@@ -21,18 +21,44 @@ Requires-Dist: httpx>=0.27
|
|
|
21
21
|
Requires-Dist: pydantic>=2.0
|
|
22
22
|
Provides-Extra: langchain
|
|
23
23
|
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
24
|
+
Provides-Extra: local
|
|
25
|
+
Requires-Dist: mlx-lm<0.32,>=0.31.3; extra == 'local'
|
|
26
|
+
Provides-Extra: optimize
|
|
27
|
+
Requires-Dist: dspy<4,>=3.2.1; extra == 'optimize'
|
|
24
28
|
Description-Content-Type: text/markdown
|
|
25
29
|
|
|
26
30
|
# runapprentice
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
The Python SDK for [Apprentice](https://runapprentice.com), a tool that cuts frontier-LLM cost
|
|
33
|
+
on repeatable tasks. You give it a dataset of inputs and correct outputs for one task (uploaded
|
|
34
|
+
as a CSV, or captured live from a LangChain app with one callback). Apprentice runs prompt
|
|
35
|
+
optimization (DSPy GEPA) against the verified rows (gold plus silver), or fine-tunes a small open model on
|
|
36
|
+
your Mac, and reports the score change on a held-out slice, so the gain is measured, never
|
|
37
|
+
claimed. Gated, eval-verified takeover of production traffic is still in development.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
Docs: [docs.runapprentice.com](https://docs.runapprentice.com) · Reproducible benchmark:
|
|
40
|
+
[apprentice-benchmark](https://github.com/singhabhishekkk/apprentice-benchmark)
|
|
41
|
+
|
|
42
|
+
This README documents the **implemented surface**. Anything not shown here does not exist yet.
|
|
43
|
+
|
|
44
|
+
## CLI (no code, no account required)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add 'runapprentice[optimize]' # add [local] too for MLX fine-tuning on Apple silicon
|
|
48
|
+
|
|
49
|
+
apprentice optimize <task> --local --data golden.csv # free, your OPENAI_API_KEY
|
|
50
|
+
apprentice train <task> --local --data golden.csv --effort high # free, fine-tunes on your Mac
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both commands work on a CSV alone, no Apprentice account. No golden dataset yet? `apprentice
|
|
54
|
+
login` also lets Apprentice generate a starter set and route rows to a subject-matter expert
|
|
55
|
+
for review, and tracks every run in the console. `apprentice --help` (or any
|
|
56
|
+
subcommand `--help`) documents every flag; `--json` gives clean machine-readable output.
|
|
31
57
|
|
|
32
58
|
## Install
|
|
33
59
|
|
|
34
60
|
```bash
|
|
35
|
-
uv add runapprentice[langchain] # [langchain] extra enables zero-code capture
|
|
61
|
+
uv add 'runapprentice[langchain]' # [langchain] extra enables zero-code capture
|
|
36
62
|
```
|
|
37
63
|
|
|
38
64
|
## Quickstart (the whole trial journey)
|
|
@@ -163,11 +189,25 @@ want grounding + refusal optimized together.
|
|
|
163
189
|
|
|
164
190
|
## Feedback (your end-users' signal)
|
|
165
191
|
|
|
192
|
+
Capture records the call; feedback records whether it worked. That score is what the
|
|
193
|
+
console's Drift tab charts, and what decides when a retrain is worth doing.
|
|
194
|
+
|
|
166
195
|
```python
|
|
167
|
-
client.
|
|
168
|
-
|
|
196
|
+
trace_id = client.capture(task="support-triage", input=question, output=answer)
|
|
197
|
+
if trace_id: # None means the capture was rejected locally; capture never raises
|
|
198
|
+
client.feedback(trace_id, good=True) # thumbs
|
|
199
|
+
client.feedback(trace_id, score=0.5, note="partial") # graded
|
|
169
200
|
```
|
|
170
201
|
|
|
202
|
+
In async mode (the default), `feedback()` first waits up to two seconds for pending
|
|
203
|
+
captures to deliver, so it can never race its own trace; if the queue cannot drain in
|
|
204
|
+
time it raises `ApprenticeError` instead of sending a request that would 404.
|
|
205
|
+
|
|
206
|
+
Wire it to a signal you already have: a thumbs up or down, the user accepting or discarding
|
|
207
|
+
the output, a downstream check that passed or failed. Do not add a second model to grade the
|
|
208
|
+
first one, this score decides when to retrain. With no real signal, send nothing: captured
|
|
209
|
+
rows still become gold when a human verifies them.
|
|
210
|
+
|
|
171
211
|
## Debugging
|
|
172
212
|
|
|
173
213
|
```python
|
|
@@ -195,7 +235,7 @@ count and the minimum).
|
|
|
195
235
|
| `job.report()` | `OptimizationReport(baseline_score, optimized_score, optimized_prompt, ...)` | if job has no report |
|
|
196
236
|
| `client.prompts.get(task, version=None)` | `PromptVersion(version, text, score)` | 404 if never optimized |
|
|
197
237
|
| `client.prompts.history(task)` | `list[PromptVersion]` | `ApprenticeError` on API failure |
|
|
198
|
-
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace |
|
|
238
|
+
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace; `ApprenticeError` if pending captures cannot drain in time, or on API failure |
|
|
199
239
|
| `client.post_trace_failopen(record)` | trace_id or None | **never raises** |
|
|
200
240
|
| `ApprenticeCallback(task, client, redact=None)` | LangChain callback | **never raises into your chain** |
|
|
201
241
|
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
# runapprentice
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Python SDK for [Apprentice](https://runapprentice.com), a tool that cuts frontier-LLM cost
|
|
4
|
+
on repeatable tasks. You give it a dataset of inputs and correct outputs for one task (uploaded
|
|
5
|
+
as a CSV, or captured live from a LangChain app with one callback). Apprentice runs prompt
|
|
6
|
+
optimization (DSPy GEPA) against the verified rows (gold plus silver), or fine-tunes a small open model on
|
|
7
|
+
your Mac, and reports the score change on a held-out slice, so the gain is measured, never
|
|
8
|
+
claimed. Gated, eval-verified takeover of production traffic is still in development.
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
Docs: [docs.runapprentice.com](https://docs.runapprentice.com) · Reproducible benchmark:
|
|
11
|
+
[apprentice-benchmark](https://github.com/singhabhishekkk/apprentice-benchmark)
|
|
12
|
+
|
|
13
|
+
This README documents the **implemented surface**. Anything not shown here does not exist yet.
|
|
14
|
+
|
|
15
|
+
## CLI (no code, no account required)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add 'runapprentice[optimize]' # add [local] too for MLX fine-tuning on Apple silicon
|
|
19
|
+
|
|
20
|
+
apprentice optimize <task> --local --data golden.csv # free, your OPENAI_API_KEY
|
|
21
|
+
apprentice train <task> --local --data golden.csv --effort high # free, fine-tunes on your Mac
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Both commands work on a CSV alone, no Apprentice account. No golden dataset yet? `apprentice
|
|
25
|
+
login` also lets Apprentice generate a starter set and route rows to a subject-matter expert
|
|
26
|
+
for review, and tracks every run in the console. `apprentice --help` (or any
|
|
27
|
+
subcommand `--help`) documents every flag; `--json` gives clean machine-readable output.
|
|
6
28
|
|
|
7
29
|
## Install
|
|
8
30
|
|
|
9
31
|
```bash
|
|
10
|
-
uv add runapprentice[langchain] # [langchain] extra enables zero-code capture
|
|
32
|
+
uv add 'runapprentice[langchain]' # [langchain] extra enables zero-code capture
|
|
11
33
|
```
|
|
12
34
|
|
|
13
35
|
## Quickstart (the whole trial journey)
|
|
@@ -138,11 +160,25 @@ want grounding + refusal optimized together.
|
|
|
138
160
|
|
|
139
161
|
## Feedback (your end-users' signal)
|
|
140
162
|
|
|
163
|
+
Capture records the call; feedback records whether it worked. That score is what the
|
|
164
|
+
console's Drift tab charts, and what decides when a retrain is worth doing.
|
|
165
|
+
|
|
141
166
|
```python
|
|
142
|
-
client.
|
|
143
|
-
|
|
167
|
+
trace_id = client.capture(task="support-triage", input=question, output=answer)
|
|
168
|
+
if trace_id: # None means the capture was rejected locally; capture never raises
|
|
169
|
+
client.feedback(trace_id, good=True) # thumbs
|
|
170
|
+
client.feedback(trace_id, score=0.5, note="partial") # graded
|
|
144
171
|
```
|
|
145
172
|
|
|
173
|
+
In async mode (the default), `feedback()` first waits up to two seconds for pending
|
|
174
|
+
captures to deliver, so it can never race its own trace; if the queue cannot drain in
|
|
175
|
+
time it raises `ApprenticeError` instead of sending a request that would 404.
|
|
176
|
+
|
|
177
|
+
Wire it to a signal you already have: a thumbs up or down, the user accepting or discarding
|
|
178
|
+
the output, a downstream check that passed or failed. Do not add a second model to grade the
|
|
179
|
+
first one, this score decides when to retrain. With no real signal, send nothing: captured
|
|
180
|
+
rows still become gold when a human verifies them.
|
|
181
|
+
|
|
146
182
|
## Debugging
|
|
147
183
|
|
|
148
184
|
```python
|
|
@@ -170,7 +206,7 @@ count and the minimum).
|
|
|
170
206
|
| `job.report()` | `OptimizationReport(baseline_score, optimized_score, optimized_prompt, ...)` | if job has no report |
|
|
171
207
|
| `client.prompts.get(task, version=None)` | `PromptVersion(version, text, score)` | 404 if never optimized |
|
|
172
208
|
| `client.prompts.history(task)` | `list[PromptVersion]` | `ApprenticeError` on API failure |
|
|
173
|
-
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace |
|
|
209
|
+
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace; `ApprenticeError` if pending captures cannot drain in time, or on API failure |
|
|
174
210
|
| `client.post_trace_failopen(record)` | trace_id or None | **never raises** |
|
|
175
211
|
| `ApprenticeCallback(task, client, redact=None)` | LangChain callback | **never raises into your chain** |
|
|
176
212
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "runapprentice"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Cut LLM cost without losing quality: capture traffic, build golden datasets, and optimize prompts."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -23,12 +23,17 @@ dependencies = [
|
|
|
23
23
|
"pydantic>=2.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
[project.scripts]
|
|
27
|
+
apprentice = "runapprentice.cli:main"
|
|
28
|
+
|
|
26
29
|
[project.urls]
|
|
27
30
|
Homepage = "https://runapprentice.com"
|
|
28
31
|
Documentation = "https://docs.runapprentice.com"
|
|
29
32
|
|
|
30
33
|
[project.optional-dependencies]
|
|
31
34
|
langchain = ["langchain-core>=0.3"]
|
|
35
|
+
local = ["mlx-lm>=0.31.3,<0.32"]
|
|
36
|
+
optimize = ["dspy>=3.2.1,<4"]
|
|
32
37
|
|
|
33
38
|
[dependency-groups]
|
|
34
39
|
dev = [
|
|
@@ -5,6 +5,8 @@ from runapprentice.models import (
|
|
|
5
5
|
DatasetRow,
|
|
6
6
|
DatasetStatus,
|
|
7
7
|
JobInfo,
|
|
8
|
+
JobSummary,
|
|
9
|
+
LocalRun,
|
|
8
10
|
OptimizationReport,
|
|
9
11
|
PromptVersion,
|
|
10
12
|
TraceRecord,
|
|
@@ -18,6 +20,8 @@ __all__ = [
|
|
|
18
20
|
"DatasetRow",
|
|
19
21
|
"DatasetStatus",
|
|
20
22
|
"JobInfo",
|
|
23
|
+
"JobSummary",
|
|
24
|
+
"LocalRun",
|
|
21
25
|
"OptimizationReport",
|
|
22
26
|
"PromptVersion",
|
|
23
27
|
"TraceRecord",
|
|
@@ -8,8 +8,8 @@ blip must not silently drop a trace on the first try). This dispatcher gives bot
|
|
|
8
8
|
Apprentice's network).
|
|
9
9
|
- A single daemon worker drains the queue and delivers each trace with bounded
|
|
10
10
|
retry + exponential backoff.
|
|
11
|
-
- The queue is bounded; if Apprentice's backend is down long enough to fill it,
|
|
12
|
-
|
|
11
|
+
- The queue is bounded; if Apprentice's backend is down long enough to fill it, new
|
|
12
|
+
traces are rejected (fail-open; the customer app is never affected).
|
|
13
13
|
- `flush()` (also run at interpreter exit) drains in-flight traces so short scripts
|
|
14
14
|
don't lose their last captures.
|
|
15
15
|
"""
|
|
@@ -55,29 +55,20 @@ class CaptureDispatcher:
|
|
|
55
55
|
self._worker.start()
|
|
56
56
|
atexit.register(self.flush)
|
|
57
57
|
|
|
58
|
-
def submit(self, record: TraceRecord) ->
|
|
59
|
-
"""Enqueue
|
|
58
|
+
def submit(self, record: TraceRecord) -> bool:
|
|
59
|
+
"""Enqueue without blocking. Return whether the trace was accepted locally."""
|
|
60
60
|
try:
|
|
61
61
|
self._queue.put_nowait(record)
|
|
62
|
+
return True
|
|
62
63
|
except queue.Full:
|
|
63
|
-
# Backend is unreachable and the buffer is full — drop the oldest pending
|
|
64
|
-
# trace to make room for the newest, so we keep the freshest signal.
|
|
65
|
-
try:
|
|
66
|
-
self._queue.get_nowait()
|
|
67
|
-
self._queue.task_done()
|
|
68
|
-
except queue.Empty:
|
|
69
|
-
pass
|
|
70
64
|
self._dropped += 1
|
|
71
65
|
if self._dropped == 1 or self._dropped % 100 == 0:
|
|
72
66
|
logger.warning(
|
|
73
|
-
"runapprentice: capture buffer full,
|
|
67
|
+
"runapprentice: capture buffer full, rejected %d trace(s) "
|
|
74
68
|
"(backend unreachable; app unaffected)",
|
|
75
69
|
self._dropped,
|
|
76
70
|
)
|
|
77
|
-
|
|
78
|
-
self._queue.put_nowait(record)
|
|
79
|
-
except queue.Full:
|
|
80
|
-
pass
|
|
71
|
+
return False
|
|
81
72
|
|
|
82
73
|
def _run(self) -> None:
|
|
83
74
|
while True:
|
|
@@ -103,15 +94,17 @@ class CaptureDispatcher:
|
|
|
103
94
|
if last_error is not None:
|
|
104
95
|
self._on_exhausted(f"capture failed after {self._retries + 1} attempts: {last_error}")
|
|
105
96
|
|
|
106
|
-
def flush(self, timeout: float = 2.0) ->
|
|
97
|
+
def flush(self, timeout: float = 2.0) -> bool:
|
|
107
98
|
"""Wait up to `timeout` seconds for in-flight traces to finish delivering.
|
|
108
99
|
|
|
109
100
|
Counts items as done only after the worker calls `task_done()` (i.e. after
|
|
110
101
|
delivery + any retries complete), not merely when they leave the queue.
|
|
102
|
+
Returns whether every accepted trace completed within the timeout.
|
|
111
103
|
"""
|
|
112
104
|
deadline = time.monotonic() + timeout
|
|
113
105
|
while self._queue.unfinished_tasks > 0 and time.monotonic() < deadline:
|
|
114
106
|
time.sleep(0.02)
|
|
107
|
+
return self._queue.unfinished_tasks == 0
|
|
115
108
|
|
|
116
109
|
def close(self) -> None:
|
|
117
110
|
self._queue.put(_SHUTDOWN)
|