Skip to content

Commit d92a47b

Browse files
ampagentIsaac Miller
andcommitted
Add interpreter execution instructions
Amp-Thread-ID: https://ampcode.com/threads/T-019fcce1-bc8b-753e-886f-9b77c0ccb376 Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
1 parent 80b118e commit d92a47b

4 files changed

Lines changed: 67 additions & 2 deletions

File tree

‎dspy/predict/rlm.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,14 @@ def _execute_code(
658658
except (CodeExecutionError, SyntaxError) as e:
659659
return f"[Error] {format_error_for_lm(e)}"
660660

661+
def _action_signature(self, repl: CodeInterpreter) -> Any:
662+
instructions = getattr(repl, "execution_instructions", "")
663+
if not instructions or type(self.generate_action) is not dspy.Predict:
664+
return None
665+
return self.generate_action.signature.append_instructions(
666+
"Code interpreter execution environment:\n" + instructions
667+
)
668+
661669
def _execute_iteration(
662670
self,
663671
repl: CodeInterpreter,
@@ -666,13 +674,15 @@ def _execute_iteration(
666674
iteration: int,
667675
input_args: dict[str, Any],
668676
output_field_names: list[str],
677+
action_signature: Any = None,
669678
) -> Prediction | REPLHistory:
670679
"""Execute one iteration. Returns Prediction if done, else updated REPLHistory."""
671680
variables_info = [variable.format() for variable in variables]
672681
action = self.generate_action(
673682
variables_info=variables_info,
674683
repl_history=history,
675684
iteration=f"{iteration + 1}/{self.max_iters}",
685+
**({"signature": action_signature} if action_signature else {}),
676686
)
677687
if self.verbose:
678688
logger.info(
@@ -716,12 +726,13 @@ def forward(self, interpreter: CodeInterpreter | None = None, /, **input_args) -
716726
variables = self._build_variables(**input_args)
717727

718728
with self._interpreter_context(execution_tools, interpreter) as repl:
729+
action_signature = self._action_signature(repl)
719730
regular_args = self._prepare_serializable_vars(input_args, repl)
720731
history: REPLHistory = REPLHistory(max_output_chars=self.max_output_chars)
721732

722733
for iteration in range(self.max_iters):
723734
result: Prediction | REPLHistory = self._execute_iteration(
724-
repl, variables, history, iteration, regular_args, output_field_names
735+
repl, variables, history, iteration, regular_args, output_field_names, action_signature
725736
)
726737
if isinstance(result, Prediction):
727738
return result
@@ -759,13 +770,15 @@ async def _aexecute_iteration(
759770
iteration: int,
760771
input_args: dict[str, Any],
761772
output_field_names: list[str],
773+
action_signature: Any = None,
762774
) -> Prediction | REPLHistory:
763775
"""Async version: Execute one iteration."""
764776
variables_info = [variable.format() for variable in variables]
765777
pred = await self.generate_action.acall(
766778
variables_info=variables_info,
767779
repl_history=history,
768780
iteration=f"{iteration + 1}/{self.max_iters}",
781+
**({"signature": action_signature} if action_signature else {}),
769782
)
770783
if self.verbose:
771784
logger.info(
@@ -805,12 +818,13 @@ async def aforward(self, interpreter: CodeInterpreter | None = None, /, **input_
805818
variables = self._build_variables(**input_args)
806819

807820
with self._interpreter_context(execution_tools, interpreter) as repl:
821+
action_signature = self._action_signature(repl)
808822
regular_args = self._prepare_serializable_vars(input_args, repl)
809823
history = REPLHistory(max_output_chars=self.max_output_chars)
810824

811825
for iteration in range(self.max_iters):
812826
result = await self._aexecute_iteration(
813-
repl, variables, history, iteration, regular_args, output_field_names
827+
repl, variables, history, iteration, regular_args, output_field_names, action_signature
814828
)
815829
if isinstance(result, Prediction):
816830
return result

‎dspy/primitives/python_interpreter.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ def __init__(
343343
self._pending_large_vars = {}
344344
self._session_ended = False
345345

346+
@property
347+
def execution_instructions(self) -> str:
348+
"""Describe stable constraints of this execution environment."""
349+
return (
350+
"Python runs in Pyodide/WebAssembly. State persists across executions, but subprocesses and native "
351+
"extensions are unavailable. Filesystem, environment, and network access require explicit permission."
352+
)
353+
346354
def _check_session_active(self) -> None:
347355
if self._session_ended:
348356
raise CodeInterpreterError(

‎tests/predict/test_rlm.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,40 @@ def make_call():
426426

427427

428428
class TestRLMInterpreterLifecycle:
429+
def test_execution_instructions_are_invocation_local(self):
430+
interpreter = MockInterpreter(responses=[FinalOutput({"answer": "42"})])
431+
interpreter.execution_instructions = "Use this runtime."
432+
rlm = RLM("query -> answer", max_iters=1)
433+
original = rlm.generate_action.signature
434+
calls = []
435+
rlm.generate_action.forward = lambda **kwargs: calls.append(kwargs) or Prediction(
436+
reasoning="done", code="SUBMIT(answer='42')"
437+
)
438+
439+
assert rlm(interpreter, query="test").answer == "42"
440+
assert calls[0]["signature"].instructions.endswith(
441+
"Code interpreter execution environment:\nUse this runtime."
442+
)
443+
assert rlm.generate_action.signature is original
444+
445+
@pytest.mark.asyncio
446+
async def test_custom_action_does_not_receive_signature_override(self):
447+
class Action:
448+
def __call__(self, *, variables_info, repl_history, iteration):
449+
return Prediction(reasoning="done", code="SUBMIT(answer='42')")
450+
451+
async def acall(self, *, variables_info, repl_history, iteration):
452+
return self(variables_info=variables_info, repl_history=repl_history, iteration=iteration)
453+
454+
rlm = RLM("query -> answer", max_iters=1)
455+
rlm.generate_action = Action()
456+
sync = MockInterpreter(responses=[FinalOutput({"answer": "42"})])
457+
async_interpreter = MockInterpreter(responses=[FinalOutput({"answer": "42"})])
458+
sync.execution_instructions = async_interpreter.execution_instructions = "runtime"
459+
460+
assert rlm(sync, query="test").answer == "42"
461+
assert (await rlm.acall(async_interpreter, query="test")).answer == "42"
462+
429463
def test_interpreter_remains_available_as_signature_input(self):
430464
factory = MockInterpreterFactory(responses=[FinalOutput({"answer": "CPython"})])
431465
rlm = RLM("interpreter -> answer", max_iters=1, interpreter_factory=factory)

‎tests/primitives/test_python_interpreter.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,3 +1623,12 @@ def test_base_exceptions_do_not_desync_interpreter():
16231623
with pytest.raises(CodeExecutionError, match=error_type):
16241624
interpreter.execute(code)
16251625
assert interpreter.execute("print('still alive')") == "still alive\n"
1626+
1627+
1628+
def test_execution_instructions_are_stable_and_read_only():
1629+
interpreter = PythonInterpreter(deno_command=["deno", "run"])
1630+
1631+
assert interpreter.execution_instructions == interpreter.execution_instructions
1632+
assert "Pyodide" in interpreter.execution_instructions
1633+
with pytest.raises(AttributeError):
1634+
interpreter.execution_instructions = "replacement"

0 commit comments

Comments
 (0)