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.
- render_lab_tasks_braintrust-0.1.0/.gitignore +11 -0
- render_lab_tasks_braintrust-0.1.0/LICENSE +21 -0
- render_lab_tasks_braintrust-0.1.0/PKG-INFO +84 -0
- render_lab_tasks_braintrust-0.1.0/README.md +71 -0
- render_lab_tasks_braintrust-0.1.0/pyproject.toml +22 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/__init__.py +1 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/_app.py +3 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/add_feedback.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/await_review.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/client.py +453 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/delete_dataset_rows.py +20 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/ensure_dataset.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/ensure_experiment.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/gate_experiment.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/get_review_progress.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/list_dataset_rows.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/log_experiment_results.py +20 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/py.typed +0 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/retry.py +5 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/run_eval.py +18 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/summarize_experiment.py +20 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/tasks.py +41 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/types.py +193 -0
- render_lab_tasks_braintrust-0.1.0/src/render_lab_tasks_braintrust/upsert_dataset_rows.py +20 -0
|
@@ -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,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)
|
|
File without changes
|
|
@@ -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)
|