Skip to content

Commit 121f0fc

Browse files
sid-rlstainless-app[bot]
authored andcommitted
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 f57aae7 commit 121f0fc

22 files changed

Lines changed: 2074 additions & 145 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
@@ -19,6 +19,7 @@
1919
SDKAgentCreateParams,
2020
SDKDevboxCreateParams,
2121
SDKObjectCreateParams,
22+
SDKScenarioListParams,
2223
SDKScorerCreateParams,
2324
SDKBlueprintListParams,
2425
SDKBlueprintCreateParams,
@@ -31,6 +32,7 @@
3132
from .async_agent import AsyncAgent
3233
from .async_devbox import AsyncDevbox
3334
from .async_scorer import AsyncScorer
35+
from .async_scenario import AsyncScenario
3436
from .async_snapshot import AsyncSnapshot
3537
from .async_blueprint import AsyncBlueprint
3638
from ..lib.context_loader import TarFilter, build_directory_tar
@@ -548,7 +550,8 @@ async def list(self, **params: Unpack[SDKScorerListParams]) -> list[AsyncScorer]
548550
"""
549551
page = await self._client.scenarios.scorers.list(**params)
550552
return [AsyncScorer(self._client, item.id) async for item in page]
551-
553+
554+
552555
class AsyncAgentOps:
553556
"""High-level async manager for creating and managing agents.
554557
@@ -558,15 +561,10 @@ class AsyncAgentOps:
558561
Example:
559562
>>> runloop = AsyncRunloopSDK()
560563
>>> # Create agent from NPM package
561-
>>> agent = await runloop.agent.create_from_npm(
562-
... name="my-agent",
563-
... package_name="@runloop/example-agent"
564-
... )
564+
>>> agent = await runloop.agent.create_from_npm(name="my-agent", package_name="@runloop/example-agent")
565565
>>> # Create agent from Git repository
566566
>>> agent = await runloop.agent.create_from_git(
567-
... name="git-agent",
568-
... repository="https://github.com/user/agent-repo",
569-
... ref="main"
567+
... name="git-agent", repository="https://github.com/user/agent-repo", ref="main"
570568
... )
571569
>>> # List all agents
572570
>>> agents = await runloop.agent.list(limit=10)
@@ -620,7 +618,9 @@ async def create_from_npm(
620618
:raises ValueError: If 'source' is provided in params
621619
"""
622620
if "source" in params:
623-
raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration")
621+
raise ValueError(
622+
"Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration"
623+
)
624624

625625
npm_config: dict = {"package_name": package_name}
626626
if npm_version is not None:
@@ -660,7 +660,9 @@ async def create_from_pip(
660660
:raises ValueError: If 'source' is provided in params
661661
"""
662662
if "source" in params:
663-
raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration")
663+
raise ValueError(
664+
"Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration"
665+
)
664666

665667
pip_config: dict = {"package_name": package_name}
666668
if pip_version is not None:
@@ -697,7 +699,9 @@ async def create_from_git(
697699
:raises ValueError: If 'source' is provided in params
698700
"""
699701
if "source" in params:
700-
raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration")
702+
raise ValueError(
703+
"Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration"
704+
)
701705

702706
git_config: dict = {"repository": repository}
703707
if ref is not None:
@@ -729,7 +733,9 @@ async def create_from_object(
729733
:raises ValueError: If 'source' is provided in params
730734
"""
731735
if "source" in params:
732-
raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration")
736+
raise ValueError(
737+
"Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration"
738+
)
733739

734740
object_config: dict = {"object_id": object_id}
735741
if agent_setup is not None:
@@ -766,6 +772,45 @@ async def list(
766772
return [AsyncAgent(self._client, item.id, item) for item in page.agents]
767773

768774

775+
class AsyncScenarioOps:
776+
"""Manage scenarios (async). Access via ``runloop.scenario``.
777+
778+
Example:
779+
>>> runloop = AsyncRunloopSDK()
780+
>>> scenario = runloop.scenario.from_id("scn-xxx")
781+
>>> run = await scenario.run()
782+
>>> scenarios = await runloop.scenario.list()
783+
"""
784+
785+
def __init__(self, client: AsyncRunloop) -> None:
786+
"""Initialize AsyncScenarioOps.
787+
788+
:param client: AsyncRunloop client instance
789+
:type client: AsyncRunloop
790+
"""
791+
self._client = client
792+
793+
def from_id(self, scenario_id: str) -> AsyncScenario:
794+
"""Get an AsyncScenario instance for an existing scenario ID.
795+
796+
:param scenario_id: ID of the scenario
797+
:type scenario_id: str
798+
:return: AsyncScenario instance for the given ID
799+
:rtype: AsyncScenario
800+
"""
801+
return AsyncScenario(self._client, scenario_id)
802+
803+
async def list(self, **params: Unpack[SDKScenarioListParams]) -> list[AsyncScenario]:
804+
"""List all scenarios, optionally filtered by parameters.
805+
806+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioListParams` for available parameters
807+
:return: List of scenarios
808+
:rtype: list[AsyncScenario]
809+
"""
810+
page = await self._client.scenarios.list(**params)
811+
return [AsyncScenario(self._client, item.id) async for item in page]
812+
813+
769814
class AsyncRunloopSDK:
770815
"""High-level asynchronous entry point for the Runloop SDK.
771816
@@ -781,6 +826,8 @@ class AsyncRunloopSDK:
781826
:vartype devbox: AsyncDevboxOps
782827
:ivar blueprint: High-level async interface for blueprint management
783828
:vartype blueprint: AsyncBlueprintOps
829+
:ivar scenario: High-level async interface for scenario management
830+
:vartype scenario: AsyncScenarioOps
784831
:ivar scorer: High-level async interface for scorer management
785832
:vartype scorer: AsyncScorerOps
786833
:ivar snapshot: High-level async interface for snapshot management
@@ -800,6 +847,7 @@ class AsyncRunloopSDK:
800847
agent: AsyncAgentOps
801848
devbox: AsyncDevboxOps
802849
blueprint: AsyncBlueprintOps
850+
scenario: AsyncScenarioOps
803851
scorer: AsyncScorerOps
804852
snapshot: AsyncSnapshotOps
805853
storage_object: AsyncStorageObjectOps
@@ -845,6 +893,7 @@ def __init__(
845893
self.agent = AsyncAgentOps(self.api)
846894
self.devbox = AsyncDevboxOps(self.api)
847895
self.blueprint = AsyncBlueprintOps(self.api)
896+
self.scenario = AsyncScenarioOps(self.api)
848897
self.scorer = AsyncScorerOps(self.api)
849898
self.snapshot = AsyncSnapshotOps(self.api)
850899
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)