trodo-python 2.15.0__tar.gz → 2.16.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.
Files changed (58) hide show
  1. {trodo_python-2.15.0 → trodo_python-2.16.0}/PKG-INFO +1 -1
  2. {trodo_python-2.15.0 → trodo_python-2.16.0}/pyproject.toml +1 -1
  3. trodo_python-2.16.0/tests/test_datasets.py +102 -0
  4. trodo_python-2.16.0/tests/test_experiments.py +120 -0
  5. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/__init__.py +62 -1
  6. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/api/endpoints.py +3 -0
  7. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/api/http_client.py +10 -0
  8. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/client.py +18 -0
  9. trodo_python-2.16.0/trodo/managers/dataset_manager.py +74 -0
  10. trodo_python-2.16.0/trodo/managers/experiment_manager.py +105 -0
  11. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo_python.egg-info/PKG-INFO +1 -1
  12. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo_python.egg-info/SOURCES.txt +4 -0
  13. {trodo_python-2.15.0 → trodo_python-2.16.0}/README.md +0 -0
  14. {trodo_python-2.15.0 → trodo_python-2.16.0}/setup.cfg +0 -0
  15. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_anon_distinct_id.py +0 -0
  16. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_auto_instrument_fixes.py +0 -0
  17. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_cross_process_session.py +0 -0
  18. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_end_run.py +0 -0
  19. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_error_enrichment.py +0 -0
  20. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_llm_usage_cost.py +0 -0
  21. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_processor_methods.py +0 -0
  22. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_prompt_trace.py +0 -0
  23. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_prompts.py +0 -0
  24. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_register_otel.py +0 -0
  25. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_start_run.py +0 -0
  26. {trodo_python-2.15.0 → trodo_python-2.16.0}/tests/test_wrap_agent_unchanged.py +0 -0
  27. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/api/__init__.py +0 -0
  28. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/api/async_client.py +0 -0
  29. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/auto/__init__.py +0 -0
  30. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/auto/auto_event_manager.py +0 -0
  31. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/managers/__init__.py +0 -0
  32. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/managers/group_manager.py +0 -0
  33. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/managers/people_manager.py +0 -0
  34. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/managers/prompt_manager.py +0 -0
  35. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/__init__.py +0 -0
  36. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/auto_instrument.py +0 -0
  37. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/context.py +0 -0
  38. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/helpers.py +0 -0
  39. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/processor.py +0 -0
  40. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/prompt_trace.py +0 -0
  41. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/register.py +0 -0
  42. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/transport.py +0 -0
  43. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/otel/wrap_agent.py +0 -0
  44. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/prompts/__init__.py +0 -0
  45. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/prompts/compile.py +0 -0
  46. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/prompts/template.py +0 -0
  47. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/prompts/types.py +0 -0
  48. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/queue/__init__.py +0 -0
  49. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/queue/batch_flusher.py +0 -0
  50. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/queue/event_queue.py +0 -0
  51. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/session/__init__.py +0 -0
  52. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/session/server_session.py +0 -0
  53. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/session/session_manager.py +0 -0
  54. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/types.py +0 -0
  55. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo/user_context.py +0 -0
  56. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo_python.egg-info/dependency_links.txt +0 -0
  57. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo_python.egg-info/requires.txt +0 -0
  58. {trodo_python-2.15.0 → trodo_python-2.16.0}/trodo_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.15.0
