Skip to content

Commit 2d41a15

Browse files
authored
feat(scenarios): add scenario builder to sdk (#706)
* init commit * formatting fix * clean up imports in scenario ops unit tests * use Blueprint and Snapshot objects directly in ScenarioBuilder * consolidate from_blueprint and from_snapshot unit tests * further consolidate scenario builder unit tests, make sure async coverage matches sync * stricter type declaration for _build_params * expose request options in push() * add scenario creation smoketests, with push_or_update logic * update sdk smoke tests with all ops * avoid modifyng _scorers internal state when normalizing weights (create copy instead) * formatting fixes * update builder docstrings to use fluent pattern, replaced all references to RunloopSDK() to 'runloop' instead of 'sdk' or 'client' * clarify from_blueprint and from_snapshot docstrings * rename add_scorer methods to be more clear * format fix * address type check errors in scenario builder unit tests * rename add_scorer methods in docstrings * make sure it is clear that score is 0.0-1.0 inclusive * update script scorer docstrings * formatting * clarify reference solution/gold patch terminology and validation strategy * make name first argument passed to scenario builder * add preview method * clean up unit test imports, rename builder fixture to mock_builder * added `preview()` method to scenario builder * rename `_build_params` to `build` (now publicly exposed method) * formatting * update docstring examples to use builder.build() instead of builder.push() * formatting * update with_problem_statement and with_additional_context docstrings
1 parent fb3cc3d commit 2d41a15

22 files changed

+1963
-91
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ For a higher-level, Pythonic interface, check out the new [`RunloopSDK`](README-
3333
```python
3434
from runloop_api_client import RunloopSDK
3535

36-
sdk = RunloopSDK() # Uses RUNLOOP_API_KEY environment variable by default
36+
runloop = RunloopSDK() # Uses RUNLOOP_API_KEY environment variable by default
3737

3838
# Create a devbox and execute commands with a clean, object-oriented interface
39-
with sdk.devbox.create(name="my-devbox") as devbox:
39+
with runloop.devbox.create(name="my-devbox") as devbox:
4040
result = devbox.cmd.exec("echo 'Hello from Runloop!'")
4141
print(result.stdout())
4242
```

src/runloop_api_client/sdk/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, ScenarioOps, SnapshotOps, BlueprintOps, StorageObjectOps
99
from .agent import Agent
10+
from ._types import ScenarioPreview
1011
from .async_ import (
1112
AsyncAgentOps,
1213
AsyncDevboxOps,
@@ -33,9 +34,11 @@
3334
from .async_blueprint import AsyncBlueprint
3435
from .async_execution import AsyncExecution
3536
from .execution_result import ExecutionResult
37+
from .scenario_builder import ScenarioBuilder
3638
from .async_scenario_run import AsyncScenarioRun
3739
from .async_storage_object import AsyncStorageObject
3840
from .async_execution_result import AsyncExecutionResult
41+
from .async_scenario_builder import AsyncScenarioBuilder
3942

4043
__all__ = [
4144
# Main SDK entry points
@@ -71,6 +74,9 @@
7174
"AsyncScenario",
7275
"ScenarioRun",
7376
"AsyncScenarioRun",
77+
"ScenarioBuilder",
78+
"AsyncScenarioBuilder",
79+
"ScenarioPreview",
7480
"Scorer",
7581
"AsyncScorer",
7682
"Snapshot",

src/runloop_api_client/sdk/_types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
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.input_context import InputContext
9+
from ..types.scenario_view import ScenarioView
810
from ..types.agent_list_params import AgentListParams
911
from ..types.devbox_list_params import DevboxListParams
1012
from ..types.object_list_params import ObjectListParams
@@ -186,3 +188,18 @@ class SDKScenarioRunAsyncParams(ScenarioStartRunBaseParams, LongRequestOptions):
186188

187189
class SDKScenarioRunParams(ScenarioStartRunBaseParams, LongPollingRequestOptions):
188190
pass
191+
192+
193+
class InputContextPreview(InputContext):
194+
problem_statement: Optional[str] = None # type: ignore[assignment]
195+
"""The problem statement for the Scenario."""
196+
197+
198+
class ScenarioPreview(ScenarioView):
199+
"""Preview of scenario configuration with all fields optional."""
200+
201+
id: Optional[str] = None # type: ignore[assignment]
202+
"""The ID of the Scenario."""
203+
204+
input_context: InputContextPreview # type: ignore[assignment]
205+
"""The input context for the Scenario."""

src/runloop_api_client/sdk/async_.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from .async_blueprint import AsyncBlueprint
3838
from ..lib.context_loader import TarFilter, build_directory_tar
3939
from .async_storage_object import AsyncStorageObject
40+
from .async_scenario_builder import AsyncScenarioBuilder
4041
from ..types.object_create_params import ContentType
4142
from ..types.shared_params.agent_source import Git, Npm, Pip, Object
4243

@@ -773,6 +774,16 @@ class AsyncScenarioOps:
773774
>>> scenario = runloop.scenario.from_id("scn-xxx")
774775
>>> run = await scenario.run()
775776
>>> scenarios = await runloop.scenario.list()
777+
778+
Example using builder:
779+
>>> builder = (
780+
... runloop.scenario.builder("my-scenario")
781+
... .from_blueprint(blueprint)
782+
... .with_problem_statement("Fix the bug")
783+
... .add_test_command_scorer("tests", test_command="pytest")
784+
... )
785+
>>> params = builder.build()
786+
>>> scenario = await runloop.scenario.create(**params) # equivalent to builder.push()
776787
"""
777788

778789
def __init__(self, client: AsyncRunloop) -> None:
@@ -783,6 +794,16 @@ def __init__(self, client: AsyncRunloop) -> None:
783794
"""
784795
self._client = client
785796

797+
def builder(self, name: str) -> AsyncScenarioBuilder:
798+
"""Create a new scenario builder.
799+
800+
:param name: Name for the scenario
801+
:type name: str
802+
:return: A new AsyncScenarioBuilder instance
803+
:rtype: AsyncScenarioBuilder
804+
"""
805+
return AsyncScenarioBuilder(name, self._client)
806+
786807
def from_id(self, scenario_id: str) -> AsyncScenario:
787808
"""Get an AsyncScenario instance for an existing scenario ID.
788809

0 commit comments

Comments
 (0)