Skip to content

Commit 9789eec

Browse files
fix(scenario): let LLM decide timestep unit and duration from description
Previously, timeline generation hardcoded TimestepUnit.HOUR and derived max_timesteps from population size. The LLM was told "Duration: 100 hours" with no ability to choose the unit, so "7 days" scenarios got hour-based timesteps. Now the LLM schema includes timestep_unit and max_timesteps fields, allowing it to infer the natural unit from the scenario description. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b5da4e4 commit 9789eec

3 files changed

Lines changed: 51 additions & 11 deletions

File tree

extropy/scenario/compiler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def progress(step: str, status: str):
211211
# Generate simulation config
212212
simulation_config = _determine_simulation_config()
213213

214-
timeline_events, background_context = generate_timeline(
214+
timeline_events, background_context, simulation_config = generate_timeline(
215215
scenario_description=description,
216216
base_event=event,
217217
simulation_config=simulation_config,
@@ -342,7 +342,7 @@ def progress(step: str, status: str):
342342

343343
simulation_config = _determine_simulation_config()
344344

345-
timeline_events, background_context = generate_timeline(
345+
timeline_events, background_context, simulation_config = generate_timeline(
346346
scenario_description=description,
347347
base_event=event,
348348
simulation_config=simulation_config,

extropy/scenario/timeline.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any
99

1010
from ..core.llm import reasoning_call
11-
from ..core.models import Event, SimulationConfig, TimelineEvent
11+
from ..core.models import Event, SimulationConfig, TimelineEvent, TimestepUnit
1212

1313
logger = logging.getLogger(__name__)
1414

@@ -23,6 +23,23 @@
2323
"evolving = developments over time (crisis, campaign, adoption)"
2424
),
2525
},
26+
"timestep_unit": {
27+
"type": "string",
28+
"enum": ["minute", "hour", "day"],
29+
"description": (
30+
"The natural time unit for this scenario. Choose based on how "
31+
"the scenario description frames duration: '7 days' → day, "
32+
"'48 hours' → hour, '30 minutes' → minute."
33+
),
34+
},
35+
"max_timesteps": {
36+
"type": "integer",
37+
"minimum": 1,
38+
"description": (
39+
"Total number of timesteps for the simulation. Should match "
40+
"the scenario duration in the chosen unit (e.g. 7 days → 7)."
41+
),
42+
},
2643
"background_context": {
2744
"type": "string",
2845
"description": (
@@ -79,7 +96,7 @@
7996
),
8097
},
8198
},
82-
"required": ["scenario_type", "background_context"],
99+
"required": ["scenario_type", "timestep_unit", "max_timesteps", "background_context"],
83100
}
84101

85102

@@ -107,7 +124,10 @@ def _build_timeline_prompt(
107124
"",
108125
"## Simulation Parameters",
109126
"",
110-
f"Duration: {simulation_config.max_timesteps} {simulation_config.timestep_unit.value}s",
127+
"Determine the appropriate timestep_unit (minute, hour, or day) and max_timesteps "
128+
"based on the scenario description. If the description mentions a specific duration "
129+
"(e.g. '7 days', '48 hours'), use the matching unit and count. "
130+
"Timeline event timesteps must use the same unit.",
111131
"",
112132
"## Your Task",
113133
"",
@@ -174,7 +194,7 @@ def generate_timeline(
174194
base_event: Event,
175195
simulation_config: SimulationConfig,
176196
timeline_mode: str | None = None,
177-
) -> tuple[list[TimelineEvent], str | None]:
197+
) -> tuple[list[TimelineEvent], str | None, SimulationConfig]:
178198
"""Generate timeline events and background context.
179199
180200
Args:
@@ -186,7 +206,7 @@ def generate_timeline(
186206
- "evolving": Multi-event timeline (ASI-style)
187207
188208
Returns:
189-
Tuple of (timeline_events, background_context)
209+
Tuple of (timeline_events, background_context, updated_simulation_config)
190210
timeline_events will be empty for static scenarios
191211
"""
192212
prompt = _build_timeline_prompt(
@@ -206,12 +226,32 @@ def generate_timeline(
206226

207227
if not response:
208228
logger.warning("[TIMELINE] LLM returned empty response, using defaults")
209-
return [], None
229+
return [], None, simulation_config
210230

211231
scenario_type = response.get("scenario_type", "static")
212232
background_context = response.get("background_context")
213233
raw_events = response.get("timeline_events", [])
214234

235+
# Update simulation config with LLM's chosen unit and duration
236+
llm_unit = response.get("timestep_unit")
237+
llm_max = response.get("max_timesteps")
238+
if llm_unit:
239+
unit_map = {"minute": TimestepUnit.MINUTE, "hour": TimestepUnit.HOUR, "day": TimestepUnit.DAY}
240+
resolved_unit = unit_map.get(llm_unit, simulation_config.timestep_unit)
241+
simulation_config = SimulationConfig(
242+
max_timesteps=llm_max if llm_max else simulation_config.max_timesteps,
243+
timestep_unit=resolved_unit,
244+
stop_conditions=simulation_config.stop_conditions,
245+
seed=simulation_config.seed,
246+
)
247+
elif llm_max:
248+
simulation_config = SimulationConfig(
249+
max_timesteps=llm_max,
250+
timestep_unit=simulation_config.timestep_unit,
251+
stop_conditions=simulation_config.stop_conditions,
252+
seed=simulation_config.seed,
253+
)
254+
215255
# Honor explicit mode override
216256
if timeline_mode == "static":
217257
scenario_type = "static"
@@ -255,4 +295,4 @@ def generate_timeline(
255295
# Sort by timestep
256296
timeline_events.sort(key=lambda te: te.timestep)
257297

258-
return timeline_events, background_context
298+
return timeline_events, background_context, simulation_config

tests/test_compiler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def test_creates_valid_scenario(
175175
],
176176
)
177177

178-
mock_timeline.return_value = ([], None) # No timeline events, no background
178+
mock_timeline.return_value = ([], None, _determine_simulation_config(10)) # No timeline events, no background
179179

180180
spec, validation_result = create_scenario(
181181
description="Test product launch scenario",
@@ -257,7 +257,7 @@ def test_progress_callback_called(
257257
],
258258
)
259259

260-
mock_timeline.return_value = ([], None) # No timeline events, no background
260+
mock_timeline.return_value = ([], None, _determine_simulation_config(10)) # No timeline events, no background
261261

262262
progress_calls = []
263263

0 commit comments

Comments
 (0)