Skip to content

Commit 1fffeeb

Browse files
committed
added preview() method to scenario builder
1 parent 034399b commit 1fffeeb

5 files changed

Lines changed: 146 additions & 34 deletions

File tree

src/runloop_api_client/sdk/_types.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Union, Callable, Optional
1+
from typing import Union, Callable, Optional
22
from typing_extensions import TypedDict
33

44
from .._types import Body, Query, Headers, Timeout, NotGiven
@@ -7,7 +7,6 @@
77
from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams, ScorerValidateParams
88
from ..types.input_context import InputContext
99
from ..types.scenario_view import ScenarioView
10-
from ..types.scoring_contract import ScoringContract
1110
from ..types.agent_list_params import AgentListParams
1211
from ..types.devbox_list_params import DevboxListParams
1312
from ..types.object_list_params import ObjectListParams
@@ -202,14 +201,5 @@ class ScenarioPreview(ScenarioView):
202201
id: Optional[str] = None # type: ignore[assignment]
203202
"""The ID of the Scenario."""
204203

205-
input_context: Optional[InputContextPreview] = None # type: ignore[assignment]
204+
input_context: InputContextPreview # type: ignore[assignment]
206205
"""The input context for the Scenario."""
207-
208-
metadata: Optional[Dict[str, str]] = None # type: ignore[assignment]
209-
"""User defined metadata to attach to the scenario for organization."""
210-
211-
name: Optional[str] = None # type: ignore[assignment]
212-
"""The name of the Scenario."""
213-
214-
scoring_contract: Optional[ScoringContract] = None # type: ignore[assignment]
215-
"""The scoring contract for the Scenario."""

src/runloop_api_client/sdk/async_scenario_builder.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(self, name: str, client: AsyncRunloop) -> None:
6565
self._scorers: List[ScoringFunctionParam] = []
6666

6767
# Metadata and other options
68-
self._metadata: Optional[Dict[str, str]] = None
68+
self._metadata: Dict[str, str] = {}
6969
self._reference_output: Optional[str] = None
7070
self._required_env_vars: Optional[List[str]] = None
7171
self._required_secrets: Optional[List[str]] = None
@@ -392,16 +392,15 @@ def _build_normalized_scorers(self) -> List[ScoringFunctionParam]:
392392
total_weight = sum(s["weight"] for s in self._scorers)
393393
return [{**s, "weight": s["weight"] / total_weight} for s in self._scorers]
394394

395-
def _build_environment_params(self) -> ScenarioEnvironmentParam:
395+
def _build_environment_params(self) -> Optional[ScenarioEnvironmentParam]:
396396
"""Build environment parameters."""
397-
env_params: ScenarioEnvironmentParam = {}
398-
if self._blueprint:
399-
env_params["blueprint_id"] = self._blueprint.id
400-
if self._snapshot:
401-
env_params["snapshot_id"] = self._snapshot.id
402-
if self._working_directory:
403-
env_params["working_directory"] = self._working_directory
404-
return env_params
397+
if not self._blueprint and not self._snapshot and not self._working_directory:
398+
return None
399+
return {
400+
"blueprint_id": self._blueprint.id if self._blueprint else None,
401+
"snapshot_id": self._snapshot.id if self._snapshot else None,
402+
"working_directory": self._working_directory if self._working_directory else None,
403+
}
405404

406405
def _build_params(self) -> ScenarioCreateParams:
407406
"""Build the scenario creation parameters.

src/runloop_api_client/sdk/scenario_builder.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(self, name: str, client: Runloop) -> None:
6565
self._scorers: List[ScoringFunctionParam] = []
6666

6767
# Metadata and other options
68-
self._metadata: Optional[Dict[str, str]] = None
68+
self._metadata: Dict[str, str] = {}
6969
self._reference_output: Optional[str] = None
7070
self._required_env_vars: Optional[List[str]] = None
7171
self._required_secrets: Optional[List[str]] = None
@@ -392,16 +392,15 @@ def _build_normalized_scorers(self) -> List[ScoringFunctionParam]:
392392
total_weight = sum(s["weight"] for s in self._scorers)
393393
return [{**s, "weight": s["weight"] / total_weight} for s in self._scorers]
394394

395-
def _build_environment_params(self) -> ScenarioEnvironmentParam:
395+
def _build_environment_params(self) -> Optional[ScenarioEnvironmentParam]:
396396
"""Build environment parameters"""
397-
env_params: ScenarioEnvironmentParam = {}
398-
if self._blueprint:
399-
env_params["blueprint_id"] = self._blueprint.id
400-
if self._snapshot:
401-
env_params["snapshot_id"] = self._snapshot.id
402-
if self._working_directory:
403-
env_params["working_directory"] = self._working_directory
404-
return env_params
397+
if not self._blueprint and not self._snapshot and not self._working_directory:
398+
return None
399+
return {
400+
"blueprint_id": self._blueprint.id if self._blueprint else None,
401+
"snapshot_id": self._snapshot.id if self._snapshot else None,
402+
"working_directory": self._working_directory if self._working_directory else None,
403+
}
405404

406405
def _build_params(self) -> ScenarioCreateParams:
407406
"""Build the scenario creation parameters.

