Skip to content

Commit 21f46f1

Browse files
authored
feat(scenarios): added scenario class to sdk (#701)
* uv lock upgrade * scenario wrapper * modified ScenarioStartRunParams to follow our previous pattern for use in the SDK * fixed redundancy and formatting in scenario and scenario run unit tests * formatting fixes * changed scenario and scenario smoketests to update existing scenarios and add metadata tag instead of creating new ones every time (workaround for lack of cleanup option) * changed scenario run methods to `run` and `run_async`, updated parameter TypedDicts appropriately * exposed all scenario update parameters appropriately using typeddict * formatting fixes * remove scenario builder smoketests * add score_and_complete and download_logs methods to scenario runs, and update docstrings to not leak internals * add unit tests for ScenarioOps and AsyncScenarioOps * update tests for scenario.update and change test name for scenario.run and run_async * add unit tests for download_logs and score_and_complete * don't pass polling_config to get_info within await_env_ready
1 parent 5fe7d00 commit 21f46f1

22 files changed

Lines changed: 2093 additions & 167 deletions

src/runloop_api_client/sdk/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,35 @@
55

66
from __future__ import annotations
77

8-
from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps
8+
from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, ScenarioOps, SnapshotOps, BlueprintOps, StorageObjectOps
99
from .agent import Agent
1010
from .async_ import (
1111
AsyncAgentOps,
1212
AsyncDevboxOps,
1313
AsyncScorerOps,
1414
AsyncRunloopSDK,
15+
AsyncScenarioOps,
1516
AsyncSnapshotOps,
1617
AsyncBlueprintOps,
1718
AsyncStorageObjectOps,
1819
)
1920
from .devbox import Devbox, NamedShell
2021
from .scorer import Scorer
22+
from .scenario import Scenario
2123
from .snapshot import Snapshot
2224
from .blueprint import Blueprint
2325
from .execution import Execution
2426
from .async_agent import AsyncAgent
2527
from .async_devbox import AsyncDevbox, AsyncNamedShell
2628
from .async_scorer import AsyncScorer
29+
from .scenario_run import ScenarioRun
30+
from .async_scenario import AsyncScenario
2731
from .async_snapshot import AsyncSnapshot
2832
from .storage_object import StorageObject
2933
from .async_blueprint import AsyncBlueprint
3034
from .async_execution import AsyncExecution
3135
from .execution_result import ExecutionResult
36+
from .async_scenario_run import AsyncScenarioRun
3237
from .async_storage_object import AsyncStorageObject
3338
from .async_execution_result import AsyncExecutionResult
3439

@@ -43,6 +48,8 @@
4348
"AsyncDevboxOps",
4449
"BlueprintOps",
4550
"AsyncBlueprintOps",
51+
"ScenarioOps",
52+
"AsyncScenarioOps",
4653
"ScorerOps",
4754
"AsyncScorerOps",
4855
"SnapshotOps",
@@ -60,6 +67,10 @@
6067
"AsyncExecutionResult",
6168
"Blueprint",
6269
"AsyncBlueprint",
70+
"Scenario",
71+
"AsyncScenario",
72+
"ScenarioRun",
73+
"AsyncScenarioRun",
6374
"Scorer",
6475
"AsyncScorer",
6576
"Snapshot",

src/runloop_api_client/sdk/_types.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
from ..types.agent_create_params import AgentCreateParams
1212
from ..types.devbox_create_params import DevboxCreateParams, DevboxBaseCreateParams
1313
from ..types.object_create_params import ObjectCreateParams
14+
from ..types.scenario_list_params import ScenarioListParams
1415
from ..types.blueprint_list_params import BlueprintListParams
1516
from ..types.object_download_params import ObjectDownloadParams
17+
from ..types.scenario_update_params import ScenarioUpdateParams
1618
from ..types.blueprint_create_params import BlueprintCreateParams
1719
from ..types.devbox_upload_file_params import DevboxUploadFileParams
20+
from ..types.scenario_start_run_params import ScenarioStartRunBaseParams
1821
from ..types.devbox_create_tunnel_params import DevboxCreateTunnelParams
1922
from ..types.devbox_download_file_params import DevboxDownloadFileParams
2023
from ..types.devbox_execute_async_params import DevboxNiceExecuteAsyncParams
@@ -167,3 +170,19 @@ class SDKAgentCreateParams(AgentCreateParams, LongRequestOptions):
167170

168171
class SDKAgentListParams(AgentListParams, BaseRequestOptions):
169172
pass
173+
174+
175+
class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions):
176+
pass
177+
178+
179+
class SDKScenarioUpdateParams(ScenarioUpdateParams, LongRequestOptions):
180+
pass
181+
182+
183+
class SDKScenarioRunAsyncParams(ScenarioStartRunBaseParams, LongRequestOptions):
184+
pass
185+
186+
187+
class SDKScenarioRunParams(ScenarioStartRunBaseParams, LongPollingRequestOptions):
188+
pass

