Skip to content

Commit 9b434d9

Browse files
authored
feat(sdk): add BenchmarkOps and AsyncBenchmarkOps to SDK (#716)
1 parent 8909d8a commit 9b434d9

8 files changed

Lines changed: 271 additions & 27 deletions

File tree

src/runloop_api_client/sdk/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55

66
from __future__ import annotations
77

8-
from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, ScenarioOps, SnapshotOps, BlueprintOps, StorageObjectOps
8+
from .sync import (
9+
AgentOps,
10+
DevboxOps,
11+
ScorerOps,
12+
RunloopSDK,
13+
ScenarioOps,
14+
SnapshotOps,
15+
BenchmarkOps,
16+
BlueprintOps,
17+
StorageObjectOps,
18+
)
919
from .agent import Agent
1020
from ._types import ScenarioPreview
1121
from .async_ import (
@@ -15,6 +25,7 @@
1525
AsyncRunloopSDK,
1626
AsyncScenarioOps,
1727
AsyncSnapshotOps,
28+
AsyncBenchmarkOps,
1829
AsyncBlueprintOps,
1930
AsyncStorageObjectOps,
2031
)
@@ -51,6 +62,8 @@
5162
# Management interfaces
5263
"AgentOps",
5364
"AsyncAgentOps",
65+
"BenchmarkOps",
66+
"AsyncBenchmarkOps",
5467
"DevboxOps",
5568
"AsyncDevboxOps",
5669
"BlueprintOps",
@@ -66,6 +79,10 @@
6679
# Resource classes
6780
"Agent",
6881
"AsyncAgent",
82+
"Benchmark",
83+
"AsyncBenchmark",
84+
"BenchmarkRun",
85+
"AsyncBenchmarkRun",
6986
"Devbox",
7087
"AsyncDevbox",
7188
"Execution",
@@ -89,8 +106,4 @@
89106
"AsyncStorageObject",
90107
"NamedShell",
91108
"AsyncNamedShell",
92-
"BenchmarkRun",
93-
"AsyncBenchmarkRun",
94-
"Benchmark",
95-
"AsyncBenchmark",
96109
]

src/runloop_api_client/sdk/_types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
DevboxCreateParams,
1212
ObjectCreateParams,
1313
ScenarioListParams,
14+
BenchmarkListParams,
1415
BlueprintListParams,
1516
ObjectDownloadParams,
1617
ScenarioUpdateParams,
18+
BenchmarkCreateParams,
1719
BenchmarkUpdateParams,
1820
BlueprintCreateParams,
1921
DevboxUploadFileParams,
@@ -212,6 +214,14 @@ class ScenarioPreview(ScenarioView):
212214
"""The input context for the Scenario."""
213215

214216

217+
class SDKBenchmarkCreateParams(BenchmarkCreateParams, LongRequestOptions):
218+
pass
219+
220+
221+
class SDKBenchmarkListParams(BenchmarkListParams, BaseRequestOptions):
222+
pass
223+
224+
215225
class SDKBenchmarkUpdateParams(BenchmarkUpdateParams, LongRequestOptions):
216226
pass
217227

