-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Ask model to try again if it produced a response without text or tool call parts (e.g. only thinking) #2469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -143,6 +143,24 @@ def is_agent_node( | |||||||||||||||||||||||
return isinstance(node, AgentNode) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
def _create_thinking_retry_request(parts: list[_messages.ModelResponsePart]) -> _messages.ModelRequest | None: | ||||||||||||||||||||||||
"""Handle thinking-only responses (responses that contain only ThinkingPart instances). | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
This can happen with models that support thinking mode when they don't provide | ||||||||||||||||||||||||
actionable output alongside their thinking content. | ||||||||||||||||||||||||
""" | ||||||||||||||||||||||||
thinking_parts = [p for p in parts if isinstance(p, _messages.ThinkingPart)] | ||||||||||||||||||||||||
if thinking_parts: | ||||||||||||||||||||||||
# Create the retry request using UserPromptPart for API compatibility | ||||||||||||||||||||||||
# We'll use a special content marker to detect this is a thinking retry | ||||||||||||||||||||||||
retry_part = _messages.UserPromptPart( | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use a pydantic-ai/pydantic_ai_slim/pydantic_ai/_agent_graph.py Lines 539 to 541 in 07b8a22
|
||||||||||||||||||||||||
'Based on your thinking above, you MUST now provide ' | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we don't currently send back the thinking parts, the model won't know what this refers to. Can we simplify the message to be something more generic like "Responses without text or tool calls are not permitted." and see if that's enough to get the model to do better? Note that wrapping it in a pydantic-ai/pydantic_ai_slim/pydantic_ai/messages.py Lines 646 to 656 in 07b8a22
|
||||||||||||||||||||||||
'a specific answer or use the available tools to complete the task. ' | ||||||||||||||||||||||||
'Do not respond with only thinking content. Provide actionable output.' | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
return _messages.ModelRequest(parts=[retry_part]) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
@dataclasses.dataclass | ||||||||||||||||||||||||
class UserPromptNode(AgentNode[DepsT, NodeRunEndT]): | ||||||||||||||||||||||||
"""The node that handles the user prompt and instructions.""" | ||||||||||||||||||||||||
|
@@ -435,8 +453,7 @@ async def _run_stream( # noqa: C901 | |||||||||||||||||||||||
) -> AsyncIterator[_messages.HandleResponseEvent]: | ||||||||||||||||||||||||
if self._events_iterator is None: | ||||||||||||||||||||||||
# Ensure that the stream is only run once | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: | ||||||||||||||||||||||||
async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa: C901 | ||||||||||||||||||||||||
texts: list[str] = [] | ||||||||||||||||||||||||
tool_calls: list[_messages.ToolCallPart] = [] | ||||||||||||||||||||||||
for part in self.model_response.parts: | ||||||||||||||||||||||||
|
@@ -482,6 +499,12 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: | |||||||||||||||||||||||
self._next_node = await self._handle_text_response(ctx, last_texts) | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
# If there are no preceding model responses, we prompt the model to try again and provide actionable output. | ||||||||||||||||||||||||
breakpoint() | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove this breakpoint |
||||||||||||||||||||||||
if retry_request := _create_thinking_retry_request(self.model_response.parts): | ||||||||||||||||||||||||
self._next_node = ModelRequestNode[DepsT, NodeRunEndT](request=retry_request) | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
raise exceptions.UnexpectedModelBehavior('Received empty model response') | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
self._events_iterator = _run_stream() | ||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can drop this check and always try to get the model to try again instead of raising a hard error.