3
+ Version: 2.16.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trodo-python"
7
- version = "2.15.0"
7
+ version = "2.16.0"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -0,0 +1,102 @@
1
+ """Dataset ingest — DatasetManager.append.
2
+
3
+ Uses a fake HTTP client so no network is touched; asserts on the exact
4
+ ref + body sent to the backend and the response returned to the caller.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from trodo.managers.dataset_manager import DatasetManager
10
+
11
+
12
+ class FakeHttp:
13
+ """Records the last append call; mirrors the prompt-test fakeApi."""
14
+
15
+ def __init__(self, response=None):
16
+ self._response = response or {
17
+ "dataset_id": "ds_1",
18
+ "dataset_name": "support-eval",
19
+ "appended": 1,
20
+ "item_count": 1,
21
+ }
22
+ self.calls = 0
23
+ self.last_ref = None
24
+ self.last_body = None
25
+
26
+ def append_dataset_items(self, ref, body):
27
+ self.calls += 1
28
+ self.last_ref = ref
29
+ self.last_body = body
30
+ return self._response
31
+
32
+
33
+ def test_append_posts_ref_and_body_and_returns_response():
34
+ http = FakeHttp()
35
+ mgr = DatasetManager(http)
36
+ res = mgr.append(
37
+ "support-eval",
38
+ [{"input": {"q": "where is my order"}, "expected_output": "tracking link"}],
39
+ )
40
+
41
+ assert http.calls == 1
42
+ assert http.last_ref == "support-eval"
43
+ assert http.last_body == {
44
+ "items": [
45
+ {"input": {"q": "where is my order"}, "expected_output": "tracking link"}
46
+ ],
47
+ "create": True,
48
+ }
49
+ assert res["dataset_id"] == "ds_1"
50
+ assert res["item_count"] == 1
51
+
52
+
53
+ def test_append_wraps_a_single_dict_into_a_list():
54
+ http = FakeHttp()
55
+ DatasetManager(http).append("ds", {"input": {"x": 1}})
56
+ assert http.last_body["items"] == [{"input": {"x": 1}}]
57
+
58
+
59
+ def test_append_drops_none_optional_fields():
60
+ http = FakeHttp()
61
+ DatasetManager(http).append(
62
+ "ds",
63
+ [
64
+ {
65
+ "input": {"x": 1},
66
+ "expected_output": None,
67
+ "metadata": {"k": "v"},
68
+ "source_trace_ref": None,
69
+ }
70
+ ],
71
+ )
72
+ # None fields dropped; present ones kept.
73
+ assert http.last_body["items"] == [{"input": {"x": 1}, "metadata": {"k": "v"}}]
74
+
75
+
76
+ def test_append_passes_create_false_through():
77
+ http = FakeHttp()
78
+ DatasetManager(http).append("ds", [{"input": {"x": 1}}], create=False)
79
+ assert http.last_body["create"] is False
80
+
81
+
82
+ def test_append_keeps_source_trace_ref():
83
+ http = FakeHttp()
84
+ DatasetManager(http).append(
85
+ "ds", [{"input": {"x": 1}, "source_trace_ref": {"run_id": "r1"}}]
86
+ )
87
+ assert http.last_body["items"][0]["source_trace_ref"] == {"run_id": "r1"}
88
+
89
+
90
+ def test_append_requires_a_ref():
91
+ with pytest.raises(ValueError, match="requires a dataset ref"):
92
+ DatasetManager(FakeHttp()).append("", [{"input": {"x": 1}}])
93
+
94
+
95
+ def test_append_requires_items():
96
+ with pytest.raises(ValueError, match="requires items"):
97
+ DatasetManager(FakeHttp()).append("ds", [])
98
+
99
+
100
+ def test_append_requires_input_on_each_item():
101
+ with pytest.raises(ValueError, match="requires an 'input'"):
102
+ DatasetManager(FakeHttp()).append("ds", [{"expected_output": "x"}])
@@ -0,0 +1,120 @@
1
+ """Experiment ingest — ExperimentManager.ingest.
2
+
3
+ Uses a fake HTTP client so no network is touched; asserts on the exact body
4
+ posted to experiments/ingest and the experiment dict returned to the caller.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from trodo.managers.experiment_manager import ExperimentManager
10
+
11
+
12
+ class FakeHttp:
13
+ """Records the last ingest call; mirrors the prompt-test fakeApi."""
14
+
15
+ def __init__(self, response=None):
16
+ self._response = response if response is not None else {
17
+ "experiment": {"id": "exp_1", "status": "grading"}
18
+ }
19
+ self.calls = 0
20
+ self.last_body = None
21
+
22
+ def ingest_experiment(self, body):
23
+ self.calls += 1
24
+ self.last_body = body
25
+ return self._response
26
+
27
+
28
+ def test_ingest_posts_body_and_returns_experiment():
29
+ http = FakeHttp()
30
+ mgr = ExperimentManager(http)
31
+ exp = mgr.ingest(
32
+ "support-eval",
33
+ [{"item_position": 0, "output": "here is your tracking link"}],
34
+ )
35
+
36
+ assert http.calls == 1
37
+ assert http.last_body == {
38
+ "dataset": "support-eval",
39
+ "outputs": [{"item_position": 0, "output": "here is your tracking link"}],
40
+ }
41
+ assert exp == {"id": "exp_1", "status": "grading"}
42
+
43
+
44
+ def test_ingest_includes_all_optional_fields_when_set():
45
+ http = FakeHttp()
46
+ ExperimentManager(http).ingest(
47
+ "ds",
48
+ [{"item_position": 1, "output": "o"}],
49
+ name="run-1",
50
+ task_name="qa",
51
+ judge={"credential_id": "cred_1", "provider": "openai", "model": "gpt-4o"},
52
+ evaluator_ids=["eval_a", "eval_b"],
53
+ dataset_version_no=3,
54
+ )
55
+ assert http.last_body == {
56
+ "dataset": "ds",
57
+ "outputs": [{"item_position": 1, "output": "o"}],
58
+ "name": "run-1",
59
+ "task_name": "qa",
60
+ "judge": {"credential_id": "cred_1", "provider": "openai", "model": "gpt-4o"},
61
+ "evaluator_ids": ["eval_a", "eval_b"],
62
+ "dataset_version_no": 3,
63
+ }
64
+
65
+
66
+ def test_ingest_omits_unset_optional_fields():
67
+ http = FakeHttp()
68
+ ExperimentManager(http).ingest("ds", [{"item_position": 0, "output": "o"}])
69
+ assert set(http.last_body.keys()) == {"dataset", "outputs"}
70
+
71
+
72
+ def test_ingest_drops_none_per_output_fields():
73
+ http = FakeHttp()
74
+ ExperimentManager(http).ingest(
75
+ "ds",
76
+ [
77
+ {
78
+ "item_position": 0,
79
+ "output": "o",
80
+ "expected_output": "gold",
81
+ "query": None,
82
+ "context": None,
83
+ "cost_usd": 0.0021,
84
+ "latency_ms": None,
85
+ }
86
+ ],
87
+ )
88
+ assert http.last_body["outputs"] == [
89
+ {"item_position": 0, "output": "o", "expected_output": "gold", "cost_usd": 0.0021}
90
+ ]
91
+
92
+
93
+ def test_ingest_keeps_item_position_zero():
94
+ # item_position 0 is falsy but valid — must not be dropped.
95
+ http = FakeHttp()
96
+ ExperimentManager(http).ingest("ds", [{"item_position": 0, "output": "o"}])
97
+ assert http.last_body["outputs"][0]["item_position"] == 0
98
+
99
+
100
+ def test_ingest_returns_raw_response_when_no_experiment_key():
101
+ http = FakeHttp(response={"error": "bad"})
102
+ out = ExperimentManager(http).ingest("ds", [{"item_position": 0, "output": "o"}])
103
+ assert out == {"error": "bad"}
104
+
105
+
106
+ def test_ingest_requires_a_dataset():
107
+ with pytest.raises(ValueError, match="requires a dataset"):
108
+ ExperimentManager(FakeHttp()).ingest("", [{"item_position": 0, "output": "o"}])
109
+
110
+
111
+ def test_ingest_requires_outputs():
112
+ with pytest.raises(ValueError, match="requires outputs"):
113
+ ExperimentManager(FakeHttp()).ingest("ds", [])
114
+
115
+
116
+ def test_ingest_requires_item_position_and_output():
117
+ with pytest.raises(ValueError, match="item_position"):
118
+ ExperimentManager(FakeHttp()).ingest("ds", [{"output": "o"}])
119
+ with pytest.raises(ValueError, match="requires an 'output'"):
120
+ ExperimentManager(FakeHttp()).ingest("ds", [{"item_position": 0}])
@@ -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.15.0"
44
+ __version__ = "2.16.0"
45
45
 
