Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
46 changes: 45 additions & 1 deletion instructor/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
from __future__ import annotations

from textwrap import dedent
from typing import Any, NamedTuple
from jinja2 import Template


class InstructorError(Exception):
"""Base exception for all Instructor-specific errors."""

pass
failed_attempts: list[FailedAttempt] | None = None

@classmethod
def from_exception(
cls, exception: Exception, failed_attempts: list[FailedAttempt] | None = None
):
return cls(exception, failed_attempts=failed_attempts) # type: ignore

def __init__(
self,
*args: list[Any],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMPROVEMENT: The type hint for *args is declared as 'list[Any]' but should be more generic (e.g. '*args: Any'). This ensures proper typing of arbitrary positional arguments.

Suggested change
*args: list[Any],
*args: Any,

failed_attempts: list[FailedAttempt] | None = None,
**kwargs: dict[str, Any],
):
self.failed_attempts = failed_attempts
super().__init__(*args, **kwargs)

def __str__(self) -> str:
template = Template(
dedent(
"""
<failed_attempts>
{% for attempt in failed_attempts %}
<generation number="{{ attempt.attempt_number }}">
<exception>
{{ attempt.exception }}
</exception>
<completion>
{{ attempt.completion }}
</completion>
</generation>
{% endfor %}
</failed_attempts>

<last_exception>
{{ exception }}
</last_exception>
"""
)
)
return template.render(
exception=self.exception, failed_attempts=self.failed_attempts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

BUG: str references 'self.exception' but the constructor (init) never assigns it. This can lead to an AttributeError. Consider assigning 'self.exception = args[0]' in init.

)


class FailedAttempt(NamedTuple):
Expand Down
2 changes: 2 additions & 0 deletions instructor/core/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def retry_sync(
mode=mode,
response=response,
exception=e,
failed_attempts=failed_attempts,
)
raise e
except Exception as e:
Expand Down Expand Up @@ -391,6 +392,7 @@ async def retry_async(
mode=mode,
response=response,
exception=e,
failed_attempts=failed_attempts,
)
raise e
except Exception as e:
Expand Down
122 changes: 91 additions & 31 deletions instructor/processing/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class User(BaseModel):
from pydantic import BaseModel
from typing_extensions import ParamSpec

from instructor.core.exceptions import InstructorError

