runapprentice 0.2.0__tar.gz → 0.3.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.2.0 → runapprentice-0.3.0}/.gitignore +17 -1
- {runapprentice-0.2.0 → runapprentice-0.3.0}/PKG-INFO +17 -3
- {runapprentice-0.2.0 → runapprentice-0.3.0}/README.md +16 -2
- {runapprentice-0.2.0 → runapprentice-0.3.0}/pyproject.toml +1 -1
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/__init__.py +16 -1
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/cli.py +73 -3
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/client.py +56 -5
- runapprentice-0.3.0/src/runapprentice/models.py +179 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_cli.py +39 -1
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_client.py +210 -4
- runapprentice-0.2.0/src/runapprentice/models.py +0 -81
- {runapprentice-0.2.0 → runapprentice-0.3.0}/LICENSE +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/_capture.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/langchain.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/local.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/openai.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/src/runapprentice/py.typed +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_capture.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_dispatcher.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_failopen.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_langchain_prompts.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_local.py +0 -0
- {runapprentice-0.2.0 → runapprentice-0.3.0}/tests/test_openai.py +0 -0
|
@@ -34,5 +34,21 @@ optimized_program.json
|
|
|
34
34
|
apprentice-ui/codex_working.md
|
|
35
35
|
.codex/
|
|
36
36
|
|
|
37
|
-
# Local dev databases
|
|
37
|
+
# Local dev databases (and their manual backups)
|
|
38
38
|
*.db
|
|
39
|
+
*.db.bak*
|
|
40
|
+
|
|
41
|
+
# Local machine config with secrets — never commit
|
|
42
|
+
.mcp.json
|
|
43
|
+
|
|
44
|
+
# Local working artifacts, not repo content
|
|
45
|
+
# (.claude/skills holds symlinks into the ignored .agents/ dir — dangling on a fresh clone)
|
|
46
|
+
.claude/skills/
|
|
47
|
+
outputs/
|
|
48
|
+
test-sdk/
|
|
49
|
+
.agents/
|
|
50
|
+
skills-lock.json
|
|
51
|
+
docs/personal_learning.md
|
|
52
|
+
.uv-cache/
|
|
53
|
+
spike/Capture-*.png
|
|
54
|
+
apprentice-ui/ClaudeDesign/screenshots/localhost-*.png
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runapprentice
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -230,9 +230,23 @@ count and the minimum).
|
|
|
230
230
|
| `client.datasets.status(task)` | `DatasetStatus(gold, silver, raw, ready_for_optimization)` | same |
|
|
231
231
|
| `client.prompts.register(task, template)` | dict | `ImportError` if a LangChain template is passed without the `[langchain]` extra |
|
|
232
232
|
| `client.prompts.to_langchain(task= or artifact=)` | LangChain prompt | `ImportError` without the `[langchain]` extra |
|
|
233
|
-
| `client.optimize(task)` | `Job` | 400 if fewer than the backend's configured verified-row minimum (20 by default) |
|
|
233
|
+
| `client.optimize(task, baseline_prompt=None, budget=None, metric=None)` | `Job` | 400 if fewer than the backend's configured verified-row minimum (20 by default) |
|
|
234
|
+
| `client.preview_optimization(task, baseline_prompt=None, budget=None, metric=None)` | `OptimizationPreview` | `ApprenticeError` on API failure |
|
|
235
|
+
| `client.train(task)` | `Job` | 400 if fewer gold rows than the backend's training minimum |
|
|
234
236
|
| `client.job(job_id)` / `job.refresh()` / `job.wait(poll_seconds=, timeout_seconds=)` | `Job` | `ApprenticeError` on timeout |
|
|
235
|
-
| `
|
|
237
|
+
| `client.cancel_job(job_id)` | dict (`cancel_requested`, `status`) | 409 if job is not cancellable |
|
|
238
|
+
| `job.cancel()` | dict (`cancel_requested`, `status`) | 409 if job is not cancellable |
|
|
239
|
+
| `client.jobs(task)` | `list[JobSummary]` | `ApprenticeError` on API failure |
|
|
240
|
+
| `client.local_runs(task)` | `list[LocalRun]` | `ApprenticeError` on API failure |
|
|
241
|
+
| `client.datasets.rows(task, tier, limit=200)` | `list[DatasetRow]` | `ApprenticeError` on API failure |
|
|
242
|
+
| `client.optimize_local(task, data=None, **options)` | dict (local GEPA result) | `ApprenticeError` if no `data` and no API key |
|
|
243
|
+
| `client.train_local(task, data=None, effort=None, **options)` | dict (local MLX result) | `ApprenticeError` if no `data` and no API key |
|
|
244
|
+
| `client.register_local_run(task, base_model, dataset_row_count, seed, config=None)` | `LocalRun` | `ApprenticeError` on API failure |
|
|
245
|
+
| `client.complete_local_run(run_id, payload)` | dict | `ApprenticeError` on API failure |
|
|
246
|
+
| `client.capture(task, output, input= or inputs=, model=, latency_ms=, ...)` | trace_id or None | **never raises** |
|
|
247
|
+
| `client.flush_captures(timeout=2.0)` | None (blocks until buffered traces deliver or timeout; no-op in sync mode) | nothing by design |
|
|
248
|
+
| `client.close()` | None (flushes and closes transport) | nothing by design |
|
|
249
|
+
| `job.report()` | typed optimize, train, or generate report; raw dict for future shapes | if job has no report |
|
|
236
250
|
| `client.prompts.get(task, version=None)` | `PromptVersion(version, text, score)` | 404 if never optimized |
|
|
237
251
|
| `client.prompts.history(task)` | `list[PromptVersion]` | `ApprenticeError` on API failure |
|
|
238
252
|
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace; `ApprenticeError` if pending captures cannot drain in time, or on API failure |
|
|
@@ -201,9 +201,23 @@ count and the minimum).
|
|
|
201
201
|
| `client.datasets.status(task)` | `DatasetStatus(gold, silver, raw, ready_for_optimization)` | same |
|
|
202
202
|
| `client.prompts.register(task, template)` | dict | `ImportError` if a LangChain template is passed without the `[langchain]` extra |
|
|
203
203
|
| `client.prompts.to_langchain(task= or artifact=)` | LangChain prompt | `ImportError` without the `[langchain]` extra |
|
|
204
|
-
| `client.optimize(task)` | `Job` | 400 if fewer than the backend's configured verified-row minimum (20 by default) |
|
|
204
|
+
| `client.optimize(task, baseline_prompt=None, budget=None, metric=None)` | `Job` | 400 if fewer than the backend's configured verified-row minimum (20 by default) |
|
|
205
|
+
| `client.preview_optimization(task, baseline_prompt=None, budget=None, metric=None)` | `OptimizationPreview` | `ApprenticeError` on API failure |
|
|
206
|
+
| `client.train(task)` | `Job` | 400 if fewer gold rows than the backend's training minimum |
|
|
205
207
|
| `client.job(job_id)` / `job.refresh()` / `job.wait(poll_seconds=, timeout_seconds=)` | `Job` | `ApprenticeError` on timeout |
|
|
206
|
-
| `
|
|
208
|
+
| `client.cancel_job(job_id)` | dict (`cancel_requested`, `status`) | 409 if job is not cancellable |
|
|
209
|
+
| `job.cancel()` | dict (`cancel_requested`, `status`) | 409 if job is not cancellable |
|
|
210
|
+
| `client.jobs(task)` | `list[JobSummary]` | `ApprenticeError` on API failure |
|
|
211
|
+
| `client.local_runs(task)` | `list[LocalRun]` | `ApprenticeError` on API failure |
|
|
212
|
+
| `client.datasets.rows(task, tier, limit=200)` | `list[DatasetRow]` | `ApprenticeError` on API failure |
|
|
213
|
+
| `client.optimize_local(task, data=None, **options)` | dict (local GEPA result) | `ApprenticeError` if no `data` and no API key |
|
|
214
|
+
| `client.train_local(task, data=None, effort=None, **options)` | dict (local MLX result) | `ApprenticeError` if no `data` and no API key |
|
|
215
|
+
| `client.register_local_run(task, base_model, dataset_row_count, seed, config=None)` | `LocalRun` | `ApprenticeError` on API failure |
|
|
216
|
+
| `client.complete_local_run(run_id, payload)` | dict | `ApprenticeError` on API failure |
|
|
217
|
+
| `client.capture(task, output, input= or inputs=, model=, latency_ms=, ...)` | trace_id or None | **never raises** |
|
|
218
|
+
| `client.flush_captures(timeout=2.0)` | None (blocks until buffered traces deliver or timeout; no-op in sync mode) | nothing by design |
|
|
219
|
+
| `client.close()` | None (flushes and closes transport) | nothing by design |
|
|
220
|
+
| `job.report()` | typed optimize, train, or generate report; raw dict for future shapes | if job has no report |
|
|
207
221
|
| `client.prompts.get(task, version=None)` | `PromptVersion(version, text, score)` | 404 if never optimized |
|
|
208
222
|
| `client.prompts.history(task)` | `list[PromptVersion]` | `ApprenticeError` on API failure |
|
|
209
223
|
| `client.feedback(trace_id, good=, score=, note=)` | None | 404 unknown trace; `ApprenticeError` if pending captures cannot drain in time, or on API failure |
|
|
@@ -4,12 +4,16 @@ from runapprentice.client import Apprentice, ApprenticeError, Job, enable_debug_
|
|
|
4
4
|
from runapprentice.models import (
|
|
5
5
|
DatasetRow,
|
|
6
6
|
DatasetStatus,
|
|
7
|
+
GenerationReport,
|
|
7
8
|
JobInfo,
|
|
9
|
+
JobReport,
|
|
8
10
|
JobSummary,
|
|
9
11
|
LocalRun,
|
|
10
12
|
OptimizationReport,
|
|
13
|
+
OptimizationPreview,
|
|
11
14
|
PromptVersion,
|
|
12
15
|
TraceRecord,
|
|
16
|
+
TrainingReport,
|
|
13
17
|
)
|
|
14
18
|
|
|
15
19
|
__all__ = [
|
|
@@ -19,12 +23,23 @@ __all__ = [
|
|
|
19
23
|
"Job",
|
|
20
24
|
"DatasetRow",
|
|
21
25
|
"DatasetStatus",
|
|
26
|
+
"GenerationReport",
|
|
22
27
|
"JobInfo",
|
|
28
|
+
"JobReport",
|
|
23
29
|
"JobSummary",
|
|
24
30
|
"LocalRun",
|
|
25
31
|
"OptimizationReport",
|
|
32
|
+
"OptimizationPreview",
|
|
26
33
|
"PromptVersion",
|
|
27
34
|
"TraceRecord",
|
|
35
|
+
"TrainingReport",
|
|
28
36
|
]
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
# Single source of truth is pyproject.toml; read it from package metadata so
|
|
39
|
+
# this string can never drift from what PyPI serves (it did once, at 0.2.0).
|
|
40
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
__version__ = _version("runapprentice")
|
|
44
|
+
except PackageNotFoundError: # running from a source tree without installation
|
|
45
|
+
__version__ = "0.0.0.dev0"
|
|
@@ -13,7 +13,13 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from runapprentice.client import Apprentice, ApprenticeError
|
|
15
15
|
from runapprentice import local as _local
|
|
16
|
-
from runapprentice.models import
|
|
16
|
+
from runapprentice.models import (
|
|
17
|
+
JobInfo,
|
|
18
|
+
JobReport,
|
|
19
|
+
JobSummary,
|
|
20
|
+
OptimizationPreview,
|
|
21
|
+
OptimizationReport,
|
|
22
|
+
)
|
|
17
23
|
|
|
18
24
|
CONFIG_DIR = Path("~/.apprentice").expanduser()
|
|
19
25
|
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
@@ -107,6 +113,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
107
113
|
type=Path,
|
|
108
114
|
help="CSV with input,output columns; required for --local unless an API key is set",
|
|
109
115
|
)
|
|
116
|
+
optimize.add_argument(
|
|
117
|
+
"--budget",
|
|
118
|
+
choices=("quick", "thorough"),
|
|
119
|
+
help="hosted optimization budget; omit for balanced default",
|
|
120
|
+
)
|
|
121
|
+
optimize.add_argument("--metric", help="metric used to score this hosted run")
|
|
122
|
+
optimize.add_argument(
|
|
123
|
+
"--prompt-file",
|
|
124
|
+
type=Path,
|
|
125
|
+
help="read hosted run baseline prompt from this UTF-8 text file",
|
|
126
|
+
)
|
|
127
|
+
optimize.add_argument(
|
|
128
|
+
"--preview",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help="show hosted metric and cost estimate without enqueueing a run",
|
|
131
|
+
)
|
|
110
132
|
optimize.set_defaults(func=_optimize)
|
|
111
133
|
|
|
112
134
|
train = sub.add_parser(
|
|
@@ -153,6 +175,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
153
175
|
job = sub.add_parser("job", help="details for one hosted job")
|
|
154
176
|
job.add_argument("job_id", help="job id from `apprentice jobs`")
|
|
155
177
|
job.set_defaults(func=_job)
|
|
178
|
+
|
|
179
|
+
cancel = sub.add_parser("cancel", help="cancel a queued or running hosted job")
|
|
180
|
+
cancel.add_argument("job_id", help="job id from `apprentice jobs`")
|
|
181
|
+
cancel.set_defaults(func=_cancel)
|
|
156
182
|
return parser
|
|
157
183
|
|
|
158
184
|
|
|
@@ -180,6 +206,11 @@ def _status(args: argparse.Namespace, client: Apprentice) -> int:
|
|
|
180
206
|
|
|
181
207
|
def _optimize(args: argparse.Namespace, client: Apprentice) -> int:
|
|
182
208
|
if args.local:
|
|
209
|
+
if args.budget or args.metric or args.prompt_file or args.preview:
|
|
210
|
+
return _fail(
|
|
211
|
+
"--budget, --metric, --prompt-file, and --preview apply to hosted optimization",
|
|
212
|
+
bool(args.json),
|
|
213
|
+
)
|
|
183
214
|
return _optimize_local(
|
|
184
215
|
args.task,
|
|
185
216
|
args.data,
|
|
@@ -187,7 +218,30 @@ def _optimize(args: argparse.Namespace, client: Apprentice) -> int:
|
|
|
187
218
|
bool(args.json),
|
|
188
219
|
bool(args._api_key_present),
|
|
189
220
|
)
|
|
190
|
-
|
|
221
|
+
baseline_prompt = None
|
|
222
|
+
if args.prompt_file is not None:
|
|
223
|
+
try:
|
|
224
|
+
baseline_prompt = args.prompt_file.read_text(encoding="utf-8")
|
|
225
|
+
except OSError as exc:
|
|
226
|
+
raise ApprenticeError(f"could not read prompt file {args.prompt_file}: {exc}") from exc
|
|
227
|
+
if args.preview:
|
|
228
|
+
preview = client.preview_optimization(
|
|
229
|
+
args.task,
|
|
230
|
+
baseline_prompt=baseline_prompt,
|
|
231
|
+
budget=args.budget,
|
|
232
|
+
metric=args.metric,
|
|
233
|
+
)
|
|
234
|
+
return _emit(
|
|
235
|
+
preview.model_dump(mode="json"),
|
|
236
|
+
_preview_line(preview),
|
|
237
|
+
bool(args.json),
|
|
238
|
+
)
|
|
239
|
+
job = client.optimize(
|
|
240
|
+
args.task,
|
|
241
|
+
baseline_prompt=baseline_prompt,
|
|
242
|
+
budget=args.budget,
|
|
243
|
+
metric=args.metric,
|
|
244
|
+
).wait()
|
|
191
245
|
payload = job.info.model_dump(mode="json")
|
|
192
246
|
return _emit(payload, _report_line(job.info.report), bool(args.json))
|
|
193
247
|
|
|
@@ -222,6 +276,11 @@ def _job(args: argparse.Namespace, client: Apprentice) -> int:
|
|
|
222
276
|
return _emit(job.info.model_dump(mode="json"), _job_info_line(job.info), bool(args.json))
|
|
223
277
|
|
|
224
278
|
|
|
279
|
+
def _cancel(args: argparse.Namespace, client: Apprentice) -> int:
|
|
280
|
+
result = client.cancel_job(args.job_id)
|
|
281
|
+
return _emit(result, f"cancel requested: {args.job_id} ({result['status']})", bool(args.json))
|
|
282
|
+
|
|
283
|
+
|
|
225
284
|
def _train_local(
|
|
226
285
|
task: str,
|
|
227
286
|
client: Apprentice,
|
|
@@ -424,12 +483,23 @@ def _job_info_line(job: JobInfo) -> str:
|
|
|
424
483
|
return f"{job.job_id} {job.kind} {job.status}"
|
|
425
484
|
|
|
426
485
|
|
|
427
|
-
def _report_line(report:
|
|
486
|
+
def _report_line(report: JobReport | None) -> str:
|
|
428
487
|
if report is None:
|
|
429
488
|
return "optimize finished with no report"
|
|
489
|
+
if not isinstance(report, OptimizationReport):
|
|
490
|
+
return "optimize finished with an unrecognized report"
|
|
430
491
|
return f"optimize done: baseline={report.baseline_score} optimized={report.optimized_score}"
|
|
431
492
|
|
|
432
493
|
|
|
494
|
+
def _preview_line(preview: OptimizationPreview) -> str:
|
|
495
|
+
estimate = preview.estimate
|
|
496
|
+
return (
|
|
497
|
+
f"optimize preview: metric={preview.eval_metric} budget={estimate.get('budget')} "
|
|
498
|
+
f"rows={estimate.get('rows')} cost_usd={estimate.get('cost_usd_low')}"
|
|
499
|
+
f"-{estimate.get('cost_usd_high')}"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
433
503
|
def _emit(payload: dict[str, Any], text: str, as_json: bool) -> int:
|
|
434
504
|
print(json.dumps(payload, sort_keys=True) if as_json else text)
|
|
435
505
|
return 0
|
|
@@ -16,7 +16,7 @@ import os
|
|
|
16
16
|
import time
|
|
17
17
|
import uuid
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Any
|
|
19
|
+
from typing import Any, Literal
|
|
20
20
|
|
|
21
21
|
import httpx
|
|
22
22
|
|
|
@@ -25,9 +25,10 @@ from runapprentice.models import (
|
|
|
25
25
|
DatasetRow,
|
|
26
26
|
DatasetStatus,
|
|
27
27
|
JobInfo,
|
|
28
|
+
JobReport,
|
|
28
29
|
JobSummary,
|
|
29
30
|
LocalRun,
|
|
30
|
-
|
|
31
|
+
OptimizationPreview,
|
|
31
32
|
PromptVersion,
|
|
32
33
|
TraceRecord,
|
|
33
34
|
)
|
|
@@ -114,10 +115,50 @@ class Apprentice:
|
|
|
114
115
|
raise ApprenticeError(f"{method} {path} -> {resp.status_code}: {resp.text[:500]}{suffix}")
|
|
115
116
|
return resp.json()
|
|
116
117
|
|
|
117
|
-
def optimize(
|
|
118
|
-
|
|
118
|
+
def optimize(
|
|
119
|
+
self,
|
|
120
|
+
task: str,
|
|
121
|
+
baseline_prompt: str | None = None,
|
|
122
|
+
budget: Literal["quick", "thorough"] | None = None,
|
|
123
|
+
metric: str | None = None,
|
|
124
|
+
) -> Job:
|
|
125
|
+
payload = {"task": task}
|
|
126
|
+
payload.update(
|
|
127
|
+
{
|
|
128
|
+
key: value
|
|
129
|
+
for key, value in {
|
|
130
|
+
"baseline_prompt": baseline_prompt,
|
|
131
|
+
"budget": budget,
|
|
132
|
+
"metric": metric,
|
|
133
|
+
}.items()
|
|
134
|
+
if value is not None
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
data = self._request("POST", "/v1/optimize", json=payload)
|
|
119
138
|
return Job(self, JobInfo.model_validate(data))
|
|
120
139
|
|
|
140
|
+
def preview_optimization(
|
|
141
|
+
self,
|
|
142
|
+
task: str,
|
|
143
|
+
baseline_prompt: str | None = None,
|
|
144
|
+
budget: Literal["quick", "thorough"] | None = None,
|
|
145
|
+
metric: str | None = None,
|
|
146
|
+
) -> OptimizationPreview:
|
|
147
|
+
payload = {"task": task}
|
|
148
|
+
payload.update(
|
|
149
|
+
{
|
|
150
|
+
key: value
|
|
151
|
+
for key, value in {
|
|
152
|
+
"baseline_prompt": baseline_prompt,
|
|
153
|
+
"budget": budget,
|
|
154
|
+
"metric": metric,
|
|
155
|
+
}.items()
|
|
156
|
+
if value is not None
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
data = self._request("POST", "/v1/optimize/preview", json=payload)
|
|
160
|
+
return OptimizationPreview.model_validate(data)
|
|
161
|
+
|
|
121
162
|
def job(self, job_id: str) -> Job:
|
|
122
163
|
data = self._request("GET", f"/v1/jobs/{job_id}")
|
|
123
164
|
return Job(self, JobInfo.model_validate(data))
|
|
@@ -126,6 +167,13 @@ class Apprentice:
|
|
|
126
167
|
data = self._request("GET", f"/v1/tasks/{task}/jobs")
|
|
127
168
|
return [JobSummary.model_validate(item) for item in data]
|
|
128
169
|
|
|
170
|
+
def cancel_job(self, job_id: str) -> dict[str, Any]:
|
|
171
|
+
return dict(self._request("POST", f"/v1/jobs/{job_id}/cancel"))
|
|
172
|
+
|
|
173
|
+
def local_runs(self, task: str) -> list[LocalRun]:
|
|
174
|
+
data = self._request("GET", f"/v1/tasks/{task}/local-runs")
|
|
175
|
+
return [LocalRun.model_validate(item) for item in data]
|
|
176
|
+
|
|
129
177
|
def train(self, task: str) -> Job:
|
|
130
178
|
data = self._request("POST", f"/v1/tasks/{task}/train", json={})
|
|
131
179
|
return Job(self, JobInfo.model_validate(data))
|
|
@@ -350,7 +398,10 @@ class Job:
|
|
|
350
398
|
time.sleep(poll_seconds)
|
|
351
399
|
return self
|
|
352
400
|
|
|
353
|
-
def
|
|
401
|
+
def cancel(self) -> dict[str, Any]:
|
|
402
|
+
return self._client.cancel_job(self.info.job_id)
|
|
403
|
+
|
|
404
|
+
def report(self) -> JobReport:
|
|
354
405
|
if self.info.report is None:
|
|
355
406
|
raise ApprenticeError(
|
|
356
407
|
f"job {self.info.job_id} has no report (status={self.info.status})"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Pydantic models for the Apprentice SDK. Mirrors the control-plane API contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, SkipValidation, ValidationError, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DatasetRow(BaseModel):
|
|
12
|
+
id: str | None = None
|
|
13
|
+
input: str
|
|
14
|
+
inputs: dict[str, str] | None = None
|
|
15
|
+
output: str
|
|
16
|
+
tier: str | None = None
|
|
17
|
+
source: str | None = None
|
|
18
|
+
flag_label: str | None = None
|
|
19
|
+
feedback_score: float | None = None
|
|
20
|
+
expert_authored: bool | None = None
|
|
21
|
+
created_at: datetime | None = None
|
|
22
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DatasetStatus(BaseModel):
|
|
26
|
+
task: str
|
|
27
|
+
gold: int = 0
|
|
28
|
+
silver: int = 0
|
|
29
|
+
raw: int = 0
|
|
30
|
+
ready_for_optimization: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TraceRecord(BaseModel):
|
|
34
|
+
trace_id: str | None = Field(default=None, pattern=r"^[0-9a-f]{32}$")
|
|
35
|
+
task: str
|
|
36
|
+
input: str
|
|
37
|
+
inputs: dict[str, str] | None = None
|
|
38
|
+
output: str
|
|
39
|
+
model: str | None = None
|
|
40
|
+
latency_ms: float | None = None
|
|
41
|
+
prompt_tokens: int | None = None
|
|
42
|
+
completion_tokens: int | None = None
|
|
43
|
+
error: str | None = None
|
|
44
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OptimizationReport(BaseModel):
|
|
48
|
+
task: str
|
|
49
|
+
baseline_score: float | None = None
|
|
50
|
+
optimized_score: float | None = None
|
|
51
|
+
examples_used: int | None = None
|
|
52
|
+
optimized_prompt: str | None = None
|
|
53
|
+
optimized_template: dict[str, Any] | None = None
|
|
54
|
+
detail: dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TrainingReport(BaseModel):
|
|
58
|
+
task: str | None = None
|
|
59
|
+
model_id: str | None = None
|
|
60
|
+
base_model: str | None = None
|
|
61
|
+
adapter_ref: str | None = None
|
|
62
|
+
champion_score: float | None = None
|
|
63
|
+
challenger_score: float | None = None
|
|
64
|
+
eval_metric: str | None = None
|
|
65
|
+
metric_tier: str | None = None
|
|
66
|
+
holdout_size: int | None = None
|
|
67
|
+
status: str | None = None
|
|
68
|
+
reason: str | None = None
|
|
69
|
+
config: dict[str, Any] | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GenerationReport(BaseModel):
|
|
73
|
+
rows_inserted: int | None = None
|
|
74
|
+
rows_rejected: int | None = None
|
|
75
|
+
reject_reasons: dict[str, int] | None = None
|
|
76
|
+
model: str | None = None
|
|
77
|
+
llm_fallback_used: bool | None = None
|
|
78
|
+
input_tokens: int | None = None
|
|
79
|
+
output_tokens: int | None = None
|
|
80
|
+
samples: list[dict[str, Any]] | None = None
|
|
81
|
+
chunks_truncated: int | None = None
|
|
82
|
+
response_failures: int | None = None
|
|
83
|
+
duplicate_inputs: int | None = None
|
|
84
|
+
executor_requested: str | None = None
|
|
85
|
+
executor_actual_models: list[str] | None = None
|
|
86
|
+
executor_fallback_used: bool | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
JobReport = OptimizationReport | TrainingReport | GenerationReport | dict[str, Any]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class JobInfo(BaseModel):
|
|
93
|
+
job_id: str
|
|
94
|
+
task: str # task ID; use task_name when calling task-scoped client methods
|
|
95
|
+
task_name: str | None = None
|
|
96
|
+
kind: str
|
|
97
|
+
status: str
|
|
98
|
+
created_at: datetime | None = None
|
|
99
|
+
error: str | None = None
|
|
100
|
+
report: SkipValidation[JobReport] | None = None
|
|
101
|
+
progress: dict[str, Any] | None = None
|
|
102
|
+
queue_position: int | None = None
|
|
103
|
+
baseline_score: float | None = None
|
|
104
|
+
optimized_score: float | None = None
|
|
105
|
+
baseline_from_user: bool | None = None
|
|
106
|
+
generation_session_id: str | None = None
|
|
107
|
+
round: int | None = None
|
|
108
|
+
predecessor_job_id: str | None = None
|
|
109
|
+
phase: str | None = None
|
|
110
|
+
guidance: str | None = None
|
|
111
|
+
|
|
112
|
+
@model_validator(mode="before")
|
|
113
|
+
@classmethod
|
|
114
|
+
def parse_report_for_kind(cls, value: Any) -> Any:
|
|
115
|
+
if not isinstance(value, dict) or not isinstance(value.get("report"), dict):
|
|
116
|
+
return value
|
|
117
|
+
kind = value.get("kind")
|
|
118
|
+
if kind == "optimize":
|
|
119
|
+
report_type: type[BaseModel] = OptimizationReport
|
|
120
|
+
elif kind == "train":
|
|
121
|
+
report_type = TrainingReport
|
|
122
|
+
elif kind == "generate":
|
|
123
|
+
report_type = GenerationReport
|
|
124
|
+
else:
|
|
125
|
+
return value
|
|
126
|
+
try:
|
|
127
|
+
report = report_type.model_validate(value["report"])
|
|
128
|
+
except ValidationError:
|
|
129
|
+
return value
|
|
130
|
+
return {**value, "report": report}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class JobSummary(BaseModel):
|
|
134
|
+
job_id: str
|
|
135
|
+
task_name: str | None = None
|
|
136
|
+
status: str
|
|
137
|
+
kind: str
|
|
138
|
+
created_at: datetime | None = None
|
|
139
|
+
baseline_score: float | None = None
|
|
140
|
+
optimized_score: float | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class LocalRun(BaseModel):
|
|
144
|
+
run_id: str
|
|
145
|
+
task: str | None = None
|
|
146
|
+
task_id: str | None = None
|
|
147
|
+
status: str = "registered"
|
|
148
|
+
eval_tier: str | None = None
|
|
149
|
+
base_model: str | None = None
|
|
150
|
+
dataset_row_count: int | None = None
|
|
151
|
+
seed: int | None = None
|
|
152
|
+
baseline_score: float | None = None
|
|
153
|
+
final_score: float | None = None
|
|
154
|
+
held_out_rows: int | None = None
|
|
155
|
+
adapter_sha256: str | None = None
|
|
156
|
+
mlx_lm_version: str | None = None
|
|
157
|
+
error_message: str | None = None
|
|
158
|
+
created_at: datetime | None = None
|
|
159
|
+
completed_at: datetime | None = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class OptimizationPreview(BaseModel):
|
|
163
|
+
eval_metric: str
|
|
164
|
+
metric_tier: str
|
|
165
|
+
requested_metric: str
|
|
166
|
+
recommended_metric: str
|
|
167
|
+
metric_reason: str
|
|
168
|
+
rag_candidate: bool
|
|
169
|
+
rows_checked: int
|
|
170
|
+
rows_source: str
|
|
171
|
+
guidance: dict[str, Any]
|
|
172
|
+
estimate: dict[str, Any]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PromptVersion(BaseModel):
|
|
176
|
+
task: str
|
|
177
|
+
version: int
|
|
178
|
+
text: str
|
|
179
|
+
score: float | None = None
|
|
@@ -9,7 +9,7 @@ import pytest
|
|
|
9
9
|
|
|
10
10
|
from runapprentice import ApprenticeError, cli
|
|
11
11
|
import runapprentice.local as local
|
|
12
|
-
from runapprentice.models import DatasetRow
|
|
12
|
+
from runapprentice.models import DatasetRow, JobInfo, JobSummary
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def test_parser_supports_required_commands() -> None:
|
|
@@ -28,6 +28,44 @@ def test_parser_supports_required_commands() -> None:
|
|
|
28
28
|
assert awake.fresh is True
|
|
29
29
|
assert parser.parse_args(["jobs", "task"]).command == "jobs"
|
|
30
30
|
assert parser.parse_args(["job", "j1"]).command == "job"
|
|
31
|
+
controls = parser.parse_args(
|
|
32
|
+
[
|
|
33
|
+
"optimize",
|
|
34
|
+
"task",
|
|
35
|
+
"--budget",
|
|
36
|
+
"thorough",
|
|
37
|
+
"--metric",
|
|
38
|
+
"json_f1",
|
|
39
|
+
"--prompt-file",
|
|
40
|
+
"prompt.txt",
|
|
41
|
+
"--preview",
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
assert controls.budget == "thorough"
|
|
45
|
+
assert controls.metric == "json_f1"
|
|
46
|
+
assert controls.prompt_file == Path("prompt.txt")
|
|
47
|
+
assert controls.preview is True
|
|
48
|
+
assert parser.parse_args(["cancel", "j1"]).command == "cancel"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_cli_renders_all_job_kinds() -> None:
|
|
52
|
+
for kind, report in (
|
|
53
|
+
("optimize", {"task": "extract", "baseline_score": 50.0}),
|
|
54
|
+
("train", {"task": "extract", "champion_score": 80.0}),
|
|
55
|
+
("generate", {"rows_inserted": 10, "rows_rejected": 1}),
|
|
56
|
+
):
|
|
57
|
+
info = JobInfo.model_validate(
|
|
58
|
+
{
|
|
59
|
+
"job_id": f"j-{kind}",
|
|
60
|
+
"task": "task-id",
|
|
61
|
+
"kind": kind,
|
|
62
|
+
"status": "succeeded",
|
|
63
|
+
"report": report,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
summary = JobSummary(job_id=info.job_id, kind=kind, status=info.status)
|
|
67
|
+
assert cli._job_info_line(info) == f"j-{kind} {kind} succeeded"
|
|
68
|
+
assert cli._job_summary(summary)["kind"] == kind
|
|
31
69
|
|
|
32
70
|
|
|
33
71
|
def test_effort_explicit_overrides_hardware_detection() -> None:
|
|
@@ -13,7 +13,11 @@ import pytest
|
|
|
13
13
|
from runapprentice import Apprentice, ApprenticeError, TraceRecord
|
|
14
14
|
from runapprentice._capture import CaptureDispatcher
|
|
15
15
|
import runapprentice.local as local
|
|
16
|
-
from runapprentice.models import
|
|
16
|
+
from runapprentice.models import (
|
|
17
|
+
GenerationReport,
|
|
18
|
+
OptimizationReport,
|
|
19
|
+
TrainingReport,
|
|
20
|
+
)
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def make_client(routes: dict[str, Any]) -> tuple[Apprentice, list[dict[str, Any]]]:
|
|
@@ -219,6 +223,179 @@ def test_optimize_and_job_lifecycle() -> None:
|
|
|
219
223
|
assert report.optimized_score == 88.0
|
|
220
224
|
|
|
221
225
|
|
|
226
|
+
def test_generate_job_payload_parses_without_crashing() -> None:
|
|
227
|
+
payload = {
|
|
228
|
+
"job_id": "j-gen",
|
|
229
|
+
"task": "task-id",
|
|
230
|
+
"task_name": "invoices",
|
|
231
|
+
"kind": "generate",
|
|
232
|
+
"status": "succeeded",
|
|
233
|
+
"generation_session_id": "session-1",
|
|
234
|
+
"round": 2,
|
|
235
|
+
"predecessor_job_id": "j-preview",
|
|
236
|
+
"phase": "full",
|
|
237
|
+
"guidance": "Include boundary cases",
|
|
238
|
+
"report": {
|
|
239
|
+
"rows_inserted": 17,
|
|
240
|
+
"rows_rejected": 3,
|
|
241
|
+
"reject_reasons": {"duplicate": 2, "invalid_output": 1},
|
|
242
|
+
"model": "generator-model",
|
|
243
|
+
"llm_fallback_used": False,
|
|
244
|
+
"input_tokens": 1200,
|
|
245
|
+
"output_tokens": 700,
|
|
246
|
+
"samples": [{"input": "sample", "output": "{}"}],
|
|
247
|
+
"chunks_truncated": 1,
|
|
248
|
+
"response_failures": 1,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
client, _ = make_client({"GET /v1/jobs/j-gen": payload})
|
|
252
|
+
|
|
253
|
+
job = client.job("j-gen")
|
|
254
|
+
|
|
255
|
+
report = job.report()
|
|
256
|
+
assert isinstance(report, GenerationReport)
|
|
257
|
+
assert report.rows_inserted == 17
|
|
258
|
+
assert job.info.generation_session_id == "session-1"
|
|
259
|
+
assert job.info.task_name == "invoices"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_train_report_keeps_training_scores() -> None:
|
|
263
|
+
client, _ = make_client(
|
|
264
|
+
{
|
|
265
|
+
"GET /v1/jobs/j-train": {
|
|
266
|
+
"job_id": "j-train",
|
|
267
|
+
"task": "task-id",
|
|
268
|
+
"kind": "train",
|
|
269
|
+
"status": "succeeded",
|
|
270
|
+
"report": {
|
|
271
|
+
"task": "extract",
|
|
272
|
+
"model_id": "model-1",
|
|
273
|
+
"base_model": "base-model",
|
|
274
|
+
"adapter_ref": "adapter-1",
|
|
275
|
+
"champion_score": 81.5,
|
|
276
|
+
"challenger_score": 84.0,
|
|
277
|
+
"eval_metric": "json_f1",
|
|
278
|
+
"metric_tier": "deterministic",
|
|
279
|
+
"holdout_size": 50,
|
|
280
|
+
"status": "ready",
|
|
281
|
+
"reason": "challenger met champion and minimum score",
|
|
282
|
+
"config": {"seed": 42},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
report = client.job("j-train").report()
|
|
289
|
+
|
|
290
|
+
assert isinstance(report, TrainingReport)
|
|
291
|
+
assert report.model_dump()["champion_score"] == 81.5
|
|
292
|
+
assert report.model_dump()["challenger_score"] == 84.0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_unknown_or_malformed_report_stays_raw() -> None:
|
|
296
|
+
client, _ = make_client(
|
|
297
|
+
{
|
|
298
|
+
"GET /v1/jobs/j-future": {
|
|
299
|
+
"job_id": "j-future",
|
|
300
|
+
"task": "task-id",
|
|
301
|
+
"kind": "future-kind",
|
|
302
|
+
"status": "succeeded",
|
|
303
|
+
"report": {"new_field": [1, 2, 3]},
|
|
304
|
+
},
|
|
305
|
+
"GET /v1/jobs/j-bad": {
|
|
306
|
+
"job_id": "j-bad",
|
|
307
|
+
"task": "task-id",
|
|
308
|
+
"kind": "optimize",
|
|
309
|
+
"status": "succeeded",
|
|
310
|
+
"report": {"baseline_score": "not-a-number"},
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert client.job("j-future").report() == {"new_field": [1, 2, 3]}
|
|
316
|
+
assert client.job("j-bad").report() == {"baseline_score": "not-a-number"}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_optimize_sends_only_set_controls_and_preview_parses() -> None:
|
|
320
|
+
preview = {
|
|
321
|
+
"eval_metric": "json_f1",
|
|
322
|
+
"metric_tier": "deterministic",
|
|
323
|
+
"requested_metric": "json_f1",
|
|
324
|
+
"recommended_metric": "json_f1",
|
|
325
|
+
"metric_reason": "outputs are JSON objects",
|
|
326
|
+
"rag_candidate": False,
|
|
327
|
+
"rows_checked": 20,
|
|
328
|
+
"rows_source": "verified",
|
|
329
|
+
"guidance": {
|
|
330
|
+
"use_case": "json_extraction",
|
|
331
|
+
"recommended_min_rows": 30,
|
|
332
|
+
"verified_rows": 20,
|
|
333
|
+
"data_adequacy": "limited",
|
|
334
|
+
"best_practice": "Add more verified rows.",
|
|
335
|
+
},
|
|
336
|
+
"estimate": {
|
|
337
|
+
"rows": 20,
|
|
338
|
+
"budget": "heavy",
|
|
339
|
+
"cost_usd_low": 0.2,
|
|
340
|
+
"cost_usd_high": 0.5,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
client, received = make_client(
|
|
344
|
+
{
|
|
345
|
+
"POST /v1/optimize": {
|
|
346
|
+
"job_id": "j1",
|
|
347
|
+
"task": "task-id",
|
|
348
|
+
"kind": "optimize",
|
|
349
|
+
"status": "queued",
|
|
350
|
+
},
|
|
351
|
+
"POST /v1/optimize/preview": preview,
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
client.optimize("extract", budget="thorough")
|
|
356
|
+
result = client.preview_optimization("extract", metric="json_f1")
|
|
357
|
+
|
|
358
|
+
assert received[0]["body"] == {"task": "extract", "budget": "thorough"}
|
|
359
|
+
assert received[1]["body"] == {"task": "extract", "metric": "json_f1"}
|
|
360
|
+
assert result.estimate["cost_usd_high"] == 0.5
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_cancel_happy_path_and_job_cancel() -> None:
|
|
364
|
+
client, received = make_client(
|
|
365
|
+
{
|
|
366
|
+
"GET /v1/jobs/j1": {
|
|
367
|
+
"job_id": "j1",
|
|
368
|
+
"task": "task-id",
|
|
369
|
+
"kind": "optimize",
|
|
370
|
+
"status": "running",
|
|
371
|
+
},
|
|
372
|
+
"POST /v1/jobs/j1/cancel": {
|
|
373
|
+
"job_id": "j1",
|
|
374
|
+
"cancel_requested": True,
|
|
375
|
+
"status": "running",
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
assert client.job("j1").cancel()["cancel_requested"] is True
|
|
381
|
+
assert received[-1] == {"method": "POST", "path": "/v1/jobs/j1/cancel", "body": None}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_cancel_409_uses_normal_error_path() -> None:
|
|
385
|
+
client = Apprentice(api_key="test", base_url="http://mock", capture_async=False)
|
|
386
|
+
client._http = httpx.Client(
|
|
387
|
+
base_url="http://mock",
|
|
388
|
+
transport=httpx.MockTransport(
|
|
389
|
+
lambda _request: httpx.Response(
|
|
390
|
+
409, json={"detail": "job j1 is succeeded, nothing to cancel"}
|
|
391
|
+
)
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
with pytest.raises(ApprenticeError, match="nothing to cancel"):
|
|
396
|
+
client.cancel_job("j1")
|
|
397
|
+
|
|
398
|
+
|
|
222
399
|
def test_train_jobs_rows_and_local_run_methods() -> None:
|
|
223
400
|
client, received = make_client(
|
|
224
401
|
{
|
|
@@ -232,20 +409,49 @@ def test_train_jobs_rows_and_local_run_methods() -> None:
|
|
|
232
409
|
{"job_id": "j1", "kind": "optimize", "status": "succeeded"}
|
|
233
410
|
],
|
|
234
411
|
"GET /v1/datasets/t1/rows": {
|
|
235
|
-
"rows": [
|
|
412
|
+
"rows": [
|
|
413
|
+
{
|
|
414
|
+
"id": "r1",
|
|
415
|
+
"input": "i",
|
|
416
|
+
"output": "o",
|
|
417
|
+
"tier": "gold",
|
|
418
|
+
"source": "review",
|
|
419
|
+
"flag_label": "edge_case",
|
|
420
|
+
"feedback_score": 1.0,
|
|
421
|
+
"expert_authored": True,
|
|
422
|
+
"created_at": "2026-07-18T00:00:00+00:00",
|
|
423
|
+
}
|
|
424
|
+
],
|
|
236
425
|
"total": 1,
|
|
237
426
|
},
|
|
238
427
|
"POST /v1/local-runs": {"run_id": "lr1", "task": "t1", "status": "registered"},
|
|
239
428
|
"POST /v1/local-runs/lr1/complete": {"ok": True},
|
|
429
|
+
"GET /v1/tasks/t1/local-runs": [
|
|
430
|
+
{
|
|
431
|
+
"run_id": "lr1",
|
|
432
|
+
"task_id": "task-id",
|
|
433
|
+
"status": "completed",
|
|
434
|
+
"eval_tier": "self_reported",
|
|
435
|
+
"base_model": "m",
|
|
436
|
+
"dataset_row_count": 3,
|
|
437
|
+
"seed": 42,
|
|
438
|
+
"final_score": 91.0,
|
|
439
|
+
}
|
|
440
|
+
],
|
|
240
441
|
}
|
|
241
442
|
)
|
|
242
443
|
|
|
243
444
|
assert client.train("t1").job_id == "j2"
|
|
244
445
|
assert client.jobs("t1")[0].job_id == "j1"
|
|
245
|
-
|
|
446
|
+
row = client.datasets.rows("t1", "gold")[0]
|
|
447
|
+
assert row.id == "r1"
|
|
448
|
+
assert row.source == "review"
|
|
449
|
+
assert row.expert_authored is True
|
|
246
450
|
assert client.register_local_run("t1", "m", 3, 42).run_id == "lr1"
|
|
247
451
|
assert client.complete_local_run("lr1", {"final_score": 1.0}) == {"ok": True}
|
|
248
|
-
assert
|
|
452
|
+
assert client.local_runs("t1")[0].final_score == 91.0
|
|
453
|
+
registration = next(item for item in received if item["path"] == "/v1/local-runs")
|
|
454
|
+
assert registration["body"] == {
|
|
249
455
|
"task": "t1",
|
|
250
456
|
"base_model": "m",
|
|
251
457
|
"dataset_row_count": 3,
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""Pydantic models for the Apprentice SDK. Mirrors the control-plane API contract."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DatasetRow(BaseModel):
|
|
12
|
-
id: str | None = None
|
|
13
|
-
input: str
|
|
14
|
-
inputs: dict[str, str] | None = None
|
|
15
|
-
output: str
|
|
16
|
-
tier: str | None = None
|
|
17
|
-
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class DatasetStatus(BaseModel):
|
|
21
|
-
task: str
|
|
22
|
-
gold: int = 0
|
|
23
|
-
silver: int = 0
|
|
24
|
-
raw: int = 0
|
|
25
|
-
ready_for_optimization: bool = False
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class TraceRecord(BaseModel):
|
|
29
|
-
trace_id: str | None = Field(default=None, pattern=r"^[0-9a-f]{32}$")
|
|
30
|
-
task: str
|
|
31
|
-
input: str
|
|
32
|
-
inputs: dict[str, str] | None = None
|
|
33
|
-
output: str
|
|
34
|
-
model: str | None = None
|
|
35
|
-
latency_ms: float | None = None
|
|
36
|
-
prompt_tokens: int | None = None
|
|
37
|
-
completion_tokens: int | None = None
|
|
38
|
-
error: str | None = None
|
|
39
|
-
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class OptimizationReport(BaseModel):
|
|
43
|
-
task: str
|
|
44
|
-
baseline_score: float | None = None
|
|
45
|
-
optimized_score: float | None = None
|
|
46
|
-
examples_used: int | None = None
|
|
47
|
-
optimized_prompt: str | None = None
|
|
48
|
-
optimized_template: dict[str, Any] | None = None
|
|
49
|
-
detail: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class JobInfo(BaseModel):
|
|
53
|
-
job_id: str
|
|
54
|
-
task: str
|
|
55
|
-
kind: str
|
|
56
|
-
status: str # queued | running | succeeded | failed
|
|
57
|
-
created_at: datetime | None = None
|
|
58
|
-
error: str | None = None
|
|
59
|
-
report: OptimizationReport | None = None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class JobSummary(BaseModel):
|
|
63
|
-
job_id: str
|
|
64
|
-
status: str
|
|
65
|
-
kind: str
|
|
66
|
-
created_at: datetime | None = None
|
|
67
|
-
baseline_score: float | None = None
|
|
68
|
-
optimized_score: float | None = None
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class LocalRun(BaseModel):
|
|
72
|
-
run_id: str
|
|
73
|
-
task: str
|
|
74
|
-
status: str = "registered"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class PromptVersion(BaseModel):
|
|
78
|
-
task: str
|
|
79
|
-
version: int
|
|
80
|
-
text: str
|
|
81
|
-
score: float | None = None
|
|
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
|