46
46
  from typing import Any, Callable, Dict, List, Optional, Union
47
47
 
@@ -119,6 +119,9 @@ __all__ = [
119
119
  "TemplateError",
120
120
  "ManagedPrompt",
121
121
  "PromptSummary",
122
+ # Datasets & experiments ingest
123
+ "append_dataset",
124
+ "ingest_experiment",
122
125
  ]
123
126
 
124
127
  # ============================================================================
@@ -344,6 +347,64 @@ def render_template(
344
347
  return _render_template(template, merged, strict=strict)
345
348
 
346
349
 
350
+ # ----------------------------------------------------------------------------
351
+ # Datasets & experiments ingest
352
+ # ----------------------------------------------------------------------------
353
+
354
+ def append_dataset(
355
+ ref: str,
356
+ items: Union[Dict[str, Any], List[Dict[str, Any]]],
357
+ create: bool = True,
358
+ ) -> Dict[str, Any]:
359
+ """Append items to a dataset by UUID or name.
360
+
361
+ *items* is a list of dicts shaped ``{"input": {<var>: value, ...},
362
+ "expected_output"?: str, "metadata"?: dict, "source_trace_ref"?: dict}``;
363
+ a single dict is accepted and wrapped. When *ref* is a name that doesn't
364
+ exist, ``create=True`` (default) auto-creates the dataset::
365
+
366
+ trodo.append_dataset("support-eval", [
367
+ {"input": {"q": "where is my order"}, "expected_output": "tracking link"},
368
+ ])
369
+
370
+ Returns ``{"dataset_id", "dataset_name", "appended", "item_count"}``.
371
+ """
372
+ return _get_client().datasets.append(ref, items, create=create)
373
+
374
+
375
+ def ingest_experiment(
376
+ dataset: str,
377
+ outputs: List[Dict[str, Any]],
378
+ name: Optional[str] = None,
379
+ task_name: Optional[str] = None,
380
+ judge: Optional[Dict[str, Any]] = None,
381
+ evaluator_ids: Optional[List[str]] = None,
382
+ dataset_version_no: Optional[int] = None,
383
+ ) -> Dict[str, Any]:
384
+ """Ingest a batch of model *outputs* for *dataset* and grade them.
385
+
386
+ *outputs* is a list of dicts shaped ``{"item_position": int, "output": str,
387
+ "expected_output"?, "query"?, "context"?, "cost_usd"?, "latency_ms"?}``.
388
+ Configure grading with ``judge`` (``{"credential_id", "provider", "model"}``)
389
+ and/or ``evaluator_ids``::
390
+
391
+ exp = trodo.ingest_experiment("support-eval", [
392
+ {"item_position": 0, "output": "here is your tracking link"},
393
+ ], judge={"credential_id": "cred_1", "provider": "openai", "model": "gpt-4o"})
394
+
395
+ Returns the experiment dict.
396
+ """
397
+ return _get_client().experiments.ingest(
398
+ dataset,
399
+ outputs,
400
+ name=name,
401
+ task_name=task_name,
402
+ judge=judge,
403
+ evaluator_ids=evaluator_ids,
404
+ dataset_version_no=dataset_version_no,
405
+ )
406
+
407
+
347
408
  def enable_auto_events() -> None:
348
409
  _get_client().enable_auto_events()
349
410
 
@@ -23,3 +23,6 @@ RUNS_INGEST = "/api/sdk/runs/ingest"
23
23
  RUNS_START = "/api/sdk/runs/start"
24
24
  RUNS_BASE = "/api/sdk/runs" # /runs/{run_id}/end, /spans, /feedback
25
25
  OTLP_TRACES = "/api/sdk/otel/v1/traces"
26
+ # Datasets & experiments ingest
27
+ DATASETS_BASE = "/api/sdk/datasets" # /datasets/{ref}/items
28
+ EXPERIMENTS_INGEST = "/api/sdk/experiments/ingest"
@@ -133,6 +133,16 @@ class HttpClient:
133
133
  def list_prompts(self) -> ApiResult:
134
134
  return self._get("/api/sdk/prompts")
135
135
 
136
+ def append_dataset_items(self, ref: str, body: Dict[str, Any]) -> ApiResult:
137
+ """Append items to a dataset by UUID or name (name is URL-encoded)."""
138
+ from urllib.parse import quote
139
+ return self._request(
140
+ f"/api/sdk/datasets/{quote(str(ref), safe='')}/items", body
141
+ )
142
+
143
+ def ingest_experiment(self, body: Dict[str, Any]) -> ApiResult:
144
+ return self._request("/api/sdk/experiments/ingest", body)
145
+
136
146
  def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
137
147
  return self._request("/api/sdk/track", {"sessionData": session_data})
138
148
 
@@ -77,6 +77,8 @@ class TrodoClient:
77
77
 
78
78
  self._session_manager = SessionManager()
79
79
  self._prompts = None # lazily-built PromptManager
80
+ self._datasets = None # lazily-built DatasetManager
81
+ self._experiments = None # lazily-built ExperimentManager
80
82
 
81
83
  if batch_enabled:
82
84
  self._event_queue: Optional[EventQueue] = EventQueue(batch_size)
@@ -125,6 +127,22 @@ class TrodoClient:
125
127
  self._prompts = PromptManager(self._http)
126
128
  return self._prompts
127
129
 
130
+ @property
131
+ def datasets(self):
132
+ """Append items to the team's evaluation datasets."""
133
+ if self._datasets is None:
134
+ from .managers.dataset_manager import DatasetManager
135
+ self._datasets = DatasetManager(self._http)
136
+ return self._datasets
137
+
138
+ @property
139
+ def experiments(self):
140
+ """Ingest experiment result batches for server-side grading."""
141
+ if self._experiments is None:
142
+ from .managers.experiment_manager import ExperimentManager
143
+ self._experiments = ExperimentManager(self._http)
144
+ return self._experiments
145
+
128
146
  # --------------------------------------------------------------------------
129
147
  # Primary pattern: for_user()
130
148
  # --------------------------------------------------------------------------
@@ -0,0 +1,74 @@
1
+ """Dataset ingest — append evaluation items to a Trodo dataset.
2
+
3
+ Datasets are the fixed inputs an experiment runs against: each item is a set of
4
+ variable values (``input``) plus an optional gold answer (``expected_output``)
5
+ and free-form ``metadata``. This manager appends items to a dataset addressed by
6
+ UUID or by name; when a name is used and no such dataset exists it is
7
+ auto-created (``create=True``, the default), so a first append bootstraps the
8
+ dataset without a separate create call.
9
+
10
+ Mirrors ``sdks/trodo-node-sdk/src/managers/DatasetManager.ts`` in shape.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, List, Union
16
+
17
+ __all__ = ["DatasetManager"]
18
+
19
+ # The optional per-item fields, in wire order. ``input`` is always required.
20
+ _ITEM_OPTIONAL = ("expected_output", "metadata", "source_trace_ref")
21
+
22
+
23
+ def _clean_item(item: Dict[str, Any]) -> Dict[str, Any]:
24
+ if not isinstance(item, dict):
25
+ raise ValueError("trodo: each dataset item must be a dict")
26
+ if "input" not in item or item["input"] is None:
27
+ raise ValueError("trodo: each dataset item requires an 'input' dict")
28
+ out: Dict[str, Any] = {"input": item["input"]}
29
+ for key in _ITEM_OPTIONAL:
30
+ if item.get(key) is not None:
31
+ out[key] = item[key]
32
+ return out
33
+
34
+
35
+ class DatasetManager:
36
+ """Append items to the team's evaluation datasets."""
37
+
38
+ def __init__(self, http_client: Any) -> None:
39
+ self._http = http_client
40
+
41
+ def append(
42
+ self,
43
+ ref: str,
44
+ items: Union[Dict[str, Any], List[Dict[str, Any]]],
45
+ create: bool = True,
46
+ ) -> Dict[str, Any]:
47
+ """Append one or more items to the dataset *ref* (a UUID or a name).
48
+
49
+ *items* is a list of dicts shaped ``{"input": {<var>: value, ...},
50
+ "expected_output"?: str, "metadata"?: dict, "source_trace_ref"?: dict}``;
51
+ a single dict is accepted and wrapped into a one-item list. ``None``
52
+ optional fields are dropped from the wire payload.
53
+
54
+ When *ref* is a name that doesn't exist yet, ``create=True`` (default)
55
+ auto-creates the dataset; pass ``create=False`` to require it to exist.
56
+
57
+ Returns the parsed response dict:
58
+ ``{"dataset_id", "dataset_name", "appended", "item_count"}``.
59
+
60
+ :raises ValueError: if *ref* is empty, *items* is empty, or an item has
61
+ no ``input``.
62
+ """
63
+ if not ref:
64
+ raise ValueError("trodo: append_dataset(ref) requires a dataset ref")
65
+ if isinstance(items, dict):
66
+ items = [items]
67
+ if not items:
68
+ raise ValueError("trodo: append_dataset(ref, items) requires items")
69
+
70
+ body = {
71
+ "items": [_clean_item(i) for i in items],
72
+ "create": bool(create),
73
+ }
74
+ return self._http.append_dataset_items(ref, body)
@@ -0,0 +1,105 @@
1
+ """Experiment ingest — record a batch of model outputs against a dataset.
2
+
3
+ An experiment takes a dataset (the fixed inputs) and a set of *outputs* your
4
+ model produced for each item, then grades them server-side using the judge
5
+ and/or evaluators you name. You run the model yourself and hand Trodo the
6
+ results; Trodo scores and stores them so runs are comparable over time.
7
+
8
+ ``outputs`` are matched to dataset items by ``item_position`` (0-based),
9
+ carrying the produced ``output`` and optional per-row context (``query``,
10
+ ``context``), cost/latency telemetry, and a per-row ``expected_output`` override.
11
+
12
+ Mirrors ``sdks/trodo-node-sdk/src/managers/ExperimentManager.ts`` in shape.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ __all__ = ["ExperimentManager"]
20
+
21
+ # Optional per-output fields, in wire order. ``item_position`` + ``output`` are
22
+ # always required.
23
+ _OUTPUT_OPTIONAL = (
24
+ "expected_output",
25
+ "query",
26
+ "context",
27
+ "cost_usd",
28
+ "latency_ms",
29
+ )
30
+
31
+
32
+ def _clean_output(output: Dict[str, Any]) -> Dict[str, Any]:
33
+ if not isinstance(output, dict):
34
+ raise ValueError("trodo: each experiment output must be a dict")
35
+ if output.get("item_position") is None:
36
+ raise ValueError("trodo: each output requires an 'item_position' (int)")
37
+ if output.get("output") is None:
38
+ raise ValueError("trodo: each output requires an 'output' (str)")
39
+ out: Dict[str, Any] = {
40
+ "item_position": output["item_position"],
41
+ "output": output["output"],
42
+ }
43
+ for key in _OUTPUT_OPTIONAL:
44
+ if output.get(key) is not None:
45
+ out[key] = output[key]
46
+ return out
47
+
48
+
49
+ class ExperimentManager:
50
+ """Ingest experiment result batches for server-side grading."""
51
+
52
+ def __init__(self, http_client: Any) -> None:
53
+ self._http = http_client
54
+
55
+ def ingest(
56
+ self,
57
+ dataset: str,
58
+ outputs: List[Dict[str, Any]],
59
+ name: Optional[str] = None,
60
+ task_name: Optional[str] = None,
61
+ judge: Optional[Dict[str, Any]] = None,
62
+ evaluator_ids: Optional[List[str]] = None,
63
+ dataset_version_no: Optional[int] = None,
64
+ ) -> Dict[str, Any]:
65
+ """Ingest a batch of *outputs* for *dataset* and return the experiment.
66
+
67
+ *dataset* is a dataset name or UUID. *outputs* is a list of dicts shaped
68
+ ``{"item_position": int, "output": str, "expected_output"?, "query"?,
69
+ "context"?, "cost_usd"?, "latency_ms"?}``. ``None`` optional fields are
70
+ dropped from the wire payload.
71
+
72
+ Grading is configured server-side: pass ``judge`` (``{"credential_id",
73
+ "provider", "model"}``) for an LLM judge and/or ``evaluator_ids`` for
74
+ named evaluators. ``dataset_version_no`` pins the dataset version graded
75
+ against.
76
+
77
+ Returns the experiment dict from ``{"experiment": {...}}``.
78
+
79
+ :raises ValueError: if *dataset* is empty, *outputs* is empty, or an
80
+ output is missing ``item_position``/``output``.
81
+ """
82
+ if not dataset:
83
+ raise ValueError("trodo: ingest_experiment(dataset) requires a dataset")
84
+ if not outputs:
85
+ raise ValueError("trodo: ingest_experiment requires outputs")
86
+
87
+ body: Dict[str, Any] = {
88
+ "dataset": dataset,
89
+ "outputs": [_clean_output(o) for o in outputs],
90
+ }
91
+ if name is not None:
92
+ body["name"] = name
93
+ if task_name is not None:
94
+ body["task_name"] = task_name
95
+ if judge is not None:
96
+ body["judge"] = judge
97
+ if evaluator_ids is not None:
98
+ body["evaluator_ids"] = evaluator_ids
99
+ if dataset_version_no is not None:
100
+ body["dataset_version_no"] = dataset_version_no
101
+
102
+ res = self._http.ingest_experiment(body)
103
+ if isinstance(res, dict) and "experiment" in res:
104
+ return res["experiment"]
105
+ return res
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.15.0
3
+ Version: 2.16.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -3,8 +3,10 @@ pyproject.toml
3
3
  tests/test_anon_distinct_id.py
4
4
  tests/test_auto_instrument_fixes.py
5
5
  tests/test_cross_process_session.py
6
+ tests/test_datasets.py
6
7
  tests/test_end_run.py
7
8
  tests/test_error_enrichment.py
9
+ tests/test_experiments.py
8
10
  tests/test_llm_usage_cost.py
9
11
  tests/test_processor_methods.py
10
12
  tests/test_prompt_trace.py
@@ -23,6 +25,8 @@ trodo/api/http_client.py
23
25
  trodo/auto/__init__.py
24
26
  trodo/auto/auto_event_manager.py
25
27
  trodo/managers/__init__.py
28
+ trodo/managers/dataset_manager.py
29
+ trodo/managers/experiment_manager.py
26
30
  trodo/managers/group_manager.py
27
31
  trodo/managers/people_manager.py
28
32
  trodo/managers/prompt_manager.py
File without changes
File without changes