Skip to content

Commit a7f448a

Browse files
authored
fix(predict): reject ReActV2 input fields named 'history' or 'tools' (#10464)
ReActV2's inner signature injects internal 'history' and 'tools' input fields, and forward()/aforward() pop 'history' from the caller's kwargs. A user signature declaring an input with either name was silently overwritten (type replaced by dspy.History / list[dspy.Tool]) and the user's value never reached the task. Output fields were already validated in __init__; this adds the matching input-field check. Fixes #10336
1 parent da9211a commit a7f448a

2 files changed

Lines changed: 19 additions & 0 deletions

File tree

‎dspy/predict/react_v2.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
# expansion at the `Prediction(**final_outputs, history=..., termination_reason=...)` call sites.
2121
_RESERVED_PREDICTION_KEYS = frozenset({"history", "termination_reason"})
2222

23+
# Internal input field names ReActV2 injects into its inner Predict signature. A user
24+
# input with one of these names would be silently overwritten by `_make_react_signature`
25+
# and popped out of the caller's kwargs by `forward`/`aforward`, so it must be rejected.
26+
_RESERVED_INPUT_KEYS = frozenset({"history", "tools"})
27+
2328
if TYPE_CHECKING:
2429
from dspy.signatures.signature import Signature
2530

@@ -52,6 +57,14 @@ def __init__(self, signature: type[Signature], tools: list[Callable | Tool], max
5257
"returned Prediction. Rename these output fields on your signature."
5358
)
5459

60+
reserved_inputs = _RESERVED_INPUT_KEYS.intersection(self.signature.input_fields)
61+
if reserved_inputs:
62+
names = ", ".join(f"`{name}`" for name in sorted(reserved_inputs))
63+
raise ValueError(
64+
f"Input field name(s) {names} are reserved by ReActV2 for its internal "
65+
"conversation history and tool list. Rename these input fields on your signature."
66+
)
67+
5568
user_tools = [tool if isinstance(tool, Tool) else Tool(tool) for tool in tools]
5669
self.tools = {tool.name: tool for tool in user_tools}
5770
if "submit" in self.tools:

‎tests/predict/test_react_v2.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ def test_react_v2_rejects_reserved_output_field_names(reserved):
362362
dspy.ReActV2(f"question -> answer, {reserved}: str", tools=[])
363363

364364

365+
@pytest.mark.parametrize("reserved", ["history", "tools"])
366+
def test_react_v2_rejects_reserved_input_field_names(reserved):
367+
with pytest.raises(ValueError, match=reserved):
368+
dspy.ReActV2(f"{reserved}: str -> answer: str", tools=[])
369+
370+
365371
@pytest.mark.asyncio
366372
@pytest.mark.parametrize("use_async", [False, True])
367373
@pytest.mark.parametrize("break_reason", ["max_iters", "empty_tool_calls", "parse_error", "context_window_exceeded"])

0 commit comments

Comments
 (0)