src/runloop_api_client/sdk/agent.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ class Agent:
1919
including retrieving agent information.
2020
2121
Example:
22-
>>> agent = runloop.agent.create_from_npm(
23-
... name="my-agent",
24-
... package_name="@runloop/example-agent"
25-
... )
22+
>>> agent = runloop.agent.create_from_npm(name="my-agent", package_name="@runloop/example-agent")
2623
>>> info = agent.get_info()
2724
>>> print(info.name)
2825
"""

src/runloop_api_client/sdk/async_.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
SDKAgentCreateParams,
2222
SDKDevboxCreateParams,
2323
SDKObjectCreateParams,
24+
SDKScenarioListParams,
2425
SDKScorerCreateParams,
2526
SDKBlueprintListParams,
2627
SDKBlueprintCreateParams,
@@ -33,6 +34,7 @@
3334
from .async_agent import AsyncAgent
3435
from .async_devbox import AsyncDevbox
3536
from .async_scorer import AsyncScorer
37+
from .async_scenario import AsyncScenario
3638
from .async_snapshot import AsyncSnapshot
3739
from .async_blueprint import AsyncBlueprint
3840
from .async_storage_object import AsyncStorageObject
@@ -543,7 +545,8 @@ async def list(self, **params: Unpack[SDKScorerListParams]) -> list[AsyncScorer]
543545
"""
544546
page = await self._client.scenarios.scorers.list(**params)
545547
return [AsyncScorer(self._client, item.id) async for item in page]
546-
548+
549+
547550
class AsyncAgentOps:
548551
"""High-level async manager for creating and managing agents.
549552
@@ -553,15 +556,10 @@ class AsyncAgentOps:
553556
Example:
554557
>>> runloop = AsyncRunloopSDK()
555558
>>> # Create agent from NPM package
556-
>>> agent = await runloop.agent.create_from_npm(
557-
... name="my-agent",
558-
... package_name="@runloop/example-agent"
559-
... )
559+
>>> agent = await runloop.agent.create_from_npm(name="my-agent", package_name="@runloop/example-agent")
560560
>>> # Create agent from Git repository
561561
>>> agent = await runloop.agent.create_from_git(
562-
... name="git-agent",
563-
... repository="https://github.com/user/agent-repo",
564-
... ref="main"
562+
... name="git-agent", repository="https://github.com/user/agent-repo", ref="main"
565563
... )
566564
>>> # List all agents
567565
>>> agents = await runloop.agent.list(limit=10)
@@ -615,7 +613,9 @@ async def create_from_npm(
615613
:raises ValueError: If 'source' is provided in params
616614
"""
617615
if "source" in params:
618-
raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration")
616+
raise ValueError(
617+
"Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration"
618+
)
619619

