|
8 | 8 | from gym.spaces import Box |
9 | 9 |
|
10 | 10 | from predicators import utils |
| 11 | +from predicators.agent_sdk.bilevel_sketch import SketchStep |
11 | 12 | from predicators.agent_sdk.tools import ToolContext |
12 | 13 | from predicators.explorers import create_explorer |
13 | 14 | from predicators.explorers.agent_bilevel_explorer import AgentBilevelExplorer |
@@ -302,6 +303,115 @@ def test_plan_truncates_at_deepest_subgoal_failure_after_backtracking(): |
302 | 303 | f"giving up, got {executed_names}") |
303 | 304 |
|
304 | 305 |
|
| 306 | +def _make_captured(pick_params, place_params): |
| 307 | + """Build the (solved_plan, solved_sketch) a tool capture would stash.""" |
| 308 | + grounded_plan = [ |
| 309 | + _Pick.ground([_block0], np.array(pick_params, dtype=np.float32)), |
| 310 | + _Place.ground([_block0, _block1], |
| 311 | + np.array(place_params, dtype=np.float32)), |
| 312 | + ] |
| 313 | + captured_sketch = [ |
| 314 | + SketchStep(option=_Pick, objects=[_block0], subgoal_atoms=None), |
| 315 | + SketchStep(option=_Place, |
| 316 | + objects=[_block0, _block1], |
| 317 | + subgoal_atoms={GroundAtom(_On, [_block0, _block1])}), |
| 318 | + ] |
| 319 | + return grounded_plan, captured_sketch |
| 320 | + |
| 321 | + |
| 322 | +def test_recovers_captured_plan_when_final_text_unparseable(): |
| 323 | + """Agent validates a plan via evaluate_option_plan but ends in prose: |
| 324 | +
|
| 325 | + explorer recovers the captured plan instead of falling back to |
| 326 | + random, and seeds the captured continuous params into refinement. |
| 327 | + """ |
| 328 | + _reset_config() |
| 329 | + |
| 330 | + goal_state = _make_state({_block0: [0.5, 0.6, 0.0]}) |
| 331 | + option_model = MagicMock() |
| 332 | + option_model.get_next_state_and_num_actions.return_value = (goal_state, 3) |
| 333 | + |
| 334 | + pick_params, place_params = [0.42], [0.11, 0.22] |
| 335 | + grounded_plan, captured_sketch = _make_captured(pick_params, place_params) |
| 336 | + |
| 337 | + explorer, tool_context = _make_explorer(option_model, None) |
| 338 | + |
| 339 | + async def query_impl(_msg, **_kw): |
| 340 | + # Simulate the agent capturing a validated plan via the tool during |
| 341 | + # the query (set AFTER the explorer's entry-time capture clear), then |
| 342 | + # ending with prose that does NOT parse into a sketch. |
| 343 | + tool_context.solved_plan = grounded_plan |
| 344 | + tool_context.solved_sketch = captured_sketch |
| 345 | + return _assistant_response("Solved it. Plan: 1. pick 2. place. Done.") |
| 346 | + |
| 347 | + explorer._agent_session.query = query_impl |
| 348 | + |
| 349 | + policy, term_fn = explorer._get_exploration_strategy(0, timeout=5) |
| 350 | + |
| 351 | + # Recovered (not random fallback): subgoals/options come from the capture. |
| 352 | + assert callable(policy) |
| 353 | + assert term_fn(_make_state()) is False |
| 354 | + assert tool_context.last_sketch_options == [ |
| 355 | + ("Pick", ["block0"]), |
| 356 | + ("Place", ["block0", "block1"]), |
| 357 | + ] |
| 358 | + # The capture was consumed (cleared) so it can't leak into a later solve. |
| 359 | + assert tool_context.solved_plan is None |
| 360 | + assert tool_context.solved_sketch is None |
| 361 | + # Captured params were seeded as initial_params: the option model is |
| 362 | + # invoked with them (Pick tries them first; Place's are pooled). |
| 363 | + called = [ |
| 364 | + c.args[1] |
| 365 | + for c in option_model.get_next_state_and_num_actions.call_args_list |
| 366 | + ] |
| 367 | + pick_calls = [o for o in called if o.name == "Pick"] |
| 368 | + place_calls = [o for o in called if o.name == "Place"] |
| 369 | + assert pick_calls and place_calls |
| 370 | + np.testing.assert_allclose(pick_calls[0].params, pick_params) |
| 371 | + assert any( |
| 372 | + np.allclose(o.params, place_params) for o in place_calls), \ |
| 373 | + "captured Place params were not seeded into refinement" |
| 374 | + |
| 375 | + |
| 376 | +def test_captured_params_seed_info_gain_search(): |
| 377 | + """With info-seeking ON, the recovered capture's continuous params are |
| 378 | + seeded as candidates in the info-gain pool (not replayed verbatim).""" |
| 379 | + _reset_config(agent_explorer_info_seeking=True, |
| 380 | + agent_explorer_info_n_feasible_target=2, |
| 381 | + agent_bilevel_explorer_max_samples_per_step=4) |
| 382 | + |
| 383 | + goal_state = _make_state({_block0: [0.5, 0.6, 0.0]}) |
| 384 | + option_model = MagicMock() |
| 385 | + option_model.get_next_state_and_num_actions.return_value = (goal_state, 3) |
| 386 | + |
| 387 | + place_params = [0.33, 0.44] |
| 388 | + grounded_plan, captured_sketch = _make_captured([0.42], place_params) |
| 389 | + |
| 390 | + explorer, tool_context = _make_explorer(option_model, None) |
| 391 | + # Wire a trivial ensemble scorer so info-seeking engages on annotated |
| 392 | + # steps; constant score means the seeded candidate is chosen. |
| 393 | + tool_context.atom_disagreement_fn = lambda _s, _atoms: 0.0 |
| 394 | + |
| 395 | + async def query_impl(_msg, **_kw): |
| 396 | + tool_context.solved_plan = grounded_plan |
| 397 | + tool_context.solved_sketch = captured_sketch |
| 398 | + return _assistant_response("Done — summary only, no sketch block.") |
| 399 | + |
| 400 | + explorer._agent_session.query = query_impl |
| 401 | + |
| 402 | + policy, _ = explorer._get_exploration_strategy(0, timeout=5) |
| 403 | + assert callable(policy) |
| 404 | + # Place is the subgoal-annotated step that info-seeking pools; its captured |
| 405 | + # params must appear among the candidates the pool evaluated. |
| 406 | + place_calls = [ |
| 407 | + c.args[1] |
| 408 | + for c in option_model.get_next_state_and_num_actions.call_args_list |
| 409 | + if c.args[1].name == "Place" |
| 410 | + ] |
| 411 | + assert any(np.allclose(o.params, place_params) for o in place_calls), \ |
| 412 | + "captured Place params were not seeded into the info-gain pool" |
| 413 | + |
| 414 | + |
305 | 415 | def test_fallback_when_query_fails_and_flag_on(): |
306 | 416 | """Agent raises → random options fallback when flag enabled.""" |
307 | 417 | _reset_config(agent_explorer_fallback_to_random=True) |
|
0 commit comments