Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions dspy/predict/react.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,16 @@ def _call_with_potential_trajectory_truncation(self, module, trajectory, **input
)
except ContextWindowExceededError:
logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
trajectory = self.truncate_trajectory(trajectory)
try:
trajectory = self.truncate_trajectory(trajectory)
except ValueError:
# Cannot truncate further, raise a clear error
raise ValueError(
"The context window was exceeded and the trajectory could not be truncated further."
) from None
raise ValueError(
"The context window was exceeded even after 3 attempts to truncate the trajectory."
)

async def _async_call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
for _ in range(3):
Expand All @@ -163,7 +172,16 @@ async def _async_call_with_potential_trajectory_truncation(self, module, traject
)
except ContextWindowExceededError:
logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
trajectory = self.truncate_trajectory(trajectory)
try:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot We don't need this try-catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the try-catch blocks in both sync and async methods. The ValueError from truncate_trajectory now naturally propagates up and is caught by forward(). Fixed in 2dd54fc.

trajectory = self.truncate_trajectory(trajectory)
except ValueError:
# Cannot truncate further, raise a clear error
raise ValueError(
"The context window was exceeded and the trajectory could not be truncated further."
) from None
raise ValueError(
"The context window was exceeded even after 3 attempts to truncate the trajectory."
)

def truncate_trajectory(self, trajectory):
"""Truncates the trajectory so that it fits in the context window.
Expand Down
62 changes: 62 additions & 0 deletions tests/predict/test_react.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,68 @@ def mock_react(**kwargs):
assert result.output_text == "Final output"


def test_context_window_exceeded_after_retries():
Copy link
Collaborator

@TomeHirata TomeHirata Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can we combine two tests by parameterizing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined the sync and async tests into a single test function. Fixed in e95c2dd.

"""Test that context window errors are handled gracefully after exhausting retry attempts.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we don't need docstring in tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the docstrings from both test functions. Fixed in fa8a09b.


This tests the fix for the bug where returning None after exhausting retries
caused "'NoneType' object has no attribute 'next_thought'" error.

The fix raises a ValueError instead, which is caught in forward() and causes
the loop to break gracefully.
"""

def echo(text: str) -> str:
return f"Echoed: {text}"

react = dspy.ReAct("input_text -> output_text", tools=[echo])

# Always raise context window exceeded - simulating case where prompt is too large
# even on the very first call with empty trajectory
def mock_react(**kwargs):
raise litellm.ContextWindowExceededError("Context window exceeded", "dummy_model", "dummy_provider")

react.react = mock_react
react.extract = lambda **kwargs: dspy.Prediction(output_text="Fallback output")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can we verify what input is passed to extract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added verification that extract receives the correct inputs (input_text and trajectory). The test now captures the kwargs passed to extract and asserts they contain the expected values. Fixed in 18e999f.


# Call forward - should handle the error gracefully by logging and breaking the loop
# This should NOT raise AttributeError: 'NoneType' object has no attribute 'next_thought'
result = react(input_text="test input")

# The trajectory should be empty since the first call failed
assert result.trajectory == {}
# Extract should still be called and produce output
assert result.output_text == "Fallback output"


@pytest.mark.asyncio
async def test_async_context_window_exceeded_after_retries():
"""Test that context window errors are handled gracefully after exhausting retry attempts in async mode."""

async def echo(text: str) -> str:
return f"Echoed: {text}"

react = dspy.ReAct("input_text -> output_text", tools=[echo])

# Always raise context window exceeded
async def mock_react(**kwargs):
raise litellm.ContextWindowExceededError("Context window exceeded", "dummy_model", "dummy_provider")

async def mock_extract(**kwargs):
return dspy.Prediction(output_text="Fallback output")

react.react.acall = mock_react
react.extract.acall = mock_extract

# Call forward - should handle the error gracefully
# This should NOT raise AttributeError: 'NoneType' object has no attribute 'next_thought'
result = await react.acall(input_text="test input")

# The trajectory should be empty since the first call failed
assert result.trajectory == {}
# Extract should still be called and produce output
assert result.output_text == "Fallback output"


def test_error_retry():
# --- a tiny tool that always fails -------------------------------------
def foo(a, b):
Expand Down