render-lab-tasks-braintrust 0.1.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 (24) hide show
  1. render_lab_tasks_braintrust-0.1.0/.gitignore +11 -0
  2. render_lab_tasks_braintrust-0.1.0/LICENSE +21 -0
  3. render_lab_tasks_braintrust-0.1.0/PKG-INFO +84 -0
  4. render_lab_tasks_braintrust-0.1.0/README.md +71 -0
  5. render_lab_tasks_braintrust-0.1.0/pyproject.toml +22 -0
  6. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/__init__.py +1 -0
  7. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/_app.py +3 -0
  8. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/add_feedback.py +18 -0
  9. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/await_review.py +18 -0
  10. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/client.py +453 -0
  11. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/delete_dataset_rows.py +20 -0
  12. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/ensure_dataset.py +18 -0
  13. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/ensure_experiment.py +18 -0
  14. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/gate_experiment.py +18 -0
  15. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/get_review_progress.py +18 -0
  16. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/list_dataset_rows.py +18 -0
  17. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/log_experiment_results.py +20 -0
  18. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/py.typed +0 -0
  19. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/retry.py +5 -0
  20. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/run_eval.py +18 -0
  21. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/summarize_experiment.py +20 -0
  22. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/tasks.py +41 -0
  23. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/types.py +193 -0
  24. render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/upsert_dataset_rows.py +20 -0
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ *.egg-info/
9
+ .env
10
+ .env.*
11
+ !.env.example
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Render Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: render-lab-tasks-braintrust
3
+ Version: 0.1.0
4
+ Summary: braintrust tasks for Render Workflows
5
+ Project-URL: Repository, https://github.com/render-lab/render-tasks-python
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: httpx<0.29,>=0.28
10
+ Requires-Dist: render-lab-tasks-core<0.2,>=0.1.1
11
+ Requires-Dist: render==1.0.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # render-lab-tasks-braintrust
15
+
16
+ Unreleased Python port: **12 registered tasks** from
17
+ [render-lab/render-tasks](https://github.com/render-lab/render-tasks/tree/45f9c2d44bd28e01ae9813e0d2ff56ee6c533816/packages/tasks-braintrust).
18
+ Python 3.12+, Render SDK 1.0.1. Requires core 0.1.1 or later.
19
+
20
+ From the repository root:
21
+
22
+ ```sh
23
+ uv sync --all-packages --locked
24
+ ```
25
+
26
+ Import `render_lab_tasks_braintrust.tasks` to register tasks. The package root is inert.
27
+ Compose the exported `app` with `Workflows.from_workflows`; call tasks through
28
+ `ctx.run`. Every operation also exports `*_impl(ctx, input, *, deps=None)` for
29
+ injection. `Client(http, env)` accepts a caller-owned HTTPX client, and
30
+ `Deps(braintrust=client)` injects it. Default dependencies open and close the HTTP
31
+ client per invocation; credentials are read lazily at use. No HTTP retries or
32
+ background vendor polling occur; `retry.py` owns the durable retry policies.
33
+
34
+ ## Task surface
35
+
36
+ | Registered task | Python export |
37
+ | --- | --- |
38
+ | `braintrust.addFeedback` | `add_feedback` |
39
+ | `braintrust.awaitReview` | `await_review` |
40
+ | `braintrust.deleteDatasetRows` | `delete_dataset_rows` |
41
+ | `braintrust.ensureDataset` | `ensure_dataset` |
42
+ | `braintrust.ensureExperiment` | `ensure_experiment` |
43
+ | `braintrust.gateExperiment` | `gate_experiment` |
44
+ | `braintrust.getReviewProgress` | `get_review_progress` |
45
+ | `braintrust.listDatasetRows` | `list_dataset_rows` |
46
+ | `braintrust.logExperimentResults` | `log_experiment_results` |
47
+ | `braintrust.runEval` | `run_eval` |
48
+ | `braintrust.summarizeExperiment` | `summarize_experiment` |
49
+ | `braintrust.upsertDatasetRows` | `upsert_dataset_rows` |
50
+
51
+ Typed JSON inputs and results are in `types.py`. Task names and JSON field names
52
+ match the pinned source; Python function names use snake_case.
53
+
54
+ ## Environment
55
+
56
+ | Variable | Requirement |
57
+ | --- | --- |
58
+ | `BRAINTRUST_API_KEY` | Required at first API call. |
59
+ | `BRAINTRUST_API_URL` | Optional; https://api.braintrust.dev by default. |
60
+
61
+ ## Behavior and limits
62
+
63
+ Stable row IDs are required for upserts and experiment logs. Eval starts and release gates have no retries. Review polling counts rows carrying a score against root-row totals, matching the source; confirm review semantics with nested spans. Dataset pages and experiment summaries reject serialized results at or above 4 MiB. Gate errors contain the violated thresholds. Review polling and result counts remain unverified against a real project.
64
+
65
+ ## Verification
66
+
67
+ All registered tasks have hermetic contract fixtures executed independently against
68
+ the pinned TS implementation. SDK-backed tasks also have explicit HTTP request
69
+ fixtures. See `tests/test_batch3_contracts.py` and `tests/test_batch3_edges.py`.
70
+ These are not live vendor results.
71
+
72
+ Vendor webhooks subpaths and other registration-free TS exports are outside this
73
+ batch unless explicitly listed above. Full registered-task coverage does not
74
+ imply all supporting exports are ported.
75
+
76
+ Pending scopes, test resources, replay, and hosted checks are in the
77
+ [live-testing backlog](https://github.com/render-lab/render-tasks-python/issues/1)
78
+ and [verification tracker](../../docs/verification-tracker.md).
79
+
80
+ ## Installation
81
+
82
+ ```sh
83
+ pip install render-lab-tasks-braintrust==0.1.0
84
+ ```
@@ -0,0 +1,71 @@
1
+ # render-lab-tasks-braintrust
2
+
3
+ Unreleased Python port: **12 registered tasks** from
4
+ [render-lab/render-tasks](https://github.com/render-lab/render-tasks/tree/45f9c2d44bd28e01ae9813e0d2ff56ee6c533816/packages/tasks-braintrust).
5
+ Python 3.12+, Render SDK 1.0.1. Requires core 0.1.1 or later.
6
+
7
+ From the repository root:
8
+
9
+ ```sh
10
+ uv sync --all-packages --locked
11
+ ```
12
+
13
+ Import `render_lab_tasks_braintrust.tasks` to register tasks. The package root is inert.
14
+ Compose the exported `app` with `Workflows.from_workflows`; call tasks through
15
+ `ctx.run`. Every operation also exports `*_impl(ctx, input, *, deps=None)` for
16
+ injection. `Client(http, env)` accepts a caller-owned HTTPX client, and
17
+ `Deps(braintrust=client)` injects it. Default dependencies open and close the HTTP
18
+ client per invocation; credentials are read lazily at use. No HTTP retries or
19
+ background vendor polling occur; `retry.py` owns the durable retry policies.
20
+
21
+ ## Task surface
22
+
23
+ | Registered task | Python export |
24
+ | --- | --- |
25
+ | `braintrust.addFeedback` | `add_feedback` |
26
+ | `braintrust.awaitReview` | `await_review` |
27
+ | `braintrust.deleteDatasetRows` | `delete_dataset_rows` |
28
+ | `braintrust.ensureDataset` | `ensure_dataset` |
29
+ | `braintrust.ensureExperiment` | `ensure_experiment` |
30
+ | `braintrust.gateExperiment` | `gate_experiment` |
31
+ | `braintrust.getReviewProgress` | `get_review_progress` |
32
+ | `braintrust.listDatasetRows` | `list_dataset_rows` |
33
+ | `braintrust.logExperimentResults` | `log_experiment_results` |
34
+ | `braintrust.runEval` | `run_eval` |
35
+ | `braintrust.summarizeExperiment` | `summarize_experiment` |
36
+ | `braintrust.upsertDatasetRows` | `upsert_dataset_rows` |
37
+
38
+ Typed JSON inputs and results are in `types.py`. Task names and JSON field names
39
+ match the pinned source; Python function names use snake_case.
40
+
41
+ ## Environment
42
+
43
+ | Variable | Requirement |
44
+ | --- | --- |
45
+ | `BRAINTRUST_API_KEY` | Required at first API call. |
46
+ | `BRAINTRUST_API_URL` | Optional; https://api.braintrust.dev by default. |
47
+
48
+ ## Behavior and limits
49
+
50
+ Stable row IDs are required for upserts and experiment logs. Eval starts and release gates have no retries. Review polling counts rows carrying a score against root-row totals, matching the source; confirm review semantics with nested spans. Dataset pages and experiment summaries reject serialized results at or above 4 MiB. Gate errors contain the violated thresholds. Review polling and result counts remain unverified against a real project.
51
+
52
+ ## Verification
53
+
54
+ All registered tasks have hermetic contract fixtures executed independently against
55
+ the pinned TS implementation. SDK-backed tasks also have explicit HTTP request
56
+ fixtures. See `tests/test_batch3_contracts.py` and `tests/test_batch3_edges.py`.
57
+ These are not live vendor results.
58
+
59
+ Vendor webhooks subpaths and other registration-free TS exports are outside this
60
+ batch unless explicitly listed above. Full registered-task coverage does not
61
+ imply all supporting exports are ported.
62
+
63
+ Pending scopes, test resources, replay, and hosted checks are in the
64
+ [live-testing backlog](https://github.com/render-lab/render-tasks-python/issues/1)
65
+ and [verification tracker](../../docs/verification-tracker.md).
66
+
67
+ ## Installation
68
+
69
+ ```sh
70
+ pip install render-lab-tasks-braintrust==0.1.0
71
+ ```
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "render-lab-tasks-braintrust"
7
+ version = "0.1.0"
8
+ description = "braintrust tasks for Render Workflows"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ dependencies = ["render==1.0.1", "httpx>=0.28,<0.29", "render-lab-tasks-core>=0.1.1,<0.2"]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/render-lab/render-tasks-python"
17
+
18
+ [tool.uv.sources]
19
+ render-lab-tasks-core = { workspace = true }
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/render_lab_tasks_braintrust"]
@@ -0,0 +1 @@
1
+ """Import .tasks explicitly to register durable tasks."""
@@ -0,0 +1,3 @@
1
+ from render import Workflows
2
+
3
+ app = Workflows()
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import AddFeedbackInput, AddFeedbackResult
7
+
8
+
9
+ async def add_feedback_impl(
10
+ ctx: TaskContext, input: AddFeedbackInput, *, deps: Deps | None = None
11
+ ) -> AddFeedbackResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.add_feedback(input)
14
+
15
+
16
+ @app.task(name="braintrust.addFeedback", retry=BRAINTRUST_RETRY)
17
+ async def add_feedback(ctx: TaskContext, input: AddFeedbackInput) -> AddFeedbackResult:
18
+ return await add_feedback_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_REVIEW_RETRY
6
+ from .types import AwaitReviewInput, ReviewProgressDTO
7
+
8
+
9
+ async def await_review_impl(
10
+ ctx: TaskContext, input: AwaitReviewInput, *, deps: Deps | None = None
11
+ ) -> ReviewProgressDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.await_review(input)
14
+
15
+
16
+ @app.task(name="braintrust.awaitReview", retry=BRAINTRUST_REVIEW_RETRY)
17
+ async def await_review(ctx: TaskContext, input: AwaitReviewInput) -> ReviewProgressDTO:
18
+ return await await_review_impl(ctx, input)
@@ -0,0 +1,453 @@
1
+ """Injected vendor interface and one-attempt async HTTP adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import AsyncIterator, Mapping, Sequence
8
+ from contextlib import asynccontextmanager
9
+ from dataclasses import dataclass
10
+ from typing import Any, Protocol, cast
11
+
12
+ import httpx
13
+ from render_lab_tasks_core.http import (
14
+ HttpClient,
15
+ enc,
16
+ js_string,
17
+ nullish,
18
+ pick,
19
+ required,
20
+ )
21
+
22
+ from . import types as t
23
+
24
+
25
+ def obj(value: Any) -> dict[str, Any]:
26
+ return value if isinstance(value, dict) else {}
27
+
28
+
29
+ def array(value: Any) -> list[Any]:
30
+ return value if isinstance(value, list) else []
31
+
32
+
33
+ def number(value: Any, default: Any = None) -> Any:
34
+ return value if isinstance(value, (int, float)) and not isinstance(value, bool) else default
35
+
36
+
37
+ def json_text(value: Any) -> str:
38
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
39
+
40
+
41
+ def budget(label: str, value: Any) -> None:
42
+ size = len(json_text(value).encode())
43
+ if size >= 4 * 1024 * 1024:
44
+ raise ValueError(
45
+ f"Braintrust {label} result is {size} bytes, at or above "
46
+ f"the 4194304 byte limit. Lower the limit or narrow the "
47
+ f"request."
48
+ )
49
+
50
+
51
+ def validate_names(input: Mapping[str, Any], task: str, *keys: str) -> None:
52
+ for key in keys:
53
+ if not input[key].strip():
54
+ raise ValueError(f"braintrust.{task} requires a non-blank {key}.")
55
+
56
+
57
+ def validate_rows(rows: Sequence[Mapping[str, Any]], task: str) -> None:
58
+ if not 1 <= len(rows) <= 1000:
59
+ raise ValueError(f"braintrust.{task} accepts 1 to 1000 rows per call.")
60
+ seen: set[str] = set()
61
+ for row in rows:
62
+ id = row["id"]
63
+ if not isinstance(id, str) or not id.strip():
64
+ raise ValueError(f"braintrust.{task} requires a stable non-blank id on every row.")
65
+ if id in seen:
66
+ raise ValueError(f'braintrust.{task} received a duplicate row id "{id}".')
67
+ seen.add(id)
68
+
69
+
70
+ def dataset(value: Any) -> t.DatasetDTO:
71
+ row = obj(value)
72
+ return {
73
+ "id": nullish(row.get("id"), ""),
74
+ "projectId": nullish(row.get("project_id"), ""),
75
+ "name": nullish(row.get("name"), ""),
76
+ "description": row.get("description"),
77
+ }
78
+
79
+
80
+ def dataset_row(value: Any) -> t.DatasetRowDTO:
81
+ row = obj(value)
82
+ result = {
83
+ "id": nullish(row.get("id"), ""),
84
+ "input": row.get("input"),
85
+ **pick(row, "expected", "metadata"),
86
+ }
87
+ if isinstance(row.get("tags"), list):
88
+ result["tags"] = row["tags"]
89
+ return cast(t.DatasetRowDTO, result)
90
+
91
+
92
+ def function_ref(ref: Mapping[str, Any]) -> dict[str, Any]:
93
+ return {"function_id": ref["functionId"], **pick(ref, "version")}
94
+
95
+
96
+ def summary(value: Any) -> t.ExperimentSummaryDTO:
97
+ row = obj(value)
98
+ return {
99
+ "projectName": nullish(row.get("project_name"), ""),
100
+ "experimentName": nullish(row.get("experiment_name"), ""),
101
+ "experimentId": nullish(row.get("experiment_id"), ""),
102
+ "experimentUrl": nullish(row.get("experiment_url"), ""),
103
+ "comparisonExperimentName": row.get("comparison_experiment_name"),
104
+ "scores": {
105
+ k: {
106
+ "score": number(obj(v).get("score")),
107
+ "diff": number(obj(v).get("diff")),
108
+ "improvements": number(obj(v).get("improvements"), 0),
109
+ "regressions": number(obj(v).get("regressions"), 0),
110
+ }
111
+ for k, v in obj(row.get("scores")).items()
112
+ },
113
+ "metrics": {
114
+ k: {
115
+ "metric": number(obj(v).get("metric")),
116
+ "unit": obj(v).get("unit") if isinstance(obj(v).get("unit"), str) else None,
117
+ }
118
+ for k, v in obj(row.get("metrics")).items()
119
+ },
120
+ }
121
+
122
+
123
+ def build_review_query(input: Mapping[str, Any]) -> str:
124
+ key = '"' + input["scoreKey"].replace('"', '""') + '"'
125
+ experiment = "'" + input["experimentId"].replace("'", "''") + "'"
126
+ return (
127
+ f"\nSELECT\n sum(CASE WHEN scores.{key} IS NOT NULL THEN "
128
+ f"1 ELSE 0 END) AS reviewed,\n sum(CASE WHEN is_root "
129
+ f"THEN 1 ELSE 0 END) AS total\nFROM "
130
+ f"experiment({experiment})\n"
131
+ )
132
+
133
+
134
+ class Port(Protocol):
135
+ async def add_feedback(self, input: t.AddFeedbackInput) -> t.AddFeedbackResult: ...
136
+ async def await_review(self, input: t.AwaitReviewInput) -> t.ReviewProgressDTO: ...
137
+ async def delete_dataset_rows(
138
+ self, input: t.DeleteDatasetRowsInput
139
+ ) -> t.DeleteDatasetRowsResult: ...
140
+ async def ensure_dataset(self, input: t.EnsureDatasetInput) -> t.DatasetDTO: ...
141
+ async def ensure_experiment(self, input: t.EnsureExperimentInput) -> t.ExperimentDTO: ...
142
+ async def gate_experiment(self, input: t.GateExperimentInput) -> t.GateExperimentResult: ...
143
+ async def get_review_progress(self, input: t.GetReviewProgressInput) -> t.ReviewProgressDTO: ...
144
+ async def list_dataset_rows(self, input: t.ListDatasetRowsInput) -> t.ListDatasetRowsResult: ...
145
+ async def log_experiment_results(
146
+ self, input: t.LogExperimentResultsInput
147
+ ) -> t.LogExperimentResultsResult: ...
148
+ async def run_eval(self, input: t.RunEvalInput) -> t.ExperimentSummaryDTO: ...
149
+ async def summarize_experiment(
150
+ self, input: t.SummarizeExperimentInput
151
+ ) -> t.ExperimentSummaryDTO: ...
152
+ async def upsert_dataset_rows(
153
+ self, input: t.UpsertDatasetRowsInput
154
+ ) -> t.UpsertDatasetRowsResult: ...
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class Deps:
159
+ braintrust: Port
160
+
161
+
162
+ class Client:
163
+ def __init__(self, http: httpx.AsyncClient, env: Mapping[str, str] | None = None) -> None:
164
+ self.env = os.environ if env is None else env
165
+ self.api = HttpClient(
166
+ http,
167
+ lambda: self.env.get("BRAINTRUST_API_URL", "https://api.braintrust.dev"),
168
+ auth=lambda: {"authorization": "Bearer " + required(self.env, "BRAINTRUST_API_KEY")},
169
+ label="Braintrust API",
170
+ )
171
+
172
+ async def ensure_dataset(self, input: t.EnsureDatasetInput) -> t.DatasetDTO:
173
+ validate_names(input, "ensureDataset", "projectId", "name")
174
+ rows = array(
175
+ obj(
176
+ await self.api.call(
177
+ "/v1/dataset?project_id="
178
+ + enc(input["projectId"])
179
+ + "&dataset_name="
180
+ + enc(input["name"])
181
+ )
182
+ ).get("objects")
183
+ )
184
+ row = (
185
+ rows[0]
186
+ if rows
187
+ else await self.api.call(
188
+ "/v1/dataset",
189
+ method="POST",
190
+ body={"project_id": input["projectId"], **pick(input, "name", "description")},
191
+ )
192
+ )
193
+ return dataset(row)
194
+
195
+ async def ensure_experiment(self, input: t.EnsureExperimentInput) -> t.ExperimentDTO:
196
+ validate_names(input, "ensureExperiment", "projectId", "name")
197
+ rows = array(
198
+ obj(
199
+ await self.api.call(
200
+ "/v1/experiment?project_id="
201
+ + enc(input["projectId"])
202
+ + "&experiment_name="
203
+ + enc(input["name"])
204
+ )
205
+ ).get("objects")
206
+ )
207
+ body = {"project_id": input["projectId"], **pick(input, "name", "description")}
208
+ if "baseExperimentId" in input:
209
+ body["base_exp_id"] = input["baseExperimentId"]
210
+ row = obj(
211
+ rows[0] if rows else await self.api.call("/v1/experiment", method="POST", body=body)
212
+ )
213
+ result: t.ExperimentDTO = {**dataset(row), "baseExperimentId": row.get("base_exp_id")}
214
+ if "baseExperimentId" in input and result["baseExperimentId"] != input["baseExperimentId"]:
215
+ raise ValueError(
216
+ f"braintrust.ensureExperiment: experiment "
217
+ f'"{input["name"]}" already exists with base '
218
+ f"{json_text(result['baseExperimentId'])}, which "
219
+ f"conflicts with the requested base "
220
+ f"{json_text(input['baseExperimentId'])}."
221
+ )
222
+ return result
223
+
224
+ async def upsert_dataset_rows(
225
+ self, input: t.UpsertDatasetRowsInput
226
+ ) -> t.UpsertDatasetRowsResult:
227
+ validate_rows(input["rows"], "upsertDatasetRows")
228
+ events = [pick(row, "id", "input", "expected", "metadata", "tags") for row in input["rows"]]
229
+ data = obj(
230
+ await self.api.call(
231
+ "/v1/dataset/" + enc(input["datasetId"]) + "/insert",
232
+ method="POST",
233
+ body={"events": events},
234
+ )
235
+ )
236
+ return {
237
+ "rowIds": data["row_ids"]
238
+ if isinstance(data.get("row_ids"), list)
239
+ else [r["id"] for r in input["rows"]]
240
+ }
241
+
242
+ async def list_dataset_rows(self, input: t.ListDatasetRowsInput) -> t.ListDatasetRowsResult:
243
+ if "limit" in input and not 1 <= input["limit"] <= 1000:
244
+ raise ValueError("braintrust.listDatasetRows limit must be between 1 and 1000.")
245
+ data = obj(
246
+ await self.api.call(
247
+ "/v1/dataset/" + enc(input["datasetId"]) + "/fetch",
248
+ method="POST",
249
+ body=pick(input, "limit", "cursor"),
250
+ )
251
+ )
252
+ result: t.ListDatasetRowsResult = {
253
+ "rows": [dataset_row(r) for r in array(data.get("events"))],
254
+ "nextCursor": data.get("cursor"),
255
+ }
256
+ budget("listDatasetRows", result)
257
+ return result
258
+
259
+ async def delete_dataset_rows(
260
+ self, input: t.DeleteDatasetRowsInput
261
+ ) -> t.DeleteDatasetRowsResult:
262
+ ids = input["rowIds"]
263
+ if not ids:
264
+ return {"deletedRowIds": []}
265
+ if len(ids) > 1000:
266
+ raise ValueError("braintrust.deleteDatasetRows accepts at most 1000 ids per call.")
267
+ if any(not isinstance(id, str) or not id.strip() for id in ids):
268
+ raise ValueError("braintrust.deleteDatasetRows requires non-blank row ids.")
269
+ await self.api.call(
270
+ "/v1/dataset/" + enc(input["datasetId"]) + "/insert",
271
+ method="POST",
272
+ body={"events": [{"id": id, "_object_delete": True} for id in ids]},
273
+ )
274
+ return {"deletedRowIds": ids}
275
+
276
+ async def run_eval(self, input: t.RunEvalInput) -> t.ExperimentSummaryDTO:
277
+ if not input["scores"]:
278
+ raise ValueError("braintrust.runEval requires at least one scoring function reference.")
279
+ body: dict[str, Any] = {
280
+ "project_id": input["projectId"],
281
+ "data": {"dataset_id": input["datasetId"]},
282
+ "task": function_ref(input["task"]),
283
+ "scores": [function_ref(r) for r in input["scores"]],
284
+ "stream": False,
285
+ **pick(input, "metadata"),
286
+ }
287
+ if "experimentName" in input:
288
+ body["experiment_name"] = input["experimentName"]
289
+ if "repoCommit" in input:
290
+ body["repo_info"] = {"commit": input["repoCommit"]}
291
+ data = obj(await self.api.call("/v1/eval", method="POST", body=body))
292
+ result = summary(nullish(data.get("summary"), data))
293
+ budget("runEval", result)
294
+ return result
295
+
296
+ async def log_experiment_results(
297
+ self, input: t.LogExperimentResultsInput
298
+ ) -> t.LogExperimentResultsResult:
299
+ validate_rows(input["rows"], "logExperimentResults")
300
+ for row in input["rows"]:
301
+ for key, value in row.get("scores", {}).items():
302
+ if value < 0 or value > 1:
303
+ raise ValueError(
304
+ f'braintrust.logExperimentResults score "{key}" on row '
305
+ f'"{row["id"]}" is {js_string(value)}; scores must be '
306
+ f"within [0, 1]."
307
+ )
308
+ data = obj(
309
+ await self.api.call(
310
+ "/v1/experiment/" + enc(input["experimentId"]) + "/insert",
311
+ method="POST",
312
+ body={
313
+ "events": [
314
+ pick(
315
+ r,
316
+ "id",
317
+ "input",
318
+ "output",
319
+ "expected",
320
+ "scores",
321
+ "metrics",
322
+ "metadata",
323
+ "tags",
324
+ )
325
+ for r in input["rows"]
326
+ ]
327
+ },
328
+ )
329
+ )
330
+ return {
331
+ "rowIds": data["row_ids"]
332
+ if isinstance(data.get("row_ids"), list)
333
+ else [r["id"] for r in input["rows"]]
334
+ }
335
+
336
+ async def _summary(self, input: Mapping[str, Any]) -> t.ExperimentSummaryDTO:
337
+ path = "/v1/experiment/" + enc(input["experimentId"]) + "/summarize?summarize_scores=true"
338
+ if "comparisonExperimentId" in input:
339
+ path += "&comparison_experiment_id=" + enc(input["comparisonExperimentId"])
340
+ result = summary(await self.api.call(path))
341
+ budget("summarizeExperiment", result)
342
+ return result
343
+
344
+ async def summarize_experiment(
345
+ self, input: t.SummarizeExperimentInput
346
+ ) -> t.ExperimentSummaryDTO:
347
+ validate_names(input, "summarizeExperiment", "experimentId")
348
+ return await self._summary(input)
349
+
350
+ async def add_feedback(self, input: t.AddFeedbackInput) -> t.AddFeedbackResult:
351
+ if not input.get("scores") and not input.get("comment"):
352
+ raise ValueError("braintrust.addFeedback requires at least one score or a comment.")
353
+ await self.api.call(
354
+ "/v1/experiment/" + enc(input["experimentId"]) + "/feedback",
355
+ method="POST",
356
+ body={
357
+ "feedback": [{"id": input["rowId"], **pick(input, "scores", "comment", "source")}]
358
+ },
359
+ )
360
+ return {"experimentId": input["experimentId"], "rowId": input["rowId"], "applied": True}
361
+
362
+ async def _review(self, input: Mapping[str, Any]) -> t.ReviewProgressDTO:
363
+ rows = obj(
364
+ await self.api.call(
365
+ "/btql", method="POST", body={"query": build_review_query(input), "fmt": "json"}
366
+ )
367
+ ).get("data")
368
+ row = obj(rows[0]) if isinstance(rows, list) and rows else {}
369
+ reviewed = number(row.get("reviewed"), 0)
370
+ total = number(row.get("total"), 0)
371
+ return {
372
+ "experimentId": input["experimentId"],
373
+ "scoreKey": input["scoreKey"],
374
+ "reviewed": reviewed,
375
+ "total": total,
376
+ "pending": max(0, total - reviewed),
377
+ "complete": total > 0 and reviewed >= total,
378
+ }
379
+
380
+ async def get_review_progress(self, input: t.GetReviewProgressInput) -> t.ReviewProgressDTO:
381
+ validate_names(input, "getReviewProgress", "experimentId", "scoreKey")
382
+ return await self._review(input)
383
+
384
+ async def await_review(self, input: t.AwaitReviewInput) -> t.ReviewProgressDTO:
385
+ result = await self._review(input)
386
+ if result["complete"] and (
387
+ "minimumReviewed" not in input or result["reviewed"] >= input["minimumReviewed"]
388
+ ):
389
+ return result
390
+ raise RuntimeError(
391
+ f"Braintrust review of experiment "
392
+ f'"{input["experimentId"]}" for score '
393
+ f'"{input["scoreKey"]}" is still pending.'
394
+ )
395
+
396
+ async def gate_experiment(self, input: t.GateExperimentInput) -> t.GateExperimentResult:
397
+ thresholds = input["thresholds"]
398
+ if "minimumReviewed" in thresholds and "reviewScoreKey" not in input:
399
+ raise ValueError(
400
+ "braintrust.gateExperiment requires reviewScoreKey when "
401
+ "thresholds.minimumReviewed is set."
402
+ )
403
+ result = await self._summary(input)
404
+ violations: list[t.GateViolation] = []
405
+ for key, minimum in thresholds.get("minimumScores", {}).items():
406
+ actual = obj(result["scores"].get(key)).get("score")
407
+ if actual is None or actual < minimum:
408
+ violations.append(
409
+ {"metric": key, "actual": actual, "operator": ">=", "threshold": minimum}
410
+ )
411
+ if "maximumErrorRate" in thresholds:
412
+ actual = obj(result["metrics"].get("error_rate")).get("metric")
413
+ if actual is None or actual > thresholds["maximumErrorRate"]:
414
+ violations.append(
415
+ {
416
+ "metric": "error_rate",
417
+ "actual": actual,
418
+ "operator": "<=",
419
+ "threshold": thresholds["maximumErrorRate"],
420
+ }
421
+ )
422
+ if "minimumReviewed" in thresholds:
423
+ progress = await self._review(
424
+ {"experimentId": input["experimentId"], "scoreKey": input["reviewScoreKey"]}
425
+ )
426
+ if progress["reviewed"] < thresholds["minimumReviewed"]:
427
+ violations.append(
428
+ {
429
+ "metric": "reviewed",
430
+ "actual": progress["reviewed"],
431
+ "operator": ">=",
432
+ "threshold": thresholds["minimumReviewed"],
433
+ }
434
+ )
435
+ if violations:
436
+ raise ValueError("Release gate failed: " + json_text(violations))
437
+ return {"passed": True, "summary": result}
438
+
439
+
440
+ @asynccontextmanager
441
+ async def dependencies(deps: Deps | None) -> AsyncIterator[Deps]:
442
+ if deps is not None:
443
+ yield deps
444
+ else:
445
+ async with httpx.AsyncClient(
446
+ timeout=30, transport=httpx.AsyncHTTPTransport(retries=0)
447
+ ) as http:
448
+ yield Deps(braintrust=Client(http))
449
+
450
+
451
+ BraintrustPort = Port
452
+ BraintrustClient = Client
453
+ BraintrustDeps = Deps
@@ -0,0 +1,20 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import DeleteDatasetRowsInput, DeleteDatasetRowsResult
7
+
8
+
9
+ async def delete_dataset_rows_impl(
10
+ ctx: TaskContext, input: DeleteDatasetRowsInput, *, deps: Deps | None = None
11
+ ) -> DeleteDatasetRowsResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.delete_dataset_rows(input)
14
+
15
+
16
+ @app.task(name="braintrust.deleteDatasetRows", retry=BRAINTRUST_RETRY)
17
+ async def delete_dataset_rows(
18
+ ctx: TaskContext, input: DeleteDatasetRowsInput
19
+ ) -> DeleteDatasetRowsResult:
20
+ return await delete_dataset_rows_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import DatasetDTO, EnsureDatasetInput
7
+
8
+
9
+ async def ensure_dataset_impl(
10
+ ctx: TaskContext, input: EnsureDatasetInput, *, deps: Deps | None = None
11
+ ) -> DatasetDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.ensure_dataset(input)
14
+
15
+
16
+ @app.task(name="braintrust.ensureDataset", retry=BRAINTRUST_RETRY)
17
+ async def ensure_dataset(ctx: TaskContext, input: EnsureDatasetInput) -> DatasetDTO:
18
+ return await ensure_dataset_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import EnsureExperimentInput, ExperimentDTO
7
+
8
+
9
+ async def ensure_experiment_impl(
10
+ ctx: TaskContext, input: EnsureExperimentInput, *, deps: Deps | None = None
11
+ ) -> ExperimentDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.ensure_experiment(input)
14
+
15
+
16
+ @app.task(name="braintrust.ensureExperiment", retry=BRAINTRUST_RETRY)
17
+ async def ensure_experiment(ctx: TaskContext, input: EnsureExperimentInput) -> ExperimentDTO:
18
+ return await ensure_experiment_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_NO_RETRY
6
+ from .types import GateExperimentInput, GateExperimentResult
7
+
8
+
9
+ async def gate_experiment_impl(
10
+ ctx: TaskContext, input: GateExperimentInput, *, deps: Deps | None = None
11
+ ) -> GateExperimentResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.gate_experiment(input)
14
+
15
+
16
+ @app.task(name="braintrust.gateExperiment", retry=BRAINTRUST_NO_RETRY)
17
+ async def gate_experiment(ctx: TaskContext, input: GateExperimentInput) -> GateExperimentResult:
18
+ return await gate_experiment_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import GetReviewProgressInput, ReviewProgressDTO
7
+
8
+
9
+ async def get_review_progress_impl(
10
+ ctx: TaskContext, input: GetReviewProgressInput, *, deps: Deps | None = None
11
+ ) -> ReviewProgressDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.get_review_progress(input)
14
+
15
+
16
+ @app.task(name="braintrust.getReviewProgress", retry=BRAINTRUST_RETRY)
17
+ async def get_review_progress(ctx: TaskContext, input: GetReviewProgressInput) -> ReviewProgressDTO:
18
+ return await get_review_progress_impl(ctx, input)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import ListDatasetRowsInput, ListDatasetRowsResult
7
+
8
+
9
+ async def list_dataset_rows_impl(
10
+ ctx: TaskContext, input: ListDatasetRowsInput, *, deps: Deps | None = None
11
+ ) -> ListDatasetRowsResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.list_dataset_rows(input)
14
+
15
+
16
+ @app.task(name="braintrust.listDatasetRows", retry=BRAINTRUST_RETRY)
17
+ async def list_dataset_rows(ctx: TaskContext, input: ListDatasetRowsInput) -> ListDatasetRowsResult:
18
+ return await list_dataset_rows_impl(ctx, input)
@@ -0,0 +1,20 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import LogExperimentResultsInput, LogExperimentResultsResult
7
+
8
+
9
+ async def log_experiment_results_impl(
10
+ ctx: TaskContext, input: LogExperimentResultsInput, *, deps: Deps | None = None
11
+ ) -> LogExperimentResultsResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.log_experiment_results(input)
14
+
15
+
16
+ @app.task(name="braintrust.logExperimentResults", retry=BRAINTRUST_RETRY)
17
+ async def log_experiment_results(
18
+ ctx: TaskContext, input: LogExperimentResultsInput
19
+ ) -> LogExperimentResultsResult:
20
+ return await log_experiment_results_impl(ctx, input)
@@ -0,0 +1,5 @@
1
+ from render import Retry
2
+
3
+ BRAINTRUST_RETRY = Retry(max_retries=3, wait_duration_ms=1000, backoff_scaling=2)
4
+ BRAINTRUST_REVIEW_RETRY = Retry(max_retries=20160, wait_duration_ms=30000, backoff_scaling=1)
5
+ BRAINTRUST_NO_RETRY = Retry(max_retries=0, wait_duration_ms=0, backoff_scaling=1)
@@ -0,0 +1,18 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_NO_RETRY
6
+ from .types import ExperimentSummaryDTO, RunEvalInput
7
+
8
+
9
+ async def run_eval_impl(
10
+ ctx: TaskContext, input: RunEvalInput, *, deps: Deps | None = None
11
+ ) -> ExperimentSummaryDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.run_eval(input)
14
+
15
+
16
+ @app.task(name="braintrust.runEval", retry=BRAINTRUST_NO_RETRY)
17
+ async def run_eval(ctx: TaskContext, input: RunEvalInput) -> ExperimentSummaryDTO:
18
+ return await run_eval_impl(ctx, input)
@@ -0,0 +1,20 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import ExperimentSummaryDTO, SummarizeExperimentInput
7
+
8
+
9
+ async def summarize_experiment_impl(
10
+ ctx: TaskContext, input: SummarizeExperimentInput, *, deps: Deps | None = None
11
+ ) -> ExperimentSummaryDTO:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.summarize_experiment(input)
14
+
15
+
16
+ @app.task(name="braintrust.summarizeExperiment", retry=BRAINTRUST_RETRY)
17
+ async def summarize_experiment(
18
+ ctx: TaskContext, input: SummarizeExperimentInput
19
+ ) -> ExperimentSummaryDTO:
20
+ return await summarize_experiment_impl(ctx, input)
@@ -0,0 +1,41 @@
1
+ from ._app import app
2
+ from .add_feedback import add_feedback, add_feedback_impl
3
+ from .await_review import await_review, await_review_impl
4
+ from .delete_dataset_rows import delete_dataset_rows, delete_dataset_rows_impl
5
+ from .ensure_dataset import ensure_dataset, ensure_dataset_impl
6
+ from .ensure_experiment import ensure_experiment, ensure_experiment_impl
7
+ from .gate_experiment import gate_experiment, gate_experiment_impl
8
+ from .get_review_progress import get_review_progress, get_review_progress_impl
9
+ from .list_dataset_rows import list_dataset_rows, list_dataset_rows_impl
10
+ from .log_experiment_results import log_experiment_results, log_experiment_results_impl
11
+ from .run_eval import run_eval, run_eval_impl
12
+ from .summarize_experiment import summarize_experiment, summarize_experiment_impl
13
+ from .upsert_dataset_rows import upsert_dataset_rows, upsert_dataset_rows_impl
14
+
15
+ __all__ = [
16
+ "app",
17
+ "add_feedback",
18
+ "add_feedback_impl",
19
+ "await_review",
20
+ "await_review_impl",
21
+ "delete_dataset_rows",
22
+ "delete_dataset_rows_impl",
23
+ "ensure_dataset",
24
+ "ensure_dataset_impl",
25
+ "ensure_experiment",
26
+ "ensure_experiment_impl",
27
+ "gate_experiment",
28
+ "gate_experiment_impl",
29
+ "get_review_progress",
30
+ "get_review_progress_impl",
31
+ "list_dataset_rows",
32
+ "list_dataset_rows_impl",
33
+ "log_experiment_results",
34
+ "log_experiment_results_impl",
35
+ "run_eval",
36
+ "run_eval_impl",
37
+ "summarize_experiment",
38
+ "summarize_experiment_impl",
39
+ "upsert_dataset_rows",
40
+ "upsert_dataset_rows_impl",
41
+ ]
@@ -0,0 +1,193 @@
1
+ """JSON contracts ported from the pinned TypeScript pack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, NotRequired, TypedDict
6
+
7
+ type JsonPrimitive = str | float | bool | None
8
+
9
+ type JsonValue = JsonPrimitive | list[JsonValue] | dict[str, JsonValue]
10
+
11
+ type JsonObject = dict[str, JsonValue]
12
+
13
+
14
+ class GateThresholds(TypedDict):
15
+ minimumScores: NotRequired[dict[str, float]]
16
+ maximumErrorRate: NotRequired[float]
17
+ minimumReviewed: NotRequired[float]
18
+
19
+
20
+ class GateViolation(TypedDict):
21
+ metric: str
22
+ actual: float | None
23
+ operator: Literal[">="] | Literal["<="]
24
+ threshold: float
25
+
26
+
27
+ class DatasetDTO(TypedDict):
28
+ id: str
29
+ projectId: str
30
+ name: str
31
+ description: str | None
32
+
33
+
34
+ class EnsureDatasetInput(TypedDict):
35
+ projectId: str
36
+ name: str
37
+ description: NotRequired[str]
38
+
39
+
40
+ class DatasetRowDTO(TypedDict):
41
+ id: str
42
+ input: JsonValue
43
+ expected: NotRequired[JsonValue]
44
+ metadata: NotRequired[JsonObject]
45
+ tags: NotRequired[list[str]]
46
+
47
+
48
+ class UpsertDatasetRowsInput(TypedDict):
49
+ datasetId: str
50
+ rows: list[DatasetRowDTO]
51
+
52
+
53
+ class UpsertDatasetRowsResult(TypedDict):
54
+ rowIds: list[str]
55
+
56
+
57
+ class ListDatasetRowsInput(TypedDict):
58
+ datasetId: str
59
+ limit: NotRequired[float]
60
+ cursor: NotRequired[str]
61
+
62
+
63
+ class ListDatasetRowsResult(TypedDict):
64
+ rows: list[DatasetRowDTO]
65
+ nextCursor: str | None
66
+
67
+
68
+ class DeleteDatasetRowsInput(TypedDict):
69
+ datasetId: str
70
+ rowIds: list[str]
71
+
72
+
73
+ class DeleteDatasetRowsResult(TypedDict):
74
+ deletedRowIds: list[str]
75
+
76
+
77
+ class SavedFunctionRef(TypedDict):
78
+ functionId: str
79
+ version: NotRequired[str]
80
+
81
+
82
+ class RunEvalInput(TypedDict):
83
+ projectId: str
84
+ datasetId: str
85
+ task: SavedFunctionRef
86
+ scores: list[SavedFunctionRef]
87
+ experimentName: NotRequired[str]
88
+ metadata: NotRequired[JsonObject]
89
+ repoCommit: NotRequired[str]
90
+
91
+
92
+ class ScoreSummaryDTO(TypedDict):
93
+ score: float | None
94
+ diff: float | None
95
+ improvements: float
96
+ regressions: float
97
+
98
+
99
+ class MetricSummaryDTO(TypedDict):
100
+ metric: float | None
101
+ unit: str | None
102
+
103
+
104
+ class ExperimentSummaryDTO(TypedDict):
105
+ projectName: str
106
+ experimentName: str
107
+ experimentId: str
108
+ experimentUrl: str
109
+ comparisonExperimentName: str | None
110
+ scores: dict[str, ScoreSummaryDTO]
111
+ metrics: dict[str, MetricSummaryDTO]
112
+
113
+
114
+ class ExperimentDTO(TypedDict):
115
+ id: str
116
+ projectId: str
117
+ name: str
118
+ description: str | None
119
+ baseExperimentId: str | None
120
+
121
+
122
+ class EnsureExperimentInput(TypedDict):
123
+ projectId: str
124
+ name: str
125
+ description: NotRequired[str]
126
+ baseExperimentId: NotRequired[str]
127
+
128
+
129
+ class ExperimentResultRowDTO(TypedDict):
130
+ id: str
131
+ input: JsonValue
132
+ output: JsonValue
133
+ expected: NotRequired[JsonValue]
134
+ scores: NotRequired[dict[str, float]]
135
+ metrics: NotRequired[dict[str, float]]
136
+ metadata: NotRequired[JsonObject]
137
+ tags: NotRequired[list[str]]
138
+
139
+
140
+ class LogExperimentResultsInput(TypedDict):
141
+ experimentId: str
142
+ rows: list[ExperimentResultRowDTO]
143
+
144
+
145
+ class LogExperimentResultsResult(TypedDict):
146
+ rowIds: list[str]
147
+
148
+
149
+ class SummarizeExperimentInput(TypedDict):
150
+ experimentId: str
151
+ comparisonExperimentId: NotRequired[str]
152
+
153
+
154
+ class GateExperimentInput(SummarizeExperimentInput):
155
+ thresholds: GateThresholds
156
+ reviewScoreKey: NotRequired[str]
157
+
158
+
159
+ class GateExperimentResult(TypedDict):
160
+ passed: Literal[True]
161
+ summary: ExperimentSummaryDTO
162
+
163
+
164
+ class AddFeedbackInput(TypedDict):
165
+ experimentId: str
166
+ rowId: str
167
+ scores: NotRequired[dict[str, float | None]]
168
+ comment: NotRequired[str]
169
+ source: NotRequired[Literal["app"] | Literal["api"]]
170
+
171
+
172
+ class AddFeedbackResult(TypedDict):
173
+ experimentId: str
174
+ rowId: str
175
+ applied: Literal[True]
176
+
177
+
178
+ class GetReviewProgressInput(TypedDict):
179
+ experimentId: str
180
+ scoreKey: str
181
+
182
+
183
+ class ReviewProgressDTO(TypedDict):
184
+ experimentId: str
185
+ scoreKey: str
186
+ reviewed: float
187
+ total: float
188
+ pending: float
189
+ complete: bool
190
+
191
+
192
+ class AwaitReviewInput(GetReviewProgressInput):
193
+ minimumReviewed: NotRequired[float]
@@ -0,0 +1,20 @@
1
+ from render import TaskContext
2
+
3
+ from ._app import app
4
+ from .client import Deps, dependencies
5
+ from .retry import BRAINTRUST_RETRY
6
+ from .types import UpsertDatasetRowsInput, UpsertDatasetRowsResult
7
+
8
+
9
+ async def upsert_dataset_rows_impl(
10
+ ctx: TaskContext, input: UpsertDatasetRowsInput, *, deps: Deps | None = None
11
+ ) -> UpsertDatasetRowsResult:
12
+ async with dependencies(deps) as resolved:
13
+ return await resolved.braintrust.upsert_dataset_rows(input)
14
+
15
+
16
+ @app.task(name="braintrust.upsertDatasetRows", retry=BRAINTRUST_RETRY)
17
+ async def upsert_dataset_rows(
18
+ ctx: TaskContext, input: UpsertDatasetRowsInput
19
+ ) -> UpsertDatasetRowsResult:
20
+ return await upsert_dataset_rows_impl(ctx, input)