tests/sdk/test_async_scenario_builder.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from runloop_api_client.sdk import AsyncSnapshot, AsyncBlueprint, AsyncScenarioBuilder
9+
from runloop_api_client.sdk import AsyncSnapshot, AsyncBlueprint, ScenarioPreview, AsyncScenarioBuilder
1010
from runloop_api_client.types.scoring_function_param import ScorerTestBasedScoringFunctionTestFile
1111

1212

@@ -230,3 +230,65 @@ def test_fluent_chaining(self, mock_builder: AsyncScenarioBuilder, mock_blueprin
230230
assert mock_builder._working_directory == "/app"
231231
assert mock_builder._problem_statement == "Fix the bug"
232232
assert len(mock_builder._scorers) == 1
233+
234+
def test_preview_with_no_config(self, mock_builder: AsyncScenarioBuilder) -> None:
235+
"""Test preview() works with no configuration (only name from constructor)."""
236+
preview = mock_builder.preview()
237+
238+
assert isinstance(preview, ScenarioPreview)
239+
assert preview.name == "test-scenario"
240+
assert preview.input_context is not None
241+
assert preview.input_context.problem_statement is None
242+
assert preview.input_context.additional_context is None
243+
assert preview.scoring_contract is not None
244+
assert len(preview.scoring_contract.scoring_function_parameters) == 0
245+
assert preview.environment is None
246+
assert len(preview.metadata) == 0
247+
assert preview.reference_output is None
248+
assert preview.required_environment_variables is None
249+
assert preview.required_secret_names is None
250+
assert preview.validation_type is None
251+
252+
def test_preview_with_full_config(self, mock_builder: AsyncScenarioBuilder, mock_blueprint: AsyncBlueprint) -> None:
253+
"""Test preview() with all fields configured, including weight normalization."""
254+
mock_builder.with_problem_statement("Fix the bug")
255+
mock_builder.with_additional_context({"hint": "line 42"})
256+
mock_builder.from_blueprint(mock_blueprint)
257+
mock_builder.with_working_directory("/app")
258+
mock_builder.with_metadata({"team": "infra"})
259+
mock_builder.with_reference_output("diff content")
260+
mock_builder.with_required_env_vars(["API_KEY"])
261+
mock_builder.with_required_secrets(["db_pass"])
262+
mock_builder.with_validation_type("FORWARD")
263+
# Add multiple scorers with different weights to test normalization
264+
mock_builder.add_bash_script_scorer("scorer1", bash_script="echo 1", weight=1.0)
265+
mock_builder.add_bash_script_scorer("scorer2", bash_script="echo 2", weight=2.0)
266+
mock_builder.add_bash_script_scorer("scorer3", bash_script="echo 3", weight=3.0)
267+
268+
preview = mock_builder.preview()
269+
270+
# Verify it returns ScenarioPreview
271+
assert isinstance(preview, ScenarioPreview)
272+
273+
# Verify all fields are populated
274+
assert preview.name == "test-scenario"
275+
assert preview.input_context is not None
276+
assert preview.input_context.problem_statement == "Fix the bug"
277+
assert preview.input_context.additional_context == {"hint": "line 42"}
278+
assert preview.environment is not None
279+
assert preview.environment.blueprint_id == "bp-123"
280+
assert preview.environment.working_directory == "/app"
281+
assert preview.metadata == {"team": "infra"}
282+
assert preview.reference_output == "diff content"
283+
assert preview.required_environment_variables == ["API_KEY"]
284+
assert preview.required_secret_names == ["db_pass"]
285+
assert preview.validation_type == "FORWARD"
286+
287+
# Verify weights are normalized (1, 2, 3 -> 1/6, 2/6, 3/6)
288+
assert preview.scoring_contract is not None
289+
scorers = preview.scoring_contract.scoring_function_parameters
290+
assert len(scorers) == 3
291+
assert abs(scorers[0].weight - 1 / 6) < 0.0001
292+
assert abs(scorers[1].weight - 2 / 6) < 0.0001
293+
assert abs(scorers[2].weight - 3 / 6) < 0.0001
294+
assert abs(sum(s.weight for s in scorers) - 1.0) < 0.0001

tests/sdk/test_scenario_builder.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from runloop_api_client.sdk import Snapshot, Blueprint, ScenarioBuilder
9+
from runloop_api_client.sdk import Snapshot, Blueprint, ScenarioBuilder, ScenarioPreview
1010
from runloop_api_client.types.scoring_function_param import ScorerTestBasedScoringFunctionTestFile
1111

1212

@@ -223,3 +223,65 @@ def test_fluent_chaining(self, mock_builder: ScenarioBuilder, mock_blueprint: Bl
223223
assert mock_builder._working_directory == "/app"
224224
assert mock_builder._problem_statement == "Fix the bug"
225225
assert len(mock_builder._scorers) == 1
226+
227+
def test_preview_with_no_config(self, mock_builder: ScenarioBuilder) -> None:
228+
"""Test preview() works with no configuration (only name from constructor)."""
229+
preview = mock_builder.preview()
230+
231+
assert isinstance(preview, ScenarioPreview)
232+
assert preview.name == "test-scenario"
233+
assert preview.input_context is not None
234+
assert preview.input_context.problem_statement is None
235+
assert preview.input_context.additional_context is None
236+
assert preview.scoring_contract is not None
237+
assert len(preview.scoring_contract.scoring_function_parameters) == 0
238+
assert preview.environment is None
239+
assert len(preview.metadata) == 0
240+
assert preview.reference_output is None
241+
assert preview.required_environment_variables is None
242+
assert preview.required_secret_names is None
243+
assert preview.validation_type is None
244+
245+
def test_preview_with_full_config(self, mock_builder: ScenarioBuilder, mock_blueprint: Blueprint) -> None:
246+
"""Test preview() with all fields configured, including weight normalization."""
247+
mock_builder.with_problem_statement("Fix the bug")
248+
mock_builder.with_additional_context({"hint": "line 42"})
249+
mock_builder.from_blueprint(mock_blueprint)
250+
mock_builder.with_working_directory("/app")
251+
mock_builder.with_metadata({"team": "infra"})
252+
mock_builder.with_reference_output("diff content")
253+
mock_builder.with_required_env_vars(["API_KEY"])
254+
mock_builder.with_required_secrets(["db_pass"])
255+
mock_builder.with_validation_type("FORWARD")
256+
# Add multiple scorers with different weights to test normalization
257+
mock_builder.add_bash_script_scorer("scorer1", bash_script="echo 1", weight=1.0)
258+
mock_builder.add_bash_script_scorer("scorer2", bash_script="echo 2", weight=2.0)
259+
mock_builder.add_bash_script_scorer("scorer3", bash_script="echo 3", weight=3.0)
260+
261+
preview = mock_builder.preview()
262+
263+
# Verify it returns ScenarioPreview
264+
assert isinstance(preview, ScenarioPreview)
265+
266+
# Verify all fields are populated
267+
assert preview.name == "test-scenario"
268+
assert preview.input_context is not None
269+
assert preview.input_context.problem_statement == "Fix the bug"
270+
assert preview.input_context.additional_context == {"hint": "line 42"}
271+
assert preview.environment is not None
272+
assert preview.environment.blueprint_id == "bp-123"
273+
assert preview.environment.working_directory == "/app"
274+
assert preview.metadata == {"team": "infra"}
275+
assert preview.reference_output == "diff content"
276+
assert preview.required_environment_variables == ["API_KEY"]
277+
assert preview.required_secret_names == ["db_pass"]
278+
assert preview.validation_type == "FORWARD"
279+
280+
# Verify weights are normalized (1, 2, 3 -> 1/6, 2/6, 3/6)
281+
assert preview.scoring_contract is not None
282+
scorers = preview.scoring_contract.scoring_function_parameters
283+
assert len(scorers) == 3
284+
assert abs(scorers[0].weight - 1 / 6) < 0.0001
285+
assert abs(scorers[1].weight - 2 / 6) < 0.0001
286+
assert abs(scorers[2].weight - 3 / 6) < 0.0001
287+
assert abs(sum(s.weight for s in scorers) - 1.0) < 0.0001

0 commit comments

Comments
 (0)