Skip to content

Commit 82f3597

Browse files
committed
add score_and_complete and download_logs methods to scenario runs, and update docstrings to not leak internals
1 parent 7ca3154 commit 82f3597

4 files changed

Lines changed: 178 additions & 138 deletions

File tree

src/runloop_api_client/sdk/async_scenario.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,24 @@
1111

1212

1313
class AsyncScenario:
14-
"""Async wrapper around a scenario resource.
14+
"""A scenario for evaluating agent performance (async).
1515
1616
Provides async methods for retrieving scenario details, updating the scenario,
17-
and starting scenario runs.
17+
and starting scenario runs. Obtain instances via ``runloop.scenario.from_id()``
18+
or ``runloop.scenario.list()``.
1819
1920
Example:
20-
>>> scenario = sdk.scenario.from_id("scn-xxx")
21+
>>> scenario = runloop.scenario.from_id("scn-xxx")
2122
>>> info = await scenario.get_info()
2223
>>> run = await scenario.run(run_name="test-run")
23-
>>> devbox = run.devbox
2424
"""
2525

2626
def __init__(self, client: AsyncRunloop, scenario_id: str) -> None:
27-
"""Initialize the wrapper.
27+
"""Create an AsyncScenario instance.
2828
29-
:param client: Generated AsyncRunloop client
29+
:param client: AsyncRunloop client instance
3030
:type client: AsyncRunloop
31-
:param scenario_id: Scenario ID returned by the API
31+
:param scenario_id: Scenario ID
3232
:type scenario_id: str
3333
"""
3434
self._client = client
@@ -53,7 +53,7 @@ async def get_info(
5353
) -> ScenarioView:
5454
"""Retrieve current scenario details.
5555
56-
:param options: Optional request configuration
56+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
5757
:return: Current scenario info
5858
:rtype: ScenarioView
5959
"""
@@ -70,7 +70,7 @@ async def update(
7070
7171
Only provided fields will be updated.
7272
73-
:param params: See SDKScenarioUpdateParams for available parameters
73+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioUpdateParams` for available parameters
7474
:return: Updated scenario info
7575
:rtype: ScenarioView
7676
"""
@@ -83,14 +83,14 @@ async def run_async(
8383
self,
8484
**params: Unpack[SDKScenarioRunAsyncParams],
8585
) -> AsyncScenarioRun:
86-
"""Start a new scenario run.
86+
"""Start a new scenario run without waiting for the devbox.
8787
88-
Creates a new scenario run and returns a wrapper for managing it.
89-
The underlying devbox may still be starting; call await_env_ready()
90-
on the returned AsyncScenarioRun to wait for it to be ready.
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.
9191
92-
:param params: See SDKScenarioRunParams for available parameters
93-
:return: Wrapper for the new scenario run
92+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioRunAsyncParams` for available parameters
93+
:return: AsyncScenarioRun instance for managing the run
9494
:rtype: AsyncScenarioRun
9595
"""
9696
run_view = await self._client.scenarios.start_run(
@@ -103,12 +103,12 @@ async def run(
103103
self,
104104
**params: Unpack[SDKScenarioRunParams],
105105
) -> AsyncScenarioRun:
106-
"""Start a new scenario run and wait for environment to be ready.
106+
"""Start a new scenario run and wait for the devbox to be ready.
107107
108108
Convenience method that starts a run and waits for the devbox to be ready.
109109
110-
:param params: See SDKScenarioRunParams for available parameters
111-
:return: Wrapper for the scenario run with ready environment
110+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScenarioRunParams` for available parameters
111+
:return: AsyncScenarioRun instance with ready devbox
112112
:rtype: AsyncScenarioRun
113113
"""
114114
run_view = await self._client.scenarios.start_run_and_await_env_ready(

src/runloop_api_client/sdk/async_scenario_run.py

Lines changed: 71 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,42 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Optional
5+
import os
6+
from typing import Union, Optional
7+
from functools import cached_property
68
from typing_extensions import Unpack, override
79

810
from ..types import ScenarioRunView
9-
from ._types import BaseRequestOptions, LongRequestOptions
11+
from ._types import BaseRequestOptions, LongRequestOptions, PollingRequestOptions
1012
from .._client import AsyncRunloop
11-
from ..lib.polling import PollingConfig
13+
from ._helpers import filter_params
14+
from .async_devbox import AsyncDevbox
1215
from ..types.scoring_contract_result_view import ScoringContractResultView
1316

14-
if TYPE_CHECKING:
15-
from .async_devbox import AsyncDevbox
16-
1717

1818
class AsyncScenarioRun:
19-
"""Async wrapper around a running scenario with devbox access.
19+
"""A running scenario with devbox access (async).
2020
2121
Provides async methods for managing the scenario run lifecycle, accessing
22-
the underlying devbox, and retrieving scoring results.
22+
the devbox, and retrieving scoring results. Obtain instances via
23+
``scenario.run()`` or ``scenario.run_async()``.
2324
2425
Example:
25-
>>> scenario = await sdk.scenario.from_id("scn-xxx")
26-
>>> run = await scenario.run()
26+
>>> scenario = runloop.scenario.from_id("scn-xxx")
27+
>>> run = await scenario.run_async()
2728
>>> await run.await_env_ready()
2829
>>> devbox = run.devbox
2930
>>> # ... agent does work on the devbox ...
30-
>>> await run.score()
31-
>>> await run.await_scored()
32-
>>> result = await run.get_score()
31+
>>> await run.score_and_await()
32+
>>> score = await run.get_score()
3333
"""
3434

3535
def __init__(self, client: AsyncRunloop, run_id: str, devbox_id: str) -> None:
36-
"""Initialize the wrapper.
36+
"""Create an AsyncScenarioRun instance.
3737
38-
:param client: Generated AsyncRunloop client
38+
:param client: AsyncRunloop client instance
3939
:type client: AsyncRunloop
40-
:param run_id: ScenarioRun ID returned by the API
40+
:param run_id: Scenario run ID
4141
:type run_id: str
4242
:param devbox_id: Devbox ID associated with this run
4343
:type devbox_id: str
@@ -68,17 +68,15 @@ def devbox_id(self) -> str:
6868
"""
6969
return self._devbox_id
7070

71-
@property
72-
def devbox(self) -> "AsyncDevbox":
73-
"""Return an AsyncDevbox wrapper for the underlying devbox.
71+
@cached_property
72+
def devbox(self) -> AsyncDevbox:
73+
"""The devbox instance for this scenario run.
7474
7575
Use this to interact with the devbox environment during the scenario run.
7676
77-
:return: AsyncDevbox wrapper instance
77+
:return: AsyncDevbox instance
7878
:rtype: AsyncDevbox
7979
"""
80-
from .async_devbox import AsyncDevbox
81-
8280
return AsyncDevbox(self._client, self._devbox_id)
8381

8482
async def get_info(
@@ -87,7 +85,7 @@ async def get_info(
8785
) -> ScenarioRunView:
8886
"""Retrieve current scenario run status and metadata.
8987
90-
:param options: Optional request configuration
88+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
9189
:return: Current scenario run state info
9290
:rtype: ScenarioRunView
9391
"""
@@ -98,22 +96,18 @@ async def get_info(
9896

9997
async def await_env_ready(
10098
self,
101-
*,
102-
polling_config: PollingConfig | None = None,
103-
**options: Unpack[BaseRequestOptions],
99+
**options: Unpack[PollingRequestOptions],
104100
) -> ScenarioRunView:
105101
"""Wait for the scenario environment (devbox) to be ready.
106102
107103
Blocks until the devbox reaches running state.
108104
109-
:param polling_config: Optional polling configuration
110-
:type polling_config: PollingConfig | None
111-
:param options: Optional request configuration
105+
:param options: See :typeddict:`~runloop_api_client.sdk._types.PollingRequestOptions` for available options
112106
:return: Scenario run state after environment is ready
113107
:rtype: ScenarioRunView
114108
"""
115-
await self._client.devboxes.await_running(self._devbox_id, polling_config=polling_config)
116-
return await self.get_info(**options)
109+
await self._client.devboxes.await_running(self._devbox_id, polling_config=options.get("polling_config"))
110+
return await self.get_info(**filter_params(options, PollingRequestOptions))
117111

118112
async def score(
119113
self,
@@ -123,7 +117,7 @@ async def score(
123117
124118
This triggers the scoring process using the scenario's scoring contract.
125119
126-
:param options: Optional long-running request configuration
120+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
127121
:return: Updated scenario run state
128122
:rtype: ScenarioRunView
129123
"""
@@ -134,45 +128,53 @@ async def score(
134128

135129
async def await_scored(
136130
self,
137-
*,
138-
polling_config: PollingConfig | None = None,
139-
**options: Unpack[BaseRequestOptions],
131+
**options: Unpack[PollingRequestOptions],
140132
) -> ScenarioRunView:
141133
"""Wait for the scenario run to be scored.
142134
143135
Blocks until scoring is complete.
144136
145-
:param polling_config: Optional polling configuration
146-
:type polling_config: PollingConfig | None
147-
:param options: Optional request configuration
137+
:param options: See :typeddict:`~runloop_api_client.sdk._types.PollingRequestOptions` for available options
148138
:return: Scored scenario run state
149139
:rtype: ScenarioRunView
150140
"""
151141
return await self._client.scenarios.runs.await_scored(
152142
self._id,
153-
polling_config=polling_config,
154143
**options,
155144
)
156145

157146
async def score_and_await(
158147
self,
159-
*,
160-
polling_config: PollingConfig | None = None,
161-
**options: Unpack[BaseRequestOptions],
148+
**options: Unpack[PollingRequestOptions],
162149
) -> ScenarioRunView:
163150
"""Submit for scoring and wait for completion.
164151
165152
Convenience method that calls score() then await_scored().
166153
167-
:param polling_config: Optional polling configuration
168-
:type polling_config: PollingConfig | None
169-
:param options: Optional request configuration
154+
:param options: See :typeddict:`~runloop_api_client.sdk._types.PollingRequestOptions` for available options
170155
:return: Scored scenario run state
171156
:rtype: ScenarioRunView
172157
"""
173158
return await self._client.scenarios.runs.score_and_await(
174159
self._id,
175-
polling_config=polling_config,
160+
**options,
161+
)
162+
163+
async def score_and_complete(
164+
self,
165+
**options: Unpack[PollingRequestOptions],
166+
) -> ScenarioRunView:
167+
"""Score the run, wait for scoring, then complete and shutdown.
168+
169+
Convenience method that scores the scenario run, waits for scoring to
170+
finish, then completes the run and shuts down the devbox.
171+
172+
:param options: See :typeddict:`~runloop_api_client.sdk._types.PollingRequestOptions` for available options
173+
:return: Completed scenario run state with scoring results
174+
:rtype: ScenarioRunView
175+
"""
176+
return await self._client.scenarios.runs.score_and_complete(
177+
self._id,
176178
**options,
177179
)
178180

@@ -182,7 +184,7 @@ async def complete(
182184
) -> ScenarioRunView:
183185
"""Complete the scenario run and shutdown the devbox.
184186
185-
:param options: Optional long-running request configuration
187+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
186188
:return: Final scenario run state
187189
:rtype: ScenarioRunView
188190
"""
@@ -197,7 +199,7 @@ async def cancel(
197199
) -> ScenarioRunView:
198200
"""Cancel the scenario run and shutdown the devbox.
199201
200-
:param options: Optional long-running request configuration
202+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
201203
:return: Cancelled scenario run state
202204
:rtype: ScenarioRunView
203205
"""
@@ -206,16 +208,34 @@ async def cancel(
206208
**options,
207209
)
208210

211+
async def download_logs(
212+
self,
213+
file: Union[str, os.PathLike[str]],
214+
**options: Unpack[LongRequestOptions],
215+
) -> None:
216+
"""Download all logs for this scenario run to a zip file.
217+
218+
Downloads a zip archive containing all logs from the scenario run's
219+
associated devbox.
220+
221+
:param file: Path where the zip file will be written
222+
:type file: str | os.PathLike[str]
223+
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
224+
"""
225+
response = await self._client.scenarios.runs.download_logs(self._id, **options)
226+
await response.write_to_file(file)
227+
209228
async def get_score(
210229
self,
211230
**options: Unpack[BaseRequestOptions],
212231
) -> Optional[ScoringContractResultView]:
213232
"""Get the scoring result for this run.
214233
215-
Returns None if the run has not been scored yet.
234+
Returns None if the run has not been scored yet. Always makes an API
235+
call to retrieve the current scoring result.
216236
217-
:param options: Optional request configuration
218-
:return: Scoring result or None
237+
:param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options
238+
:return: Scoring result or None if not yet scored
219239
:rtype: Optional[ScoringContractResultView]
220240
"""
221241
info = await self.get_info(**options)

0 commit comments

Comments
 (0)