620620
npm_config: dict = {"package_name": package_name}
621621
if npm_version is not None:
@@ -655,7 +655,9 @@ async def create_from_pip(
655655
:raises ValueError: If 'source' is provided in params
656656
"""
657657
if "source" in params:
658-
raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration")
658+
raise ValueError(
659+
"Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration"
660+
)
659661

660662
pip_config: dict = {"package_name": package_name}
661663
if pip_version is not None:
@@ -692,7 +694,9 @@ async def create_from_git(
692694
:raises ValueError: If 'source' is provided in params
693695
"""
694696
if "source" in params:
695-
raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration")
697+
raise ValueError(
698+
"Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration"
699+
)
696700

697701
git_config: dict = {"repository": repository}
698702
if ref is not None:
@@ -724,7 +728,9 @@ async def create_from_object(
724728
:raises ValueError: If 'source' is provided in params
725729
"""
726730
if "source" in params:
727-
raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration")
731+
raise ValueError(
732+
"Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration"
733+
)
728734

729735
object_config: dict = {"object_id": object_id}
730736
if agent_setup is not None:
@@ -761,6 +767,45 @@ async def list(
761767
return [AsyncAgent(self._client, item.id, item) for item in page.agents]
762768

763769

770+
class AsyncScenarioOps:
771+
"""Manage scenarios (async). Access via ``runloop.scenario``.
772+
773+
Example:
774+
>>> runloop = AsyncRunloopSDK()
775+
>>> scenario = runloop.scenario.from_id("scn-xxx")
776+
>>> run = await scenario.run()
777+
>>> scenarios = await runloop.scenario.list()
778+
"""
779+
780+
def __init__(self, client: AsyncRunloop) -> None:
781+
"""Initialize AsyncScenarioOps.
782+
783+
:param client: AsyncRunloop client instance
784+
:type client: AsyncRunloop
785+
"""
786+
self._client = client
787+
788+
def from_id(self, scenario_id: str) -> AsyncScenario:
789+
"""Get an AsyncScenario instance for an existing scenario ID.
790+
791+
:param scenario_id: ID of the scenario
792+
:type scenario_id: str
793+
:return: AsyncScenario instance for the given ID
794+
:rtype: AsyncScenario
795+
"""
796+
return AsyncScenario(self._client, scenario_id)
797+
798+
async def list(self, **params: Unpack[SDKScenarioListParams]) -> list[AsyncScenario]:
799+
"""List all scenarios, optionally filtered by parameters.
800+
801+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioListParams` for available parameters
802+
:return: List of scenarios
803+
:rtype: list[AsyncScenario]
804+
"""
805+
page = await self._client.scenarios.list(**params)
806+
return [AsyncScenario(self._client, item.id) async for item in page]
807+
808+
764809
class AsyncRunloopSDK:
765810
"""High-level asynchronous entry point for the Runloop SDK.
766811
@@ -776,6 +821,8 @@ class AsyncRunloopSDK:
776821
:vartype devbox: AsyncDevboxOps
777822
:ivar blueprint: High-level async interface for blueprint management
778823
:vartype blueprint: AsyncBlueprintOps
824+
:ivar scenario: High-level async interface for scenario management
825+
:vartype scenario: AsyncScenarioOps
779826
:ivar scorer: High-level async interface for scorer management
780827
:vartype scorer: AsyncScorerOps
781828
:ivar snapshot: High-level async interface for snapshot management
@@ -795,6 +842,7 @@ class AsyncRunloopSDK:
795842
agent: AsyncAgentOps
796843
devbox: AsyncDevboxOps
797844
blueprint: AsyncBlueprintOps
845+
scenario: AsyncScenarioOps
798846
scorer: AsyncScorerOps
799847
snapshot: AsyncSnapshotOps
800848
storage_object: AsyncStorageObjectOps
@@ -840,6 +888,7 @@ def __init__(
840888
self.agent = AsyncAgentOps(self.api)
841889
self.devbox = AsyncDevboxOps(self.api)
842890
self.blueprint = AsyncBlueprintOps(self.api)
891+
self.scenario = AsyncScenarioOps(self.api)
843892
self.scorer = AsyncScorerOps(self.api)
844893
self.snapshot = AsyncSnapshotOps(self.api)
845894
self.storage_object = AsyncStorageObjectOps(self.api)

src/runloop_api_client/sdk/async_agent.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ class AsyncAgent:
1919
including retrieving agent information.
2020
2121
Example:
22-
>>> agent = await runloop.agent.create_from_npm(
23-
... name="my-agent",
24-
... package_name="@runloop/example-agent"
25-
... )
22+
>>> agent = await runloop.agent.create_from_npm(name="my-agent", package_name="@runloop/example-agent")
2623
>>> info = await agent.get_info()
2724
>>> print(info.name)
2825
"""
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""AsyncScenario resource class for asynchronous operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Unpack, override
6+
7+
from ..types import ScenarioView
8+
from ._types import BaseRequestOptions, SDKScenarioRunParams, SDKScenarioUpdateParams, SDKScenarioRunAsyncParams
9+
from .._client import AsyncRunloop
10+
from .async_scenario_run import AsyncScenarioRun
11+
12+
13+
class AsyncScenario:
14+
"""A scenario for evaluating agent performance (async).
15+
16+
Provides async methods for retrieving scenario details, updating the scenario,
17+
and starting scenario runs. Obtain instances via ``runloop.scenario.from_id()``
18+
or ``runloop.scenario.list()``.
19+
20+
Example:
21+
>>> scenario = runloop.scenario.from_id("scn-xxx")
22+
>>> info = await scenario.get_info()
23+
>>> run = await scenario.run(run_name="test-run")
24+
"""
25+
26+
def __init__(self, client: AsyncRunloop, scenario_id: str) -> None:
27+
"""Create an AsyncScenario instance.
28+
29+
:param client: AsyncRunloop client instance
30+
:type client: AsyncRunloop
31+
:param scenario_id: Scenario ID
32+
:type scenario_id: str
33+
"""
34+
self._client = client
35+
self._id = scenario_id
36+
37+
@override
38+
def __repr__(self) -> str:
39+
return f"<AsyncScenario id={self._id!r}>"
40+
41+
@property
42+
def id(self) -> str:
43+
"""Return the scenario ID.
44+
45+
:return: Unique scenario ID
46+
:rtype: str
47+
"""
48+
return self._id
49+
50+
async def get_info(
51+
self,
52+
**options: Unpack[BaseRequestOptions],
53+
) -> ScenarioView:
54+
"""Retrieve current scenario details.
55+
56+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
57+
:return: Current scenario info
58+
:rtype: ScenarioView
59+
"""
60+
return await self._client.scenarios.retrieve(
61+
self._id,
62+
**options,
63+
)
64+
65+
async def update(
66+
self,
67+
**params: Unpack[SDKScenarioUpdateParams],
68+
) -> ScenarioView:
69+
"""Update the scenario.
70+
71+
Only provided fields will be updated.
72+
73+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioUpdateParams` for available parameters
74+
:return: Updated scenario info
75+
:rtype: ScenarioView
76+
"""
77+
return await self._client.scenarios.update(
78+
self._id,
79+
**params,
80+
)
81+
82+
async def run_async(
83+
self,
84+
**params: Unpack[SDKScenarioRunAsyncParams],
85+
) -> AsyncScenarioRun:
86+
"""Start a new scenario run without waiting for the devbox.
87+
88+
Creates a new scenario run and returns immediately. The devbox may still
89+
be starting; call ``await_env_ready()`` on the returned AsyncScenarioRun
90+
to wait for it to be ready.
91+
92+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioRunAsyncParams` for available parameters
93+
:return: AsyncScenarioRun instance for managing the run
94+
:rtype: AsyncScenarioRun
95+
"""
96+
run_view = await self._client.scenarios.start_run(
97+
scenario_id=self._id,
98+
**params,
99+
)
100+
return AsyncScenarioRun(self._client, run_view.id, run_view.devbox_id)
101+
102+
async def run(
103+
self,
104+
**params: Unpack[SDKScenarioRunParams],
105+
) -> AsyncScenarioRun:
106+
"""Start a new scenario run and wait for the devbox to be ready.
107+
108+
Convenience method that starts a run and waits for the devbox to be ready.
109+
110+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioRunParams` for available parameters
111+
:return: AsyncScenarioRun instance with ready devbox
112+
:rtype: AsyncScenarioRun
113+
"""
114+
run_view = await self._client.scenarios.start_run_and_await_env_ready(
115+
scenario_id=self._id,
116+
**params,
117+
)
118+
return AsyncScenarioRun(self._client, run_view.id, run_view.devbox_id)

0 commit comments

Comments
 (0)