Skip to content

Commit 6aa83e2

Browse files
sid-rlstainless-app[bot]
authored andcommitted
feat(sdk): add BenchmarkRun and AsyncBenchmarkRun classes (#712)
* update requirements-dev * pyproject formatting nit * feat(sdk): add BenchmarkRun and AsyncBenchmarkRun classes * fixed smoketests * `list_scenario_runs()` now returns a list of ScenarioRun/AsyncScenarioRun objects
1 parent cfd04b6 commit 6aa83e2

10 files changed

Lines changed: 815 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies = [
1515
"anyio>=3.5.0, <5",
1616
"distro>=1.7.0, <2",
1717
"sniffio",
18-
"uuid-utils>=0.11.0",
18+
"uuid-utils>=0.11.0",
1919
]
2020

2121
requires-python = ">= 3.9"

requirements-dev.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ python-dateutil==2.9.0.post0 ; python_full_version < '3.10'
9494
# via time-machine
9595
respx==0.22.0
9696
rich==14.2.0
97-
ruff==0.14.8
97+
ruff==0.14.9
9898
six==1.17.0 ; python_full_version < '3.10'
9999
# via python-dateutil
100100
sniffio==1.3.1

src/runloop_api_client/sdk/_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ..lib.polling import PollingConfig
66
from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams
77
from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams, ScorerValidateParams
8+
from ..types.benchmarks import RunListScenarioRunsParams
89
from ..types.input_context import InputContext
910
from ..types.scenario_view import ScenarioView
1011
from ..types.agent_list_params import AgentListParams
@@ -203,3 +204,8 @@ class ScenarioPreview(ScenarioView):
203204

204205
input_context: InputContextPreview # type: ignore[assignment]
205206
"""The input context for the Scenario."""
207+
208+
209+
# Benchmark Run params
210+
class SDKBenchmarkRunListScenarioRunsParams(RunListScenarioRunsParams, BaseRequestOptions):
211+
pass
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""AsyncBenchmarkRun resource class for asynchronous operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing import List
6+
from typing_extensions import Unpack, override
7+
8+
from ..types import BenchmarkRunView
9+
from ._types import BaseRequestOptions, LongRequestOptions, SDKBenchmarkRunListScenarioRunsParams
10+
from .._client import AsyncRunloop
11+
from .async_scenario_run import AsyncScenarioRun
12+
13+
14+
class AsyncBenchmarkRun:
15+
"""A benchmark run for evaluating agent performance across scenarios (async).
16+
17+
Provides async methods for monitoring run status, managing the run lifecycle,
18+
and accessing scenario run results. Obtain instances via
19+
``benchmark.run()`` or ``benchmark.list_runs()``.
20+
21+
Example:
22+
>>> benchmark = runloop.benchmark.from_id("bench-xxx")
23+
>>> run = await benchmark.run(run_name="evaluation-v1")
24+
>>> info = await run.get_info()
25+
>>> scenario_runs = await run.list_scenario_runs()
26+
"""
27+
28+
def __init__(self, client: AsyncRunloop, run_id: str, benchmark_id: str) -> None:
29+
"""Create an AsyncBenchmarkRun instance.
30+
31+
:param client: AsyncRunloop client instance
32+
:type client: AsyncRunloop
33+
:param run_id: Benchmark run ID
34+
:type run_id: str
35+
:param benchmark_id: Parent benchmark ID
36+
:type benchmark_id: str
37+
"""
38+
self._client = client
39+
self._id = run_id
40+
self._benchmark_id = benchmark_id
41+
42+
@override
43+
def __repr__(self) -> str:
44+
return f"<AsyncBenchmarkRun id={self._id!r}>"
45+
46+
@property
47+
def id(self) -> str:
48+
"""Return the benchmark run ID.
49+
50+
:return: Unique benchmark run ID
51+
:rtype: str
52+
"""
53+
return self._id
54+
55+
@property
56+
def benchmark_id(self) -> str:
57+
"""Return the parent benchmark ID.
58+
59+
:return: Parent benchmark ID
60+
:rtype: str
61+
"""
62+
return self._benchmark_id
63+
64+
async def get_info(
65+
self,
66+
**options: Unpack[BaseRequestOptions],
67+
) -> BenchmarkRunView:
68+
"""Retrieve current benchmark run status and metadata.
69+
70+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
71+
:return: Current benchmark run state info
72+
:rtype: BenchmarkRunView
73+
"""
74+
return await self._client.benchmarks.runs.retrieve(
75+
self._id,
76+
**options,
77+
)
78+
79+
async def cancel(
80+
self,
81+
**options: Unpack[LongRequestOptions],
82+
) -> BenchmarkRunView:
83+
"""Cancel the benchmark run.
84+
85+
Stops all running scenarios and marks the run as canceled.
86+
87+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
88+
:return: Updated benchmark run state
89+
:rtype: BenchmarkRunView
90+
"""
91+
return await self._client.benchmarks.runs.cancel(
92+
self._id,
93+
**options,
94+
)
95+
96+
async def complete(
97+
self,
98+
**options: Unpack[LongRequestOptions],
99+
) -> BenchmarkRunView:
100+
"""Complete the benchmark run.
101+
102+
Marks the run as completed. Call this after all scenarios have finished.
103+
104+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
105+
:return: Completed benchmark run state
106+
:rtype: BenchmarkRunView
107+
"""
108+
return await self._client.benchmarks.runs.complete(
109+
self._id,
110+
**options,
111+
)
112+
113+
async def list_scenario_runs(
114+
self,
115+
**params: Unpack[SDKBenchmarkRunListScenarioRunsParams],
116+
) -> List[AsyncScenarioRun]:
117+
"""List all scenario runs for this benchmark run.
118+
119+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkRunListScenarioRunsParams` for available parameters
120+
:return: List of async scenario run objects
121+
:rtype: List[AsyncScenarioRun]
122+
"""
123+
page = await self._client.benchmarks.runs.list_scenario_runs(
124+
self._id,
125+
**params,
126+
)
127+
return [AsyncScenarioRun(self._client, run.id, run.devbox_id) for run in page.runs]
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""BenchmarkRun resource class for synchronous operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing import List
6+
from typing_extensions import Unpack, override
7+
8+
from ..types import BenchmarkRunView
9+
from ._types import BaseRequestOptions, LongRequestOptions, SDKBenchmarkRunListScenarioRunsParams
10+
from .._client import Runloop
11+
from .scenario_run import ScenarioRun
12+
13+
14+
class BenchmarkRun:
15+
"""A benchmark run for evaluating agent performance across scenarios.
16+
17+
Provides methods for monitoring run status, managing the run lifecycle,
18+
and accessing scenario run results. Obtain instances via
19+
``benchmark.run()`` or ``benchmark.list_runs()``.
20+
21+
Example:
22+
>>> benchmark = runloop.benchmark.from_id("bench-xxx")
23+
>>> run = benchmark.run(run_name="evaluation-v1")
24+
>>> info = run.get_info()
25+
>>> scenario_runs = run.list_scenario_runs()
26+
"""
27+
28+
def __init__(self, client: Runloop, run_id: str, benchmark_id: str) -> None:
29+
"""Create a BenchmarkRun instance.
30+
31+
:param client: Runloop client instance
32+
:type client: Runloop
33+
:param run_id: Benchmark run ID
34+
:type run_id: str
35+
:param benchmark_id: Parent benchmark ID
36+
:type benchmark_id: str
37+
"""
38+
self._client = client
39+
self._id = run_id
40+
self._benchmark_id = benchmark_id
41+
42+
@override
43+
def __repr__(self) -> str:
44+
return f"<BenchmarkRun id={self._id!r}>"
45+
46+
@property
47+
def id(self) -> str:
48+
"""Return the benchmark run ID.
49+
50+
:return: Unique benchmark run ID
51+
:rtype: str
52+
"""
53+
return self._id
54+
55+
@property
56+
def benchmark_id(self) -> str:
57+
"""Return the parent benchmark ID.
58+
59+
:return: Parent benchmark ID
60+
:rtype: str
61+
"""
62+
return self._benchmark_id
63+
64+
def get_info(
65+
self,
66+
**options: Unpack[BaseRequestOptions],
67+
) -> BenchmarkRunView:
68+
"""Retrieve current benchmark run status and metadata.
69+
70+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
71+
:return: Current benchmark run state info
72+
:rtype: BenchmarkRunView
73+
"""
74+
return self._client.benchmarks.runs.retrieve(
75+
self._id,
76+
**options,
77+
)
78+
79+
def cancel(
80+
self,
81+
**options: Unpack[LongRequestOptions],
82+
) -> BenchmarkRunView:
83+
"""Cancel the benchmark run.
84+
85+
Stops all running scenarios and marks the run as canceled.
86+
87+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
88+
:return: Updated benchmark run state
89+
:rtype: BenchmarkRunView
90+
"""
91+
return self._client.benchmarks.runs.cancel(
92+
self._id,
93+
**options,
94+
)
95+
96+
def complete(
97+
self,
98+
**options: Unpack[LongRequestOptions],
99+
) -> BenchmarkRunView:
100+
"""Complete the benchmark run.
101+
102+
Marks the run as completed. Call this after all scenarios have finished.
103+
104+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
105+
:return: Completed benchmark run state
106+
:rtype: BenchmarkRunView
107+
"""
108+
return self._client.benchmarks.runs.complete(
109+
self._id,
110+
**options,
111+
)
112+
113+
def list_scenario_runs(
114+
self,
115+
**params: Unpack[SDKBenchmarkRunListScenarioRunsParams],
116+
) -> List[ScenarioRun]:
117+
"""List all scenario runs for this benchmark run.
118+
119+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkRunListScenarioRunsParams` for available parameters
120+
:return: List of scenario run objects
121+
:rtype: List[ScenarioRun]
122+
"""
123+
page = self._client.benchmarks.runs.list_scenario_runs(
124+
self._id,
125+
**params,
126+
)
127+
return [ScenarioRun(self._client, run.id, run.devbox_id) for run in page.runs]

tests/sdk/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,30 @@ class MockScenarioRunView:
129129
scoring_contract_result: object = None
130130

131131

132+
@dataclass
133+
class MockBenchmarkRunView:
134+
"""Mock BenchmarkRunView for testing."""
135+
136+
id: str = "bench_run_123"
137+
benchmark_id: str = "bench_123"
138+
state: str = "running"
139+
metadata: Dict[str, str] = field(default_factory=dict)
140+
start_time_ms: int = 1234567890000
141+
duration_ms: int | None = None
142+
score: float | None = None
143+
144+
145+
class AsyncIterableMock:
146+
"""A simple async iterable mock for testing paginated responses."""
147+
148+
def __init__(self, items: list[Any]) -> None:
149+
self._items = items
150+
151+
async def __aiter__(self):
152+
for item in self._items:
153+
yield item
154+
155+
132156
def create_mock_httpx_client(methods: dict[str, Any] | None = None) -> AsyncMock:
133157
"""
134158
Create a mock httpx.AsyncClient with proper context manager setup.
@@ -237,6 +261,12 @@ def scenario_run_view() -> MockScenarioRunView:
237261
return MockScenarioRunView()
238262

239263

264+
@pytest.fixture
265+
def benchmark_run_view() -> MockBenchmarkRunView:
266+
"""Create a mock BenchmarkRunView."""
267+
return MockBenchmarkRunView()
268+
269+
240270
@pytest.fixture
241271
def mock_httpx_response() -> Mock:
242272
"""Create a mock httpx.Response."""

0 commit comments

Comments
 (0)