trodo-python 2.26.2__tar.gz → 3.0.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.
- {trodo_python-2.26.2 → trodo_python-3.0.0}/PKG-INFO +1 -1
- {trodo_python-2.26.2 → trodo_python-3.0.0}/pyproject.toml +1 -1
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_datasets.py +82 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/__init__.py +24 -89
- trodo_python-3.0.0/trodo/_version.py +1 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/api/endpoints.py +1 -6
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/api/http_client.py +0 -25
- trodo_python-3.0.0/trodo/api/management.py +90 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/client.py +0 -9
- trodo_python-3.0.0/trodo/managers/dataset_manager.py +287 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo_python.egg-info/SOURCES.txt +1 -2
- trodo_python-2.26.2/tests/test_experiments.py +0 -176
- trodo_python-2.26.2/trodo/_version.py +0 -1
- trodo_python-2.26.2/trodo/managers/dataset_manager.py +0 -134
- trodo_python-2.26.2/trodo/managers/experiment_manager.py +0 -286
- {trodo_python-2.26.2 → trodo_python-3.0.0}/README.md +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/setup.cfg +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_auto_instrument_fixes.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_end_run.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_end_run_order.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_framework_deference.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_get_prompt_version_types.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_instrumentor_resolution.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_multi_agent_shape.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_prompt_fingerprint.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_prompt_trace.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_prompts.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_register_otel.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_sender.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_start_run.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_user_traits.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/api/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/api/_retry.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/api/async_client.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/delivery/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/delivery/sender.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/managers/prompt_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/managers/user_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/auto_instrument.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/context.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/processor.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/prompt_trace.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/register.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/transport.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/otel/wrap_agent.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/prompts/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/prompts/compile.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/prompts/template.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/prompts/types.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/session/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/session/server_session.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/types.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/user_context.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/util/__init__.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo/util/lru.py +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.26.2 → trodo_python-3.0.0}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -144,3 +144,85 @@ def test_append_accepts_unit_references_without_input():
|
|
|
144
144
|
{"conversation_id": "c1", "metadata": {"k": 1}},
|
|
145
145
|
{"eval_run_id": "v1"},
|
|
146
146
|
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Management with an API key (the team API the CLI uses)
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
class FakeApi:
|
|
154
|
+
"""Records every call to the team API; datasets are looked up by name."""
|
|
155
|
+
|
|
156
|
+
def __init__(self):
|
|
157
|
+
self.calls = []
|
|
158
|
+
|
|
159
|
+
def get(self, path, params=None):
|
|
160
|
+
self.calls.append(("GET", path, params))
|
|
161
|
+
if path == "/datasets":
|
|
162
|
+
return {"datasets": [{"id": "d1", "name": "Refunds"}]}
|
|
163
|
+
return {"dataset": {"id": "d1", "name": "Refunds", "columns": ["q"]}}
|
|
164
|
+
|
|
165
|
+
def post(self, path, body=None):
|
|
166
|
+
self.calls.append(("POST", path, body))
|
|
167
|
+
if path == "/datasets":
|
|
168
|
+
return {"dataset": {"id": "d2", "name": body["name"]}}
|
|
169
|
+
if path.endswith("/items/update"):
|
|
170
|
+
return {"updated": len(body["updates"]), "unchanged": 0, "missing": [], "version": 4}
|
|
171
|
+
if path.endswith("/items/archive"):
|
|
172
|
+
return {"archived": len(body["ids"]), "unchanged": 0, "version": 5}
|
|
173
|
+
if path.endswith("/items/restore"):
|
|
174
|
+
return {"restored": len(body["ids"]), "unchanged": 0, "version": 6}
|
|
175
|
+
if path.endswith("/columns/add"):
|
|
176
|
+
return {"added": 1, "columns": ["q", "lang"], "derived_columns": []}
|
|
177
|
+
return {"written": len(body.get("items", []))}
|
|
178
|
+
|
|
179
|
+
def lines(self, path, params=None):
|
|
180
|
+
self.calls.append(("LINES", path, params))
|
|
181
|
+
return iter([{"id": "i1", "input": {"query": "hi"}}])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_create_with_rows_sends_expected_as_expected_output():
|
|
185
|
+
api = FakeApi()
|
|
186
|
+
out = DatasetManager(None, api).create("New", items=[{"input": "2+2?", "expected": {"answer": 4}}])
|
|
187
|
+
assert out == {"dataset": {"id": "d2", "name": "New"}, "written": 1}
|
|
188
|
+
assert api.calls[-1] == ("POST", "/datasets/d2/items", {"items": [{"input": "2+2?", "expected_output": {"answer": 4}}]})
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_update_resolves_the_name_and_sends_one_call():
|
|
192
|
+
api = FakeApi()
|
|
193
|
+
out = DatasetManager(None, api).update("refunds", [{"id": "i1", "input": "redacted"}, {"no": "id"}], label="derived", version_name="pii")
|
|
194
|
+
assert out == {"updated": 1, "unchanged": 0, "missing": [], "version": 4}
|
|
195
|
+
assert api.calls[-1] == ("POST", "/datasets/d1/items/update", {"updates": [{"id": "i1", "input": "redacted"}], "label": "derived", "version_name": "pii"})
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_archive_restore_and_columns():
|
|
199
|
+
api = FakeApi()
|
|
200
|
+
mgr = DatasetManager(None, api)
|
|
201
|
+
assert mgr.archive("Refunds", ["i1", "i2"])["archived"] == 2
|
|
202
|
+
assert mgr.restore("Refunds", ["i1"])["restored"] == 1
|
|
203
|
+
assert mgr.add_columns("Refunds", ["lang"])["added"] == 1
|
|
204
|
+
assert [c[1] for c in api.calls if c[0] == "POST"] == ["/datasets/d1/items/archive", "/datasets/d1/items/restore", "/datasets/d1/columns/add"]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_rows_streams_the_export_with_its_filters():
|
|
208
|
+
api = FakeApi()
|
|
209
|
+
rows = list(DatasetManager(None, api).rows("Refunds", rows="failed", format="openai"))
|
|
210
|
+
assert rows[0]["id"] == "i1"
|
|
211
|
+
assert api.calls[-1] == ("LINES", "/datasets/d1/export", {"rows": "failed", "eval": None, "format": "openai", "target": None, "version": None})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_an_unknown_dataset_and_missing_ids_are_errors():
|
|
215
|
+
mgr = DatasetManager(None, FakeApi())
|
|
216
|
+
with pytest.raises(ValueError, match="no dataset"):
|
|
217
|
+
mgr.update("Nope", [{"id": "i1"}])
|
|
218
|
+
with pytest.raises(ValueError, match="row ids"):
|
|
219
|
+
mgr.archive("Refunds", [])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_management_needs_an_api_key(monkeypatch):
|
|
223
|
+
from trodo.api.management import ManagementClient
|
|
224
|
+
monkeypatch.delenv("TRODO_API_KEY", raising=False)
|
|
225
|
+
with pytest.raises(ValueError, match="TRODO_API_KEY"):
|
|
226
|
+
ManagementClient()
|
|
227
|
+
c = ManagementClient(api_key="trodo_mk_x", api_url="http://localhost:8002/")
|
|
228
|
+
assert c.base == "http://localhost:8002/api/v1"
|
|
@@ -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
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SDK_VERSION = "3.0.0"
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -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
|