from ..dsl.iterable import IterableBase
from ..dsl.parallel import ParallelBase
from ..dsl.partial import PartialBase
Expand Down Expand Up @@ -491,55 +493,113 @@ def handle_reask_kwargs(
mode: Mode,
response: Any,
exception: Exception,
failed_attempts: list[Any] | None = None,
) -> dict[str, Any]:
"""Handle validation errors by reformatting the request for retry (reask).

When a response fails validation (e.g., missing required fields, wrong types),
this function prepares a new request that includes information about the error.
This allows the LLM to understand what went wrong and correct its response.
This function serves as the central dispatcher for handling validation failures
across all supported LLM providers. When a response fails validation, it prepares
a new request that includes detailed error information and retry context, allowing
the LLM to understand what went wrong and generate a corrected response.

The reask logic is provider-specific because each provider has different ways
of handling function/tool calls and different message formats.
The reask process involves:
1. Analyzing the validation error and failed response
2. Selecting the appropriate provider-specific reask handler
3. Enriching the exception with retry history (failed_attempts)
4. Formatting error feedback in the provider's expected message format
5. Preserving original request parameters while adding retry context

Args:
kwargs (dict[str, Any]): The original request parameters that resulted in
a validation error. Includes messages, tools, temperature, etc.
a validation error. Contains all parameters passed to the LLM API:
- messages: conversation history
- tools/functions: available function definitions
- temperature, max_tokens: generation parameters
- model, provider-specific settings
mode (Mode): The provider/format mode that determines which reask handler
to use. Each mode has a specific strategy for formatting error feedback.
to use. Each mode implements a specific strategy for formatting error
feedback and retry messages. Examples:
- Mode.TOOLS: OpenAI function calling
- Mode.ANTHROPIC_TOOLS: Anthropic tool use
- Mode.JSON: JSON-only responses
response (Any): The raw response from the LLM that failed validation.
Type varies by provider:
- OpenAI: ChatCompletion with tool_calls
- Anthropic: Message with tool_use blocks
Type and structure varies by provider:
- OpenAI: ChatCompletion with tool_calls or content
- Anthropic: Message with tool_use blocks or text content
- Google: GenerateContentResponse with function calls
exception (Exception): The validation error that occurred. Usually a
Pydantic ValidationError with details about which fields failed.
- Cohere: NonStreamedChatResponse with tool calls
exception (Exception): The validation error that occurred, typically:
- Pydantic ValidationError: field validation failures
- JSONDecodeError: malformed JSON responses
- Custom validation errors from response processors
The exception will be enriched with failed_attempts data.
failed_attempts (list[FailedAttempt] | None): Historical record of previous
retry attempts for this request. Each FailedAttempt contains:
- attempt_number: sequential attempt counter
- exception: the validation error for that attempt
- completion: the raw LLM response that failed
Used to provide retry context and prevent repeated mistakes.

Returns:
dict[str, Any]: Modified kwargs for the retry request, typically including:
- Updated messages with error context
- Same tool/function definitions
- Preserved generation parameters
- Provider-specific formatting

Reask Strategies by Provider:
Each provider has a specific strategy for handling retries:

**JSON Modes:**
- Adds assistant message with failed attempt
- Adds user message with error details

**Tool Calls:**
- Preserves tool definitions
- Formats the errors as tool calls responses
dict[str, Any]: Modified kwargs for the retry request with:
- Updated messages including error feedback
- Original tool/function definitions preserved
- Generation parameters maintained (temperature, etc.)
- Provider-specific error formatting applied
- Retry context embedded in appropriate message format

Provider-Specific Reask Strategies:
**OpenAI Modes:**
- TOOLS/FUNCTIONS: Adds tool response messages with validation errors
- JSON modes: Appends user message with correction instructions
- Preserves function schemas and conversation context

**Anthropic Modes:**
- TOOLS: Creates tool_result blocks with error details
- JSON: Adds user message with structured error feedback
- Maintains conversation flow with proper message roles

**Google/Gemini Modes:**
- TOOLS: Formats as function response with error content
- JSON: Appends user message with validation feedback

**Other Providers (Cohere, Mistral, etc.):**
- Provider-specific message formatting
- Consistent error reporting patterns
- Maintained conversation context

Error Enrichment:
The exception parameter is enriched with retry metadata:
- exception.failed_attempts: list of previous failures
- exception.retry_attempt_number: current attempt number
This allows downstream handlers to access full retry context.

Example:
```python
# After a ValidationError occurs during retry attempt #2
new_kwargs = handle_reask_kwargs(
kwargs=original_request,
mode=Mode.TOOLS,
response=failed_completion,
exception=validation_error, # Will be enriched with failed_attempts
failed_attempts=[attempt1, attempt2] # Previous failures
)
# new_kwargs now contains retry messages with error context
```

Note:
This function is typically called internally by the retry logic when
max_retries > 1. It ensures that each retry attempt includes context
about previous failures, helping the LLM learn from its mistakes.
This function is called internally by retry_sync() and retry_async()
when max_retries > 1. It ensures each retry includes progressively
more context about previous failures, helping the LLM learn from
mistakes and avoid repeating the same errors.
"""
# Create a shallow copy of kwargs to avoid modifying the original
kwargs_copy = kwargs.copy()

exception = InstructorError.from_exception(
exception, failed_attempts=failed_attempts
)

# Organized by provider (matching process_response.py structure)
REASK_HANDLERS = {
# OpenAI modes
Expand Down
6 changes: 6 additions & 0 deletions instructor/providers/openai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def reask_tools(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
failed_attempts: list[Any] | None = None, # noqa: ARG001
):
"""
Handle reask for OpenAI tools mode when validation fails.
Expand Down Expand Up @@ -51,6 +52,7 @@ def reask_responses_tools(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
failed_attempts: list[Any] | None = None, # noqa: ARG001
):
"""
Handle reask for OpenAI responses tools mode when validation fails.
Expand Down Expand Up @@ -79,6 +81,7 @@ def reask_md_json(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
failed_attempts: list[Any] | None = None, # noqa: ARG001
):
"""
Handle reask for OpenAI JSON modes when validation fails.
Expand All @@ -88,6 +91,7 @@ def reask_md_json(
"""
kwargs = kwargs.copy()
reask_msgs = [dump_message(response.choices[0].message)]

reask_msgs.append(
{
"role": "user",
Expand All @@ -102,6 +106,7 @@ def reask_default(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
failed_attempts: list[Any] | None = None, # noqa: ARG001
):
"""
Handle reask for OpenAI default mode when validation fails.
Expand All @@ -111,6 +116,7 @@ def reask_default(
"""
kwargs = kwargs.copy()
reask_msgs = [dump_message(response.choices[0].message)]

reask_msgs.append(
{
"role": "user",
Expand Down
Loading