src/runloop_api_client/sdk/async_.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
SDKObjectCreateParams,
2222
SDKScenarioListParams,
2323
SDKScorerCreateParams,
24+
SDKBenchmarkListParams,
2425
SDKBlueprintListParams,
26+
SDKBenchmarkCreateParams,
2527
SDKBlueprintCreateParams,
2628
SDKDiskSnapshotListParams,
2729
SDKDevboxCreateFromImageParams,
@@ -34,6 +36,7 @@
3436
from .async_scorer import AsyncScorer
3537
from .async_scenario import AsyncScenario
3638
from .async_snapshot import AsyncSnapshot
39+
from .async_benchmark import AsyncBenchmark
3740
from .async_blueprint import AsyncBlueprint
3841
from ..lib.context_loader import TarFilter, build_directory_tar
3942
from .async_storage_object import AsyncStorageObject
@@ -815,6 +818,55 @@ async def list(self, **params: Unpack[SDKScenarioListParams]) -> list[AsyncScena
815818
return [AsyncScenario(self._client, item.id) async for item in page]
816819

817820

821+
class AsyncBenchmarkOps:
822+
"""Manage benchmarks (async). Access via ``runloop.benchmark``.
823+
824+
Example:
825+
>>> runloop = AsyncRunloopSDK()
826+
>>> benchmarks = await runloop.benchmark.list()
827+
>>> benchmark = runloop.benchmark.from_id("bmd_xxx")
828+
>>> run = await benchmark.start_run(run_name="evaluation-v1")
829+
"""
830+
831+
def __init__(self, client: AsyncRunloop) -> None:
832+
"""Initialize AsyncBenchmarkOps.
833+
834+
:param client: AsyncRunloop client instance
835+
:type client: AsyncRunloop
836+
"""
837+
self._client = client
838+
839+
async def create(self, **params: Unpack[SDKBenchmarkCreateParams]) -> AsyncBenchmark:
840+
"""Create a new benchmark.
841+
842+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkCreateParams` for available parameters
843+
:return: The newly created benchmark
844+
:rtype: AsyncBenchmark
845+
"""
846+
response = await self._client.benchmarks.create(**params)
847+
return AsyncBenchmark(self._client, response.id)
848+
849+
def from_id(self, benchmark_id: str) -> AsyncBenchmark:
850+
"""Get an AsyncBenchmark instance for an existing benchmark ID.
851+
852+
:param benchmark_id: ID of the benchmark
853+
:type benchmark_id: str
854+
:return: AsyncBenchmark instance for the given ID
855+
:rtype: AsyncBenchmark
856+
"""
857+
return AsyncBenchmark(self._client, benchmark_id)
858+
859+
async def list(self, **params: Unpack[SDKBenchmarkListParams]) -> list[AsyncBenchmark]:
860+
"""List all benchmarks, optionally filtered by parameters.
861+
862+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkListParams` for available parameters
863+
:return: List of benchmarks
864+
:rtype: list[AsyncBenchmark]
865+
"""
866+
page = await self._client.benchmarks.list(**params)
867+
return [AsyncBenchmark(self._client, item.id) for item in page.benchmarks]
868+
869+
818870
class AsyncRunloopSDK:
819871
"""High-level asynchronous entry point for the Runloop SDK.
820872
@@ -826,6 +878,8 @@ class AsyncRunloopSDK:
826878
:vartype api: AsyncRunloop
827879
:ivar agent: High-level async interface for agent management.
828880
:vartype agent: AsyncAgentOps
881+
:ivar benchmark: High-level async interface for benchmark management
882+
:vartype benchmark: AsyncBenchmarkOps
829883
:ivar devbox: High-level async interface for devbox management
830884
:vartype devbox: AsyncDevboxOps
831885
:ivar blueprint: High-level async interface for blueprint management
@@ -849,6 +903,7 @@ class AsyncRunloopSDK:
849903

850904
api: AsyncRunloop
851905
agent: AsyncAgentOps
906+
benchmark: AsyncBenchmarkOps
852907
devbox: AsyncDevboxOps
853908
blueprint: AsyncBlueprintOps
854909
scenario: AsyncScenarioOps
@@ -895,6 +950,7 @@ def __init__(
895950
)
896951

897952
self.agent = AsyncAgentOps(self.api)
953+
self.benchmark = AsyncBenchmarkOps(self.api)
898954
self.devbox = AsyncDevboxOps(self.api)
899955
self.blueprint = AsyncBlueprintOps(self.api)
900956
self.scenario = AsyncScenarioOps(self.api)

src/runloop_api_client/sdk/sync.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
SDKObjectCreateParams,
2222
SDKScenarioListParams,
2323
SDKScorerCreateParams,
24+
SDKBenchmarkListParams,
2425
SDKBlueprintListParams,
26+
SDKBenchmarkCreateParams,
2527
SDKBlueprintCreateParams,
2628
SDKDiskSnapshotListParams,
2729
SDKDevboxCreateFromImageParams,
@@ -33,6 +35,7 @@
3335
from ._helpers import detect_content_type
3436
from .scenario import Scenario
3537
from .snapshot import Snapshot
38+
from .benchmark import Benchmark
3639
from .blueprint import Blueprint
3740
from .storage_object import StorageObject
3841
from .scenario_builder import ScenarioBuilder
@@ -840,6 +843,55 @@ def list(self, **params: Unpack[SDKScenarioListParams]) -> list[Scenario]:
840843
return [Scenario(self._client, item.id) for item in page]
841844

842845

846+
class BenchmarkOps:
847+
"""Manage benchmarks. Access via ``runloop.benchmark``.
848+
849+
Example:
850+
>>> runloop = RunloopSDK()
851+
>>> benchmarks = runloop.benchmark.list()
852+
>>> benchmark = runloop.benchmark.from_id("bmd_xxx")
853+
>>> run = benchmark.start_run(run_name="evaluation-v1")
854+
"""
855+
856+
def __init__(self, client: Runloop) -> None:
857+
"""Initialize BenchmarkOps.
858+
859+
:param client: Runloop client instance
860+
:type client: Runloop
861+
"""
862+
self._client = client
863+
864+
def create(self, **params: Unpack[SDKBenchmarkCreateParams]) -> Benchmark:
865+
"""Create a new benchmark.
866+
867+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkCreateParams` for available parameters
868+
:return: The newly created benchmark
869+
:rtype: Benchmark
870+
"""
871+
response = self._client.benchmarks.create(**params)
872+
return Benchmark(self._client, response.id)
873+
874+
def from_id(self, benchmark_id: str) -> Benchmark:
875+
"""Get a Benchmark instance for an existing benchmark ID.
876+
877+
:param benchmark_id: ID of the benchmark
878+
:type benchmark_id: str
879+
:return: Benchmark instance for the given ID
880+
:rtype: Benchmark
881+
"""
882+
return Benchmark(self._client, benchmark_id)
883+
884+
def list(self, **params: Unpack[SDKBenchmarkListParams]) -> list[Benchmark]:
885+
"""List all benchmarks, optionally filtered by parameters.
886+
887+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBenchmarkListParams` for available parameters
888+
:return: List of benchmarks
889+
:rtype: list[Benchmark]
890+
"""
891+
page = self._client.benchmarks.list(**params)
892+
return [Benchmark(self._client, item.id) for item in page.benchmarks]
893+
894+
843895
class RunloopSDK:
844896
"""High-level synchronous entry point for the Runloop SDK.
845897
@@ -851,6 +903,8 @@ class RunloopSDK:
851903
:vartype api: Runloop
852904
:ivar agent: High-level interface for agent management.
853905
:vartype agent: AgentOps
906+
:ivar benchmark: High-level interface for benchmark management
907+
:vartype benchmark: BenchmarkOps
854908
:ivar devbox: High-level interface for devbox management
855909
:vartype devbox: DevboxOps
856910
:ivar blueprint: High-level interface for blueprint management
@@ -874,6 +928,7 @@ class RunloopSDK:
874928

875929
api: Runloop
876930
agent: AgentOps
931+
benchmark: BenchmarkOps
877932
devbox: DevboxOps
878933
blueprint: BlueprintOps
879934
scenario: ScenarioOps
@@ -920,6 +975,7 @@ def __init__(
920975
)
921976

922977
self.agent = AgentOps(self.api)
978+
self.benchmark = BenchmarkOps(self.api)
923979
self.devbox = DevboxOps(self.api)
924980
self.blueprint = BlueprintOps(self.api)
925981
self.scenario = ScenarioOps(self.api)

tests/sdk/test_async_ops.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
MockScorerView,
1818
MockScenarioView,
1919
MockSnapshotView,
20+
MockBenchmarkView,
2021
MockBlueprintView,
2122
create_mock_httpx_response,
2223
)
@@ -27,12 +28,14 @@
2728
AsyncAgentOps,
2829
AsyncScenario,
2930
AsyncSnapshot,
31+
AsyncBenchmark,
3032
AsyncBlueprint,
3133
AsyncDevboxOps,
3234
AsyncScorerOps,
3335
AsyncRunloopSDK,
3436
AsyncScenarioOps,
3537
AsyncSnapshotOps,
38+
AsyncBenchmarkOps,
3639
AsyncBlueprintOps,
3740
AsyncStorageObject,
3841
AsyncStorageObjectOps,
@@ -1200,6 +1203,62 @@ async def async_iter():
12001203
mock_async_client.scenarios.list.assert_awaited_once()
12011204

