trodo-python 2.26.1__py3-none-any.whl → 3.0.0__py3-none-any.whl
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.
- trodo/__init__.py +25 -90
- trodo/_version.py +1 -1
- trodo/api/endpoints.py +1 -6
- trodo/api/http_client.py +0 -25
- trodo/api/management.py +90 -0
- trodo/client.py +0 -9
- trodo/managers/dataset_manager.py +158 -5
- trodo/otel/processor.py +18 -1
- {trodo_python-2.26.1.dist-info → trodo_python-3.0.0.dist-info}/METADATA +1 -1
- {trodo_python-2.26.1.dist-info → trodo_python-3.0.0.dist-info}/RECORD +12 -12
- {trodo_python-2.26.1.dist-info → trodo_python-3.0.0.dist-info}/WHEEL +1 -1
- trodo/managers/experiment_manager.py +0 -286
- {trodo_python-2.26.1.dist-info → trodo_python-3.0.0.dist-info}/top_level.txt +0 -0
trodo/__init__.py
CHANGED
|
@@ -41,7 +41,7 @@ Downstream microservice (join the caller's run instead of making a new one):
|
|
|
41
41
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
|
-
__version__ = "2.26.
|
|
44
|
+
__version__ = "2.26.2"
|
|
45
45
|
|
|
46
46
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
47
47
|
|
|
@@ -120,15 +120,10 @@ __all__ = [
|
|
|
120
120
|
"TemplateError",
|
|
121
121
|
"ManagedPrompt",
|
|
122
122
|
"PromptSummary",
|
|
123
|
-
# Datasets
|
|
123
|
+
# Datasets
|
|
124
|
+
"datasets",
|
|
124
125
|
"append_dataset",
|
|
125
126
|
"export_dataset",
|
|
126
|
-
"ingest_experiment",
|
|
127
|
-
"create_experiment",
|
|
128
|
-
"run_experiment",
|
|
129
|
-
"get_experiment",
|
|
130
|
-
"wait_experiment",
|
|
131
|
-
"compare_experiments",
|
|
132
127
|
]
|
|
133
128
|
|
|
134
129
|
# ============================================================================
|
|
@@ -412,9 +407,30 @@ def render_template(
|
|
|
412
407
|
|
|
413
408
|
|
|
414
409
|
# ----------------------------------------------------------------------------
|
|
415
|
-
# Datasets
|
|
410
|
+
# Datasets
|
|
416
411
|
# ----------------------------------------------------------------------------
|
|
417
412
|
|
|
413
|
+
def datasets(api_key: Optional[str] = None, api_url: Optional[str] = None) -> Any:
|
|
414
|
+
"""The team's datasets, managed with a Trodo API key (``TRODO_API_KEY``), as the CLI does::
|
|
415
|
+
|
|
416
|
+
ds = trodo.datasets()
|
|
417
|
+
ds.create("refund regressions", items=[{"input": "Refund order 42?", "expected": {"refund": True}}])
|
|
418
|
+
for row in ds.rows("refund regressions", rows="failed"):
|
|
419
|
+
...
|
|
420
|
+
ds.update("refund regressions", [{"id": row["id"], "expected": "…"}])
|
|
421
|
+
ds.archive("refund regressions", [dup_id]) # restore() brings it back
|
|
422
|
+
ds.add_columns("refund regressions", ["lang"])
|
|
423
|
+
|
|
424
|
+
Works without ``trodo.init``; after it, ``append`` (site id) works here too.
|
|
425
|
+
Nothing deletes.
|
|
426
|
+
"""
|
|
427
|
+
from .managers.dataset_manager import DatasetManager
|
|
428
|
+
from .api.management import ManagementClient
|
|
429
|
+
if api_key is None and api_url is None and _client is not None:
|
|
430
|
+
return _client.datasets
|
|
431
|
+
return DatasetManager(_client._http if _client is not None else None, ManagementClient(api_key, api_url))
|
|
432
|
+
|
|
433
|
+
|
|
418
434
|
def append_dataset(
|
|
419
435
|
ref: str,
|
|
420
436
|
items: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
@@ -460,87 +476,6 @@ def export_dataset(
|
|
|
460
476
|
)
|
|
461
477
|
|
|
462
478
|
|
|
463
|
-
def ingest_experiment(
|
|
464
|
-
dataset: str,
|
|
465
|
-
outputs: List[Dict[str, Any]],
|
|
466
|
-
name: Optional[str] = None,
|
|
467
|
-
task_name: Optional[str] = None,
|
|
468
|
-
judge: Optional[Dict[str, Any]] = None,
|
|
469
|
-
evaluator_ids: Optional[List[str]] = None,
|
|
470
|
-
dataset_version_no: Optional[int] = None,
|
|
471
|
-
) -> Dict[str, Any]:
|
|
472
|
-
"""Ingest a batch of model *outputs* for *dataset* and grade them.
|
|
473
|
-
|
|
474
|
-
*outputs* is a list of dicts shaped ``{"item_position": int, "output": str,
|
|
475
|
-
"expected_output"?, "query"?, "context"?, "cost_usd"?, "latency_ms"?}``.
|
|
476
|
-
Configure grading with ``judge`` (``{"credential_id", "provider", "model"}``)
|
|
477
|
-
and/or ``evaluator_ids``::
|
|
478
|
-
|
|
479
|
-
exp = trodo.ingest_experiment("support-eval", [
|
|
480
|
-
{"item_position": 0, "output": "here is your tracking link"},
|
|
481
|
-
], judge={"credential_id": "cred_1", "provider": "openai", "model": "gpt-4o"})
|
|
482
|
-
|
|
483
|
-
Returns the experiment dict.
|
|
484
|
-
"""
|
|
485
|
-
return _get_client().experiments.ingest(
|
|
486
|
-
dataset,
|
|
487
|
-
outputs,
|
|
488
|
-
name=name,
|
|
489
|
-
task_name=task_name,
|
|
490
|
-
judge=judge,
|
|
491
|
-
evaluator_ids=evaluator_ids,
|
|
492
|
-
dataset_version_no=dataset_version_no,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def create_experiment(
|
|
497
|
-
*,
|
|
498
|
-
dataset: Optional[str] = None,
|
|
499
|
-
dataset_id: Optional[str] = None,
|
|
500
|
-
models: List[Dict[str, Any]],
|
|
501
|
-
**kwargs: Any,
|
|
502
|
-
) -> Dict[str, Any]:
|
|
503
|
-
"""Create an experiment (immutable refs, status pending)."""
|
|
504
|
-
return _get_client().experiments.create(
|
|
505
|
-
dataset=dataset, dataset_id=dataset_id, models=models, **kwargs
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def run_experiment(
|
|
510
|
-
*,
|
|
511
|
-
dataset: Optional[str] = None,
|
|
512
|
-
dataset_id: Optional[str] = None,
|
|
513
|
-
models: List[Dict[str, Any]],
|
|
514
|
-
**kwargs: Any,
|
|
515
|
-
) -> Dict[str, Any]:
|
|
516
|
-
"""Create and run an experiment synchronously."""
|
|
517
|
-
return _get_client().experiments.run(
|
|
518
|
-
dataset=dataset, dataset_id=dataset_id, models=models, **kwargs
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def get_experiment(experiment_id: str) -> Dict[str, Any]:
|
|
523
|
-
"""Fetch an experiment by id."""
|
|
524
|
-
return _get_client().experiments.get(experiment_id)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def wait_experiment(
|
|
528
|
-
experiment_id: str,
|
|
529
|
-
*,
|
|
530
|
-
interval_ms: int = 2500,
|
|
531
|
-
timeout_ms: int = 600_000,
|
|
532
|
-
) -> Dict[str, Any]:
|
|
533
|
-
"""Poll until the experiment completes or fails."""
|
|
534
|
-
return _get_client().experiments.wait(
|
|
535
|
-
experiment_id, interval_ms=interval_ms, timeout_ms=timeout_ms
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def compare_experiments(a_id: str, b_id: str, *, strict: bool = False) -> Dict[str, Any]:
|
|
540
|
-
"""Compare two experiments side by side."""
|
|
541
|
-
return _get_client().experiments.compare(a_id, b_id, strict=strict)
|
|
542
|
-
|
|
543
|
-
|
|
544
479
|
def enable_auto_events() -> None:
|
|
545
480
|
_get_client().enable_auto_events()
|
|
546
481
|
|
trodo/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
SDK_VERSION = "
|
|
1
|
+
SDK_VERSION = "3.0.0"
|
trodo/api/endpoints.py
CHANGED
|
@@ -28,10 +28,5 @@ RUNS_START = "/api/sdk/runs/start"
|
|
|
28
28
|
RUNS_BASE = "/api/sdk/runs" # /runs/{run_id}/end, /spans, /feedback
|
|
29
29
|
SPANS_APPEND = "/api/sdk/spans/append"
|
|
30
30
|
OTLP_TRACES = "/api/sdk/otel/v1/traces"
|
|
31
|
-
# Datasets
|
|
31
|
+
# Datasets
|
|
32
32
|
DATASETS_BASE = "/api/sdk/datasets" # /datasets/{ref}/items
|
|
33
|
-
EXPERIMENTS = "/api/sdk/experiments"
|
|
34
|
-
EXPERIMENTS_INGEST = "/api/sdk/experiments/ingest"
|
|
35
|
-
EXPERIMENTS_CREATE = "/api/sdk/experiments/create"
|
|
36
|
-
EXPERIMENTS_RUN = "/api/sdk/experiments/run"
|
|
37
|
-
EXPERIMENTS_COMPARE = "/api/sdk/experiments/compare"
|
trodo/api/http_client.py
CHANGED
|
@@ -219,31 +219,6 @@ class HttpClient:
|
|
|
219
219
|
"version": int(version) if version and version.isdigit() else None,
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
def ingest_experiment(self, body: Dict[str, Any]) -> ApiResult:
|
|
223
|
-
return self._request("/api/sdk/experiments/ingest", body)
|
|
224
|
-
|
|
225
|
-
def create_experiment(self, body: Dict[str, Any]) -> ApiResult:
|
|
226
|
-
return self._request("/api/sdk/experiments/create", body)
|
|
227
|
-
|
|
228
|
-
def run_experiment(self, body: Dict[str, Any]) -> ApiResult:
|
|
229
|
-
return self._request("/api/sdk/experiments/run", body)
|
|
230
|
-
|
|
231
|
-
def run_experiment_by_id(self, experiment_id: str) -> ApiResult:
|
|
232
|
-
from urllib.parse import quote
|
|
233
|
-
return self._request(f"/api/sdk/experiments/{quote(str(experiment_id), safe='')}/run", {})
|
|
234
|
-
|
|
235
|
-
def get_experiment(self, experiment_id: str) -> ApiResult:
|
|
236
|
-
from urllib.parse import quote
|
|
237
|
-
return self._get(f"/api/sdk/experiments/{quote(str(experiment_id), safe='')}")
|
|
238
|
-
|
|
239
|
-
def compare_experiments(
|
|
240
|
-
self, a_id: str, b_id: str, strict: bool = False
|
|
241
|
-
) -> ApiResult:
|
|
242
|
-
params: Dict[str, Any] = {"a": a_id, "b": b_id}
|
|
243
|
-
if strict:
|
|
244
|
-
params["strict"] = "true"
|
|
245
|
-
return self._get("/api/sdk/experiments/compare", params)
|
|
246
|
-
|
|
247
222
|
def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
|
|
248
223
|
return self._request("/api/sdk/track", {"sessionData": session_data})
|
|
249
224
|
|
trodo/api/management.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The team API (/api/v1) with a Trodo API key — what the CLI uses.
|
|
2
|
+
|
|
3
|
+
Dataset management (create, read rows, edit, archive, add columns) goes
|
|
4
|
+
through here, never through the site id: a site id can sit in a browser page,
|
|
5
|
+
and nobody who only has that should be able to change a dataset. The key is a
|
|
6
|
+
Trodo API key (Settings → MCP, or ``trodo login``), passed in or read from
|
|
7
|
+
``TRODO_API_KEY``; ``TRODO_API_URL`` overrides the address.
|
|
8
|
+
|
|
9
|
+
Reads retry on network errors and 502/503/504; writes are sent once.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, Iterator, Optional
|
|
18
|
+
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
from .._version import SDK_VERSION
|
|
22
|
+
|
|
23
|
+
DEFAULT_API_URL = "https://api.trodo.ai"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TrodoApiError(RuntimeError):
|
|
27
|
+
"""The API refused or failed. ``status`` and ``body`` say why."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, status: Optional[int] = None, body: Any = None) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.status = status
|
|
32
|
+
self.body = body
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ManagementClient:
|
|
36
|
+
def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None, timeout: int = 60) -> None:
|
|
37
|
+
key = api_key or os.environ.get("TRODO_API_KEY")
|
|
38
|
+
if not key:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"trodo: dataset management needs a Trodo API key: pass api_key= or set TRODO_API_KEY "
|
|
41
|
+
"(Settings → MCP, or `trodo login`). The site id cannot change datasets."
|
|
42
|
+
)
|
|
43
|
+
self.base = (api_url or os.environ.get("TRODO_API_URL") or DEFAULT_API_URL).rstrip("/") + "/api/v1"
|
|
44
|
+
self.timeout = timeout
|
|
45
|
+
self._session = requests.Session()
|
|
46
|
+
self._session.headers.update({
|
|
47
|
+
"Authorization": f"Bearer {key}",
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"User-Agent": f"trodo-python/{SDK_VERSION}",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
def _send(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None,
|
|
53
|
+
body: Any = None, stream: bool = False) -> requests.Response:
|
|
54
|
+
url = self.base + path
|
|
55
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None and v != ""}
|
|
56
|
+
tries = 4 if method == "GET" else 1
|
|
57
|
+
for attempt in range(tries):
|
|
58
|
+
try:
|
|
59
|
+
resp = self._session.request(method, url, params=clean, data=None if body is None else json.dumps(body),
|
|
60
|
+
timeout=self.timeout, stream=stream)
|
|
61
|
+
except requests.RequestException as exc:
|
|
62
|
+
if attempt + 1 < tries:
|
|
63
|
+
time.sleep(0.5 * (2 ** attempt))
|
|
64
|
+
continue
|
|
65
|
+
raise TrodoApiError(f"trodo: {method} {path} failed: {exc}") from exc
|
|
66
|
+
if resp.status_code in (502, 503, 504) and attempt + 1 < tries:
|
|
67
|
+
time.sleep(0.5 * (2 ** attempt))
|
|
68
|
+
continue
|
|
69
|
+
if resp.status_code >= 400:
|
|
70
|
+
try:
|
|
71
|
+
data = resp.json()
|
|
72
|
+
except ValueError:
|
|
73
|
+
data = {"message": resp.text[:300]}
|
|
74
|
+
msg = (data.get("message") or data.get("error")) if isinstance(data, dict) else None
|
|
75
|
+
raise TrodoApiError(f"trodo: {method} {path} → HTTP {resp.status_code} {msg or ''}".strip(), resp.status_code, data)
|
|
76
|
+
return resp
|
|
77
|
+
raise TrodoApiError(f"trodo: {method} {path} failed") # pragma: no cover
|
|
78
|
+
|
|
79
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
80
|
+
return self._send("GET", path, params=params).json()
|
|
81
|
+
|
|
82
|
+
def post(self, path: str, body: Any = None) -> Any:
|
|
83
|
+
return self._send("POST", path, body=body if body is not None else {}).json()
|
|
84
|
+
|
|
85
|
+
def lines(self, path: str, params: Optional[Dict[str, Any]] = None) -> Iterator[Dict[str, Any]]:
|
|
86
|
+
"""A JSON Lines response, one object at a time."""
|
|
87
|
+
resp = self._send("GET", path, params=params, stream=True)
|
|
88
|
+
for raw in resp.iter_lines(decode_unicode=True):
|
|
89
|
+
if raw and raw.strip():
|
|
90
|
+
yield json.loads(raw)
|
trodo/client.py
CHANGED
|
@@ -84,7 +84,6 @@ class TrodoClient:
|
|
|
84
84
|
self._session_manager = SessionManager(max_cached_users)
|
|
85
85
|
self._prompts = None # lazily-built PromptManager
|
|
86
86
|
self._datasets = None # lazily-built DatasetManager
|
|
87
|
-
self._experiments = None # lazily-built ExperimentManager
|
|
88
87
|
self._users = None # lazily-built UserManager
|
|
89
88
|
|
|
90
89
|
if batch_enabled:
|
|
@@ -149,14 +148,6 @@ class TrodoClient:
|
|
|
149
148
|
self._datasets = DatasetManager(self._http)
|
|
150
149
|
return self._datasets
|
|
151
150
|
|
|
152
|
-
@property
|
|
153
|
-
def experiments(self):
|
|
154
|
-
"""Managed experiment runs + external ingest for server-side grading."""
|
|
155
|
-
if self._experiments is None:
|
|
156
|
-
from .managers.experiment_manager import ExperimentManager
|
|
157
|
-
self._experiments = ExperimentManager(self._http)
|
|
158
|
-
return self._experiments
|
|
159
|
-
|
|
160
151
|
@property
|
|
161
152
|
def users(self):
|
|
162
153
|
"""Create-or-update a user and their traits — the stateless-backend
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Datasets — append rows with the site id; manage them with an API key.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
``append`` and ``export`` work as before (the site id, an export token).
|
|
4
|
+
Everything else — create, list, get, rows, update, archive, restore,
|
|
5
|
+
add_columns — goes through the team API with a Trodo API key, the one the
|
|
6
|
+
CLI uses (``TRODO_API_KEY``), because those change rows a site id must not
|
|
7
|
+
reach. Nothing here deletes: archive takes rows out of the working set,
|
|
8
|
+
the version before keeps them, and restore brings them back.
|
|
9
|
+
|
|
10
|
+
Datasets are fixed inputs to evaluate against: each item is a set of
|
|
4
11
|
variable values (``input``) plus an optional gold answer (``expected_output``)
|
|
5
12
|
and free-form ``metadata``. This manager appends items to a dataset addressed by
|
|
6
13
|
UUID or by name; when a name is used and no such dataset exists it is
|
|
@@ -13,7 +20,7 @@ Mirrors ``sdks/trodo-node-sdk/src/managers/DatasetManager.ts`` in shape.
|
|
|
13
20
|
from __future__ import annotations
|
|
14
21
|
|
|
15
22
|
import json
|
|
16
|
-
from typing import Any, Dict, List, Optional, Union
|
|
23
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
17
24
|
|
|
18
25
|
__all__ = ["DatasetManager"]
|
|
19
26
|
|
|
@@ -45,11 +52,157 @@ def _clean_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
45
52
|
return out
|
|
46
53
|
|
|
47
54
|
|
|
55
|
+
def _wire_row(r: Dict[str, Any]) -> Dict[str, Any]:
|
|
56
|
+
"""A row for POST /datasets/:id/items: ``expected`` is sent as ``expected_output``."""
|
|
57
|
+
if not isinstance(r, dict):
|
|
58
|
+
raise ValueError("trodo: each dataset row must be a dict")
|
|
59
|
+
out = dict(r)
|
|
60
|
+
if "expected" in out and "expected_output" not in out:
|
|
61
|
+
out["expected_output"] = out.pop("expected")
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
48
65
|
class DatasetManager:
|
|
49
|
-
"""
|
|
66
|
+
"""The team's datasets: append with the site id, manage with an API key."""
|
|
50
67
|
|
|
51
|
-
def __init__(self, http_client: Any) -> None:
|
|
68
|
+
def __init__(self, http_client: Any, api: Any = None) -> None:
|
|
52
69
|
self._http = http_client
|
|
70
|
+
self._api = api # a ManagementClient, built on first use
|
|
71
|
+
|
|
72
|
+
def _mgmt(self, api_key: Optional[str] = None, api_url: Optional[str] = None) -> Any:
|
|
73
|
+
if api_key or api_url:
|
|
74
|
+
from ..api.management import ManagementClient
|
|
75
|
+
return ManagementClient(api_key, api_url)
|
|
76
|
+
if self._api is None:
|
|
77
|
+
from ..api.management import ManagementClient
|
|
78
|
+
self._api = ManagementClient()
|
|
79
|
+
return self._api
|
|
80
|
+
|
|
81
|
+
def _resolve(self, api: Any, ref: str) -> Dict[str, Any]:
|
|
82
|
+
if not ref:
|
|
83
|
+
raise ValueError("trodo: name the dataset (its name or id)")
|
|
84
|
+
listed = api.get("/datasets").get("datasets") or []
|
|
85
|
+
low = str(ref).lower()
|
|
86
|
+
hit = next((d for d in listed if d.get("id") == ref), None) \
|
|
87
|
+
or next((d for d in listed if d.get("name") == ref), None) \
|
|
88
|
+
or next((d for d in listed if str(d.get("name", "")).lower() == low), None)
|
|
89
|
+
if not hit:
|
|
90
|
+
raise ValueError(f"trodo: no dataset {ref!r}")
|
|
91
|
+
return hit
|
|
92
|
+
|
|
93
|
+
def list(self, *, api_key: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
94
|
+
"""Every dataset of the key's team, with its row counts."""
|
|
95
|
+
return self._mgmt(api_key).get("/datasets").get("datasets") or []
|
|
96
|
+
|
|
97
|
+
def get(self, ref: str, *, api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
98
|
+
"""One dataset by name or id: its columns, derived columns and counts (not the rows)."""
|
|
99
|
+
api = self._mgmt(api_key)
|
|
100
|
+
d = self._resolve(api, ref)
|
|
101
|
+
return api.get(f"/datasets/{d['id']}", {"summary": 1}).get("dataset") or d
|
|
102
|
+
|
|
103
|
+
def create(self, name: str, *, description: Optional[str] = None,
|
|
104
|
+
items: Optional[List[Dict[str, Any]]] = None, api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
105
|
+
"""A new dataset, optionally with rows ({input, output?, expected?, metadata?} or {run_id} …).
|
|
106
|
+
|
|
107
|
+
Returns ``{"dataset": {...}, "written": int}``.
|
|
108
|
+
"""
|
|
109
|
+
if not name or not str(name).strip():
|
|
110
|
+
raise ValueError("trodo: create(name) needs a name")
|
|
111
|
+
api = self._mgmt(api_key)
|
|
112
|
+
body: Dict[str, Any] = {"name": name}
|
|
113
|
+
if description:
|
|
114
|
+
body["description"] = description
|
|
115
|
+
dataset = api.post("/datasets", body).get("dataset") or {}
|
|
116
|
+
written = 0
|
|
117
|
+
if items:
|
|
118
|
+
for i in range(0, len(items), 500):
|
|
119
|
+
out = api.post(f"/datasets/{dataset['id']}/items", {"items": [_wire_row(r) for r in items[i:i + 500]]})
|
|
120
|
+
written += int(out.get("written", out.get("added", 0)) or 0)
|
|
121
|
+
return {"dataset": dataset, "written": written}
|
|
122
|
+
|
|
123
|
+
def rows(self, ref: str, *, rows: Optional[str] = None, eval: Optional[str] = None,
|
|
124
|
+
format: Optional[str] = None, target: Optional[str] = None, version: Optional[int] = None,
|
|
125
|
+
api_key: Optional[str] = None) -> Iterator[Dict[str, Any]]:
|
|
126
|
+
"""The dataset's rows, one dict at a time — the same as ``trodo datasets export``.
|
|
127
|
+
|
|
128
|
+
*rows*: all, expected, no_expected, passed, failed, marked_wrong. *format*: trodo
|
|
129
|
+
(default), openai, dpo. *target* (openai): expected_else_output, output, expected.
|
|
130
|
+
Each Trodo row carries ``id``, so an edit can go straight back through ``update``.
|
|
131
|
+
"""
|
|
132
|
+
api = self._mgmt(api_key)
|
|
133
|
+
d = self._resolve(api, ref)
|
|
134
|
+
params = {"rows": rows, "eval": eval, "format": format, "target": target, "version": version}
|
|
135
|
+
return api.lines(f"/datasets/{d['id']}/export", params)
|
|
136
|
+
|
|
137
|
+
def update(self, ref: str, updates: List[Dict[str, Any]], *, label: str = "person",
|
|
138
|
+
version_name: Optional[str] = None, api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
139
|
+
"""Edit rows by id: ``[{"id", "input"?, "expected"?, "output"?, "metadata"?, "tags"?}]``.
|
|
140
|
+
|
|
141
|
+
*input* is replaced the way rows are added (text, a list of messages, or an object
|
|
142
|
+
of variables); *expected* may be any JSON; *metadata* merges. One version per 5,000
|
|
143
|
+
rows. *label* ``"derived"`` marks a changed Expected as written by a rule or model.
|
|
144
|
+
Returns ``{"updated", "unchanged", "missing", "version"}``.
|
|
145
|
+
"""
|
|
146
|
+
api = self._mgmt(api_key)
|
|
147
|
+
d = self._resolve(api, ref)
|
|
148
|
+
rows = [u for u in (updates or []) if isinstance(u, dict) and u.get("id")]
|
|
149
|
+
if not rows:
|
|
150
|
+
raise ValueError("trodo: update needs rows with an id")
|
|
151
|
+
total: Dict[str, Any] = {"updated": 0, "unchanged": 0, "missing": [], "version": None}
|
|
152
|
+
for i in range(0, len(rows), 5000):
|
|
153
|
+
body: Dict[str, Any] = {"updates": rows[i:i + 5000], "label": "derived" if label == "derived" else "person"}
|
|
154
|
+
if version_name:
|
|
155
|
+
body["version_name"] = version_name
|
|
156
|
+
out = api.post(f"/datasets/{d['id']}/items/update", body)
|
|
157
|
+
total["updated"] += int(out.get("updated") or 0)
|
|
158
|
+
total["unchanged"] += int(out.get("unchanged") or 0)
|
|
159
|
+
total["missing"] += out.get("missing") or []
|
|
160
|
+
total["version"] = out.get("version") or total["version"]
|
|
161
|
+
return total
|
|
162
|
+
|
|
163
|
+
def _move(self, kind: str, ref: str, ids: List[str], version_name: Optional[str], api_key: Optional[str]) -> Dict[str, Any]:
|
|
164
|
+
api = self._mgmt(api_key)
|
|
165
|
+
d = self._resolve(api, ref)
|
|
166
|
+
ids = [str(i) for i in (ids or []) if i]
|
|
167
|
+
if not ids:
|
|
168
|
+
raise ValueError(f"trodo: {kind} needs row ids")
|
|
169
|
+
key = "archived" if kind == "archive" else "restored"
|
|
170
|
+
total: Dict[str, Any] = {key: 0, "unchanged": 0, "version": None}
|
|
171
|
+
for i in range(0, len(ids), 5000):
|
|
172
|
+
body: Dict[str, Any] = {"ids": ids[i:i + 5000]}
|
|
173
|
+
if version_name:
|
|
174
|
+
body["version_name"] = version_name
|
|
175
|
+
out = api.post(f"/datasets/{d['id']}/items/{kind}", body)
|
|
176
|
+
total[key] += int(out.get(key) or 0)
|
|
177
|
+
total["unchanged"] += int(out.get("unchanged") or 0)
|
|
178
|
+
total["version"] = out.get("version") or total["version"]
|
|
179
|
+
return total
|
|
180
|
+
|
|
181
|
+
def archive(self, ref: str, ids: List[str], *, version_name: Optional[str] = None,
|
|
182
|
+
api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
183
|
+
"""Take rows out of the working set (exports, labeling and sources skip them).
|
|
184
|
+
|
|
185
|
+
Nothing is deleted: the version before holds them and ``restore`` brings them back.
|
|
186
|
+
"""
|
|
187
|
+
return self._move("archive", ref, ids, version_name, api_key)
|
|
188
|
+
|
|
189
|
+
def restore(self, ref: str, ids: List[str], *, version_name: Optional[str] = None,
|
|
190
|
+
api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
191
|
+
"""Bring archived rows back into the working set."""
|
|
192
|
+
return self._move("restore", ref, ids, version_name, api_key)
|
|
193
|
+
|
|
194
|
+
def add_columns(self, ref: str, columns: Optional[List[str]] = None, *,
|
|
195
|
+
derived: Optional[List[Dict[str, str]]] = None, api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
196
|
+
"""Add input columns by name, and derived columns ``{"name", "path"}`` or ``{"name", "expr"}``.
|
|
197
|
+
|
|
198
|
+
Columns are only ever added; a name already there is left as it is.
|
|
199
|
+
Returns ``{"added", "columns", "derived_columns"}``.
|
|
200
|
+
"""
|
|
201
|
+
if not columns and not derived:
|
|
202
|
+
raise ValueError("trodo: add_columns needs columns or derived")
|
|
203
|
+
api = self._mgmt(api_key)
|
|
204
|
+
d = self._resolve(api, ref)
|
|
205
|
+
return api.post(f"/datasets/{d['id']}/columns/add", {"columns": list(columns or []), "derived": list(derived or [])})
|
|
53
206
|
|
|
54
207
|
def append(
|
|
55
208
|
self,
|
trodo/otel/processor.py
CHANGED
|
@@ -11,6 +11,9 @@ import threading
|
|
|
11
11
|
from dataclasses import dataclass, asdict
|
|
12
12
|
from typing import Any, Dict, List, Optional, Union
|
|
13
13
|
|
|
14
|
+
# How long end_run waits for the run's queued spans to send before sending the end.
|
|
15
|
+
END_RUN_FLUSH_S = 5.0
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
@dataclass
|
|
16
19
|
class TrodoRun:
|
|
@@ -189,7 +192,21 @@ class TrodoSpanProcessor:
|
|
|
189
192
|
pass
|
|
190
193
|
|
|
191
194
|
def end_run(self, run_id: str, payload: Dict[str, Any]) -> None:
|
|
192
|
-
"""Finalise a Run opened by start_run.
|
|
195
|
+
"""Finalise a Run opened by start_run.
|
|
196
|
+
|
|
197
|
+
The run's spans go first: joined spans wait in the sender's queue, and
|
|
198
|
+
an end that overtakes them gets the run evaluated on a partial trace.
|
|
199
|
+
Bounded, and a failure here never holds up the end.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
with self._lock:
|
|
203
|
+
buffered = self._pending.pop(run_id, None) or []
|
|
204
|
+
if buffered:
|
|
205
|
+
self._send_spans(run_id, [s.to_dict() for s in buffered])
|
|
206
|
+
if self._sender is not None:
|
|
207
|
+
self._sender.flush(END_RUN_FLUSH_S)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
193
210
|
try:
|
|
194
211
|
self._http.post_run_end(run_id, payload)
|
|
195
212
|
except Exception:
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
trodo/__init__.py,sha256=
|
|
2
|
-
trodo/_version.py,sha256=
|
|
3
|
-
trodo/client.py,sha256=
|
|
1
|
+
trodo/__init__.py,sha256=x7zE63FK8CFJ6ATUvsXxeTkvZUaLKXqAJr99I6nnyZc,27099
|
|
2
|
+
trodo/_version.py,sha256=AsSlr9XWZaS6NVC4bCgB5xuBv1s5UrEYJlMsyJQfpMw,22
|
|
3
|
+
trodo/client.py,sha256=eaZtHMtOI-VLgfoqM_KVWMuP_I5Ivs7VFnR0oU96Yqk,23743
|
|
4
4
|
trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
|
|
5
5
|
trodo/user_context.py,sha256=uHCI2WYoOI3cNwdIUEuZdX4VYSu14pzm348ksn6hn_c,8195
|
|
6
6
|
trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
trodo/api/_retry.py,sha256=mHhuzfe3QuYa6j9BqRYVClvnYH0o3WkXXngmSwqWfgY,1214
|
|
8
8
|
trodo/api/async_client.py,sha256=qF97NbvoYbNhem8uXtPh14EG4JVAhEAzUCYn7busaaA,3609
|
|
9
|
-
trodo/api/endpoints.py,sha256=
|
|
10
|
-
trodo/api/http_client.py,sha256=
|
|
9
|
+
trodo/api/endpoints.py,sha256=jToYE2u5EWbkVLiRnEEGRAPtE-EbjNFMPvSFF-EfVt4,1323
|
|
10
|
+
trodo/api/http_client.py,sha256=kNqa18LuDC0f3eQFN3PQJk4I2PCB1McfSmRP_U_FuQQ,12103
|
|
11
|
+
trodo/api/management.py,sha256=uwhr0ZI1pvguz5-itW1KcgRj0TXM2n4KDa-hGIaBdTA,4023
|
|
11
12
|
trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
13
|
trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
|
|
13
14
|
trodo/delivery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
15
|
trodo/delivery/sender.py,sha256=urO7m6mRMnSEW7_cNAmJ4A6jtDNxuihZHOxCx1jQScI,21348
|
|
15
16
|
trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
trodo/managers/dataset_manager.py,sha256=
|
|
17
|
-
trodo/managers/experiment_manager.py,sha256=V-vemLtenfSI5VIcP0HPiXzEy8AyTrRrbJVCA9Z6wjo,10579
|
|
17
|
+
trodo/managers/dataset_manager.py,sha256=HTFszK-wKmwjW-aw7xJWr5EKvDpJ_om496MLDr3gelg,13920
|
|
18
18
|
trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
|
|
19
19
|
trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8pK1E,2882
|
|
20
20
|
trodo/managers/prompt_manager.py,sha256=qFVHgh7PW-SlgHp62ybirdYsJBg-fcQVMTTU3jPqFvo,13435
|
|
@@ -23,7 +23,7 @@ trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
|
|
|
23
23
|
trodo/otel/auto_instrument.py,sha256=M5YVpFoPqn9bQohDFM7yyADrVGJPqFBToJLnBO7nOSA,29029
|
|
24
24
|
trodo/otel/context.py,sha256=Jd0aTc0Q-1dM5kXXinhZD8YtnpdKgvneNiODyboVGKY,1418
|
|
25
25
|
trodo/otel/helpers.py,sha256=XOMWcgZHaq5SQbkFxDaXPE4CDFjn01xjJmJ1vIxvwpw,20730
|
|
26
|
-
trodo/otel/processor.py,sha256=
|
|
26
|
+
trodo/otel/processor.py,sha256=GiMQR3uKHq_sbqXU00MdyarStaBzrZLmPgU1YnlzZb4,8965
|
|
27
27
|
trodo/otel/prompt_trace.py,sha256=-kywRBU0Q2gWntS9HU-P5i7Kc5U-IJU7UVLjkdC2jw4,13593
|
|
28
28
|
trodo/otel/register.py,sha256=7X3AfFQ5YvHGOUmdq8J38LQGvOJFPD907yT3uAp90lI,9607
|
|
29
29
|
trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
|
|
@@ -40,7 +40,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
|
|
|
40
40
|
trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
|
|
41
41
|
trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
|
|
42
42
|
trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
|
|
43
|
-
trodo_python-
|
|
44
|
-
trodo_python-
|
|
45
|
-
trodo_python-
|
|
46
|
-
trodo_python-
|
|
43
|
+
trodo_python-3.0.0.dist-info/METADATA,sha256=Uq0WsyxJPpE-A8CBpSrafzEvniEf2a__kDIEE9055Ls,25307
|
|
44
|
+
trodo_python-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
45
|
+
trodo_python-3.0.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
|
|
46
|
+
trodo_python-3.0.0.dist-info/RECORD,,
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
"""Experiment manager — managed runs and external ingest.
|
|
2
|
-
|
|
3
|
-
Managed experiments (prompt + model over a dataset snapshot) use
|
|
4
|
-
``create``, ``run``, ``get``, ``wait``, and ``compare``.
|
|
5
|
-
External agent outputs use ``ingest``.
|
|
6
|
-
|
|
7
|
-
Mirrors ``sdks/trodo-node-sdk/src/managers/ExperimentManager.ts``.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import time
|
|
13
|
-
from typing import Any, Dict, List, Optional, Union
|
|
14
|
-
|
|
15
|
-
__all__ = ["ExperimentManager"]
|
|
16
|
-
|
|
17
|
-
_TERMINAL = frozenset({"completed", "failed"})
|
|
18
|
-
|
|
19
|
-
# Optional per-output fields for ingest
|
|
20
|
-
_OUTPUT_OPTIONAL = (
|
|
21
|
-
"item_position",
|
|
22
|
-
"dataset_item_id",
|
|
23
|
-
"expected_output",
|
|
24
|
-
"query",
|
|
25
|
-
"context",
|
|
26
|
-
"cost_usd",
|
|
27
|
-
"latency_ms",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _clean_output(output: Dict[str, Any]) -> Dict[str, Any]:
|
|
32
|
-
if not isinstance(output, dict):
|
|
33
|
-
raise ValueError("trodo: each experiment output must be a dict")
|
|
34
|
-
if output.get("output") is None:
|
|
35
|
-
raise ValueError("trodo: each output requires an 'output' (str)")
|
|
36
|
-
if output.get("item_position") is None and output.get("dataset_item_id") is None:
|
|
37
|
-
raise ValueError(
|
|
38
|
-
"trodo: each output requires 'item_position' and/or 'dataset_item_id'"
|
|
39
|
-
)
|
|
40
|
-
out: Dict[str, Any] = {"output": output["output"]}
|
|
41
|
-
for key in _OUTPUT_OPTIONAL:
|
|
42
|
-
if output.get(key) is not None:
|
|
43
|
-
out[key] = output[key]
|
|
44
|
-
return out
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _unwrap_experiment(res: Any) -> Dict[str, Any]:
|
|
48
|
-
if isinstance(res, dict) and "experiment" in res:
|
|
49
|
-
return res["experiment"]
|
|
50
|
-
return res if isinstance(res, dict) else {}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _build_managed_body(
|
|
54
|
-
*,
|
|
55
|
-
dataset: Optional[str] = None,
|
|
56
|
-
dataset_id: Optional[str] = None,
|
|
57
|
-
dataset_version_no: Optional[int] = None,
|
|
58
|
-
name: Optional[str] = None,
|
|
59
|
-
prompt_ref: Optional[Dict[str, Any]] = None,
|
|
60
|
-
prompt_version_id: Optional[str] = None,
|
|
61
|
-
prompt_id: Optional[str] = None,
|
|
62
|
-
use_prompt: Optional[bool] = None,
|
|
63
|
-
models: Optional[List[Dict[str, Any]]] = None,
|
|
64
|
-
scorer_ids: Optional[List[Union[str, int]]] = None,
|
|
65
|
-
evaluator_ids: Optional[List[Union[str, int]]] = None,
|
|
66
|
-
deep_eval: Optional[bool] = None,
|
|
67
|
-
stability_runs: Optional[int] = None,
|
|
68
|
-
row_limit: Optional[int] = None,
|
|
69
|
-
concurrency: Optional[int] = None,
|
|
70
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
71
|
-
) -> Dict[str, Any]:
|
|
72
|
-
body: Dict[str, Any] = {"models": models or []}
|
|
73
|
-
ds = dataset_id or dataset
|
|
74
|
-
if ds:
|
|
75
|
-
body["dataset_id"] = ds
|
|
76
|
-
if dataset_version_no is not None:
|
|
77
|
-
body["dataset_version_no"] = dataset_version_no
|
|
78
|
-
if name is not None:
|
|
79
|
-
body["name"] = name
|
|
80
|
-
if prompt_ref is not None:
|
|
81
|
-
body["prompt_ref"] = prompt_ref
|
|
82
|
-
if prompt_version_id is not None:
|
|
83
|
-
body["prompt_version_id"] = prompt_version_id
|
|
84
|
-
if prompt_id is not None:
|
|
85
|
-
body["prompt_id"] = prompt_id
|
|
86
|
-
if use_prompt is False:
|
|
87
|
-
body["use_prompt"] = False
|
|
88
|
-
ids = scorer_ids if scorer_ids is not None else evaluator_ids
|
|
89
|
-
if ids is not None:
|
|
90
|
-
body["scorer_ids"] = ids
|
|
91
|
-
body["evaluator_ids"] = ids
|
|
92
|
-
if deep_eval is not None:
|
|
93
|
-
body["deep_eval"] = deep_eval
|
|
94
|
-
if stability_runs is not None:
|
|
95
|
-
body["stability_runs"] = stability_runs
|
|
96
|
-
if row_limit is not None:
|
|
97
|
-
body["row_limit"] = row_limit
|
|
98
|
-
if concurrency is not None:
|
|
99
|
-
body["concurrency"] = concurrency
|
|
100
|
-
if metadata is not None:
|
|
101
|
-
body["metadata"] = metadata
|
|
102
|
-
return body
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class ExperimentManager:
|
|
106
|
-
"""Run and inspect Trodo experiments programmatically."""
|
|
107
|
-
|
|
108
|
-
def __init__(self, http_client: Any) -> None:
|
|
109
|
-
self._http = http_client
|
|
110
|
-
|
|
111
|
-
def ingest(
|
|
112
|
-
self,
|
|
113
|
-
dataset: str,
|
|
114
|
-
outputs: List[Dict[str, Any]],
|
|
115
|
-
name: Optional[str] = None,
|
|
116
|
-
task_name: Optional[str] = None,
|
|
117
|
-
judge: Optional[Dict[str, Any]] = None,
|
|
118
|
-
evaluator_ids: Optional[List[str]] = None,
|
|
119
|
-
dataset_version_no: Optional[int] = None,
|
|
120
|
-
) -> Dict[str, Any]:
|
|
121
|
-
"""Ingest externally-produced outputs and grade them (external task path)."""
|
|
122
|
-
if not dataset:
|
|
123
|
-
raise ValueError("trodo: ingest_experiment(dataset) requires a dataset")
|
|
124
|
-
if not outputs:
|
|
125
|
-
raise ValueError("trodo: ingest_experiment requires outputs")
|
|
126
|
-
|
|
127
|
-
body: Dict[str, Any] = {
|
|
128
|
-
"dataset": dataset,
|
|
129
|
-
"outputs": [_clean_output(o) for o in outputs],
|
|
130
|
-
}
|
|
131
|
-
if name is not None:
|
|
132
|
-
body["name"] = name
|
|
133
|
-
if task_name is not None:
|
|
134
|
-
body["task_name"] = task_name
|
|
135
|
-
if judge is not None:
|
|
136
|
-
body["judge"] = judge
|
|
137
|
-
if evaluator_ids is not None:
|
|
138
|
-
body["evaluator_ids"] = evaluator_ids
|
|
139
|
-
if dataset_version_no is not None:
|
|
140
|
-
body["dataset_version_no"] = dataset_version_no
|
|
141
|
-
|
|
142
|
-
return _unwrap_experiment(self._http.ingest_experiment(body))
|
|
143
|
-
|
|
144
|
-
def create(
|
|
145
|
-
self,
|
|
146
|
-
*,
|
|
147
|
-
dataset: Optional[str] = None,
|
|
148
|
-
dataset_id: Optional[str] = None,
|
|
149
|
-
models: List[Dict[str, Any]],
|
|
150
|
-
dataset_version_no: Optional[int] = None,
|
|
151
|
-
name: Optional[str] = None,
|
|
152
|
-
prompt_ref: Optional[Dict[str, Any]] = None,
|
|
153
|
-
prompt_version_id: Optional[str] = None,
|
|
154
|
-
prompt_id: Optional[str] = None,
|
|
155
|
-
use_prompt: Optional[bool] = None,
|
|
156
|
-
scorer_ids: Optional[List[Union[str, int]]] = None,
|
|
157
|
-
evaluator_ids: Optional[List[Union[str, int]]] = None,
|
|
158
|
-
deep_eval: Optional[bool] = None,
|
|
159
|
-
stability_runs: Optional[int] = None,
|
|
160
|
-
row_limit: Optional[int] = None,
|
|
161
|
-
concurrency: Optional[int] = None,
|
|
162
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
163
|
-
) -> Dict[str, Any]:
|
|
164
|
-
"""Create an experiment (immutable refs, status pending)."""
|
|
165
|
-
self._validate_run_params(dataset, dataset_id, models)
|
|
166
|
-
body = _build_managed_body(
|
|
167
|
-
dataset=dataset,
|
|
168
|
-
dataset_id=dataset_id,
|
|
169
|
-
dataset_version_no=dataset_version_no,
|
|
170
|
-
name=name,
|
|
171
|
-
prompt_ref=prompt_ref,
|
|
172
|
-
prompt_version_id=prompt_version_id,
|
|
173
|
-
prompt_id=prompt_id,
|
|
174
|
-
use_prompt=use_prompt,
|
|
175
|
-
models=models,
|
|
176
|
-
scorer_ids=scorer_ids,
|
|
177
|
-
evaluator_ids=evaluator_ids,
|
|
178
|
-
deep_eval=deep_eval,
|
|
179
|
-
stability_runs=stability_runs,
|
|
180
|
-
row_limit=row_limit,
|
|
181
|
-
concurrency=concurrency,
|
|
182
|
-
metadata=metadata,
|
|
183
|
-
)
|
|
184
|
-
return _unwrap_experiment(self._http.create_experiment(body))
|
|
185
|
-
|
|
186
|
-
def run(
|
|
187
|
-
self,
|
|
188
|
-
*,
|
|
189
|
-
dataset: Optional[str] = None,
|
|
190
|
-
dataset_id: Optional[str] = None,
|
|
191
|
-
models: List[Dict[str, Any]],
|
|
192
|
-
dataset_version_no: Optional[int] = None,
|
|
193
|
-
name: Optional[str] = None,
|
|
194
|
-
prompt_ref: Optional[Dict[str, Any]] = None,
|
|
195
|
-
prompt_version_id: Optional[str] = None,
|
|
196
|
-
prompt_id: Optional[str] = None,
|
|
197
|
-
use_prompt: Optional[bool] = None,
|
|
198
|
-
scorer_ids: Optional[List[Union[str, int]]] = None,
|
|
199
|
-
evaluator_ids: Optional[List[Union[str, int]]] = None,
|
|
200
|
-
deep_eval: Optional[bool] = None,
|
|
201
|
-
stability_runs: Optional[int] = None,
|
|
202
|
-
row_limit: Optional[int] = None,
|
|
203
|
-
concurrency: Optional[int] = None,
|
|
204
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
205
|
-
) -> Dict[str, Any]:
|
|
206
|
-
"""Create and execute an experiment synchronously."""
|
|
207
|
-
self._validate_run_params(dataset, dataset_id, models)
|
|
208
|
-
body = _build_managed_body(
|
|
209
|
-
dataset=dataset,
|
|
210
|
-
dataset_id=dataset_id,
|
|
211
|
-
dataset_version_no=dataset_version_no,
|
|
212
|
-
name=name,
|
|
213
|
-
prompt_ref=prompt_ref,
|
|
214
|
-
prompt_version_id=prompt_version_id,
|
|
215
|
-
prompt_id=prompt_id,
|
|
216
|
-
use_prompt=use_prompt,
|
|
217
|
-
models=models,
|
|
218
|
-
scorer_ids=scorer_ids,
|
|
219
|
-
evaluator_ids=evaluator_ids,
|
|
220
|
-
deep_eval=deep_eval,
|
|
221
|
-
stability_runs=stability_runs,
|
|
222
|
-
row_limit=row_limit,
|
|
223
|
-
concurrency=concurrency,
|
|
224
|
-
metadata=metadata,
|
|
225
|
-
)
|
|
226
|
-
return _unwrap_experiment(self._http.run_experiment(body))
|
|
227
|
-
|
|
228
|
-
def run_by_id(self, experiment_id: str) -> Dict[str, Any]:
|
|
229
|
-
"""Execute a pending experiment created via ``create``."""
|
|
230
|
-
if not experiment_id:
|
|
231
|
-
raise ValueError("trodo: run_by_id requires an experiment id")
|
|
232
|
-
return _unwrap_experiment(self._http.run_experiment_by_id(experiment_id))
|
|
233
|
-
|
|
234
|
-
def get(self, experiment_id: str) -> Dict[str, Any]:
|
|
235
|
-
"""Fetch an experiment by id."""
|
|
236
|
-
if not experiment_id:
|
|
237
|
-
raise ValueError("trodo: get requires an experiment id")
|
|
238
|
-
res = self._http.get_experiment(experiment_id)
|
|
239
|
-
if isinstance(res, dict) and res.get("__error"):
|
|
240
|
-
raise RuntimeError(f"trodo: get failed (HTTP {res.get('status')})")
|
|
241
|
-
return _unwrap_experiment(res)
|
|
242
|
-
|
|
243
|
-
def wait(
|
|
244
|
-
self,
|
|
245
|
-
experiment_id: str,
|
|
246
|
-
*,
|
|
247
|
-
interval_ms: int = 2500,
|
|
248
|
-
timeout_ms: int = 600_000,
|
|
249
|
-
) -> Dict[str, Any]:
|
|
250
|
-
"""Poll until the experiment reaches a terminal status."""
|
|
251
|
-
deadline = time.monotonic() + (timeout_ms / 1000.0)
|
|
252
|
-
while True:
|
|
253
|
-
exp = self.get(experiment_id)
|
|
254
|
-
status = str(exp.get("status") or "").lower()
|
|
255
|
-
if status in _TERMINAL:
|
|
256
|
-
return exp
|
|
257
|
-
if time.monotonic() >= deadline:
|
|
258
|
-
raise TimeoutError(
|
|
259
|
-
f"trodo: wait timed out after {timeout_ms}ms (status: {status or 'unknown'})"
|
|
260
|
-
)
|
|
261
|
-
time.sleep(interval_ms / 1000.0)
|
|
262
|
-
|
|
263
|
-
def compare(
|
|
264
|
-
self, a_id: str, b_id: str, *, strict: bool = False
|
|
265
|
-
) -> Dict[str, Any]:
|
|
266
|
-
"""Compare two experiments side by side."""
|
|
267
|
-
if not a_id or not b_id:
|
|
268
|
-
raise ValueError("trodo: compare requires two experiment ids")
|
|
269
|
-
res = self._http.compare_experiments(a_id, b_id, strict=strict)
|
|
270
|
-
if isinstance(res, dict) and res.get("__error"):
|
|
271
|
-
raise RuntimeError(f"trodo: compare failed (HTTP {res.get('status')})")
|
|
272
|
-
return res if isinstance(res, dict) else {}
|
|
273
|
-
|
|
274
|
-
@staticmethod
|
|
275
|
-
def _validate_run_params(
|
|
276
|
-
dataset: Optional[str],
|
|
277
|
-
dataset_id: Optional[str],
|
|
278
|
-
models: Optional[List[Dict[str, Any]]],
|
|
279
|
-
) -> None:
|
|
280
|
-
if not models:
|
|
281
|
-
raise ValueError("trodo: run requires a non-empty models list")
|
|
282
|
-
if not dataset and not dataset_id:
|
|
283
|
-
raise ValueError("trodo: run requires dataset or dataset_id")
|
|
284
|
-
for model in models:
|
|
285
|
-
if not model.get("credential_id") or not model.get("model"):
|
|
286
|
-
raise ValueError("trodo: each model needs credential_id and model")
|
|
File without changes
|