12021205

1206+
class TestAsyncBenchmarkOps:
1207+
"""Tests for AsyncBenchmarkOps class."""
1208+
1209+
@pytest.mark.asyncio
1210+
async def test_create(self, mock_async_client: AsyncMock, benchmark_view: MockBenchmarkView) -> None:
1211+
"""Test create method."""
1212+
mock_async_client.benchmarks.create = AsyncMock(return_value=benchmark_view)
1213+
1214+
ops = AsyncBenchmarkOps(mock_async_client)
1215+
benchmark = await ops.create(name="test-benchmark", scenario_ids=["scn_001", "scn_002"])
1216+
1217+
assert isinstance(benchmark, AsyncBenchmark)
1218+
assert benchmark.id == "bmd_123"
1219+
mock_async_client.benchmarks.create.assert_awaited_once_with(
1220+
name="test-benchmark", scenario_ids=["scn_001", "scn_002"]
1221+
)
1222+
1223+
def test_from_id(self, mock_async_client: AsyncMock) -> None:
1224+
"""Test from_id method."""
1225+
ops = AsyncBenchmarkOps(mock_async_client)
1226+
benchmark = ops.from_id("bmd_123")
1227+
1228+
assert isinstance(benchmark, AsyncBenchmark)
1229+
assert benchmark.id == "bmd_123"
1230+
1231+
@pytest.mark.asyncio
1232+
async def test_list_multiple(self, mock_async_client: AsyncMock) -> None:
1233+
"""Test list method with multiple results."""
1234+
benchmark_view1 = MockBenchmarkView(id="bmd_001", name="benchmark-1")
1235+
benchmark_view2 = MockBenchmarkView(id="bmd_002", name="benchmark-2")
1236+
page = SimpleNamespace(benchmarks=[benchmark_view1, benchmark_view2])
1237+
mock_async_client.benchmarks.list = AsyncMock(return_value=page)
1238+
1239+
ops = AsyncBenchmarkOps(mock_async_client)
1240+
benchmarks = await ops.list(limit=10)
1241+
1242+
assert len(benchmarks) == 2
1243+
assert isinstance(benchmarks[0], AsyncBenchmark)
1244+
assert isinstance(benchmarks[1], AsyncBenchmark)
1245+
assert benchmarks[0].id == "bmd_001"
1246+
assert benchmarks[1].id == "bmd_002"
1247+
mock_async_client.benchmarks.list.assert_awaited_once_with(limit=10)
1248+
1249+
@pytest.mark.asyncio
1250+
async def test_list_with_name_filter(self, mock_async_client: AsyncMock, benchmark_view: MockBenchmarkView) -> None:
1251+
"""Test list method with name filter."""
1252+
page = SimpleNamespace(benchmarks=[benchmark_view])
1253+
mock_async_client.benchmarks.list = AsyncMock(return_value=page)
1254+
1255+
ops = AsyncBenchmarkOps(mock_async_client)
1256+
benchmarks = await ops.list(name="test-benchmark", limit=10)
1257+
1258+
assert len(benchmarks) == 1
1259+
mock_async_client.benchmarks.list.assert_awaited_once_with(name="test-benchmark", limit=10)
1260+
1261+
12031262
class TestAsyncRunloopSDK:
12041263
"""Tests for AsyncRunloopSDK class."""
12051264

@@ -1208,6 +1267,7 @@ def test_init(self) -> None:
12081267
runloop = AsyncRunloopSDK(bearer_token="test-token")
12091268
assert runloop.api is not None
12101269
assert isinstance(runloop.agent, AsyncAgentOps)
1270+
assert isinstance(runloop.benchmark, AsyncBenchmarkOps)
12111271
assert isinstance(runloop.devbox, AsyncDevboxOps)
12121272
assert isinstance(runloop.scorer, AsyncScorerOps)
12131273
assert isinstance(runloop.snapshot, AsyncSnapshotOps)

0 commit comments

Comments
 (0)