참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View File

@@ -0,0 +1,30 @@
"""Processing components for request/response handling."""
from .function_calls import OpenAISchema, openai_schema
from .multimodal import convert_messages
from .response import (
handle_response_model,
process_response,
process_response_async,
handle_reask_kwargs,
)
from .schema import (
generate_openai_schema,
generate_anthropic_schema,
generate_gemini_schema,
)
from .validators import Validator
__all__ = [
"OpenAISchema",
"openai_schema",
"convert_messages",
"handle_response_model",
"process_response",
"process_response_async",
"handle_reask_kwargs",
"generate_openai_schema",
"generate_anthropic_schema",
"generate_gemini_schema",
"Validator",
]

View File

@@ -0,0 +1,816 @@
# type: ignore
import json
import logging
import re
from functools import wraps
from typing import Annotated, Any, Optional, TypeVar, cast
from openai.types.chat import ChatCompletion
from pydantic import (
BaseModel,
ConfigDict,
Field,
TypeAdapter,
create_model,
)
from ..core.exceptions import (
IncompleteOutputException,
ResponseParsingError,
ConfigurationError,
)
from ..mode import Mode
from ..utils import (
classproperty,
extract_json_from_codeblock,
)
from .schema import (
generate_openai_schema,
generate_anthropic_schema,
generate_gemini_schema,
)
T = TypeVar("T")
Model = TypeVar("Model", bound=BaseModel)
logger = logging.getLogger("instructor")
# No schema cache
# Utility functions for common JSON parsing operations
def _handle_incomplete_output(completion: Any) -> None:
"""Check if a completion was incomplete and raise appropriate exception."""
if (
hasattr(completion, "choices")
and completion.choices[0].finish_reason == "length"
):
raise IncompleteOutputException(last_completion=completion)
# Handle Anthropic format
if hasattr(completion, "stop_reason") and completion.stop_reason == "max_tokens":
raise IncompleteOutputException(last_completion=completion)
def _extract_text_content(completion: Any) -> str:
"""Extract text content from various completion formats."""
# OpenAI format
if hasattr(completion, "choices"):
return completion.choices[0].message.content or ""
# Simple text format
if hasattr(completion, "text"):
return completion.text
# Anthropic format
if hasattr(completion, "content"):
text_blocks = [c for c in completion.content if c.type == "text"]
if text_blocks:
return text_blocks[0].text
# Bedrock format
if isinstance(completion, dict) and "output" in completion:
try:
return completion.get("output").get("message").get("content")[0].get("text")
except (AttributeError, IndexError):
pass
return ""
def _validate_model_from_json(
cls: type[Any],
json_str: str,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> Any:
"""Validate model from JSON string with appropriate error handling."""
try:
if hasattr(cls, "model_validate_json"):
if strict:
return cls.model_validate_json(
json_str, context=validation_context, strict=True
)
# Allow control characters
parsed = json.loads(json_str, strict=False)
return cls.model_validate(parsed, context=validation_context, strict=False)
adapter = TypeAdapter(cls)
if strict:
return adapter.validate_json(
json_str, context=validation_context, strict=True
)
parsed = json.loads(json_str, strict=False)
return adapter.validate_python(parsed, context=validation_context, strict=False)
except json.JSONDecodeError as e:
logger.debug(f"JSON decode error: {e}")
raise
except Exception as e:
logger.debug(f"Model validation error: {e}")
raise
class OpenAISchema(BaseModel):
# Ignore classproperty, since Pydantic doesn't understand it like it would a normal property.
model_config = ConfigDict(ignored_types=(classproperty,))
@classproperty
def openai_schema(cls) -> dict[str, Any]:
"""
Return the schema in the format of OpenAI's schema as jsonschema
Note:
Its important to add a docstring to describe how to best use this class, it will be included in the description attribute and be part of the prompt.
Returns:
model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema
"""
return generate_openai_schema(cls)
@classproperty
def anthropic_schema(cls) -> dict[str, Any]:
# Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
return generate_anthropic_schema(cls)
@classproperty
def gemini_schema(cls) -> Any:
# This is kept for backward compatibility but deprecated
return generate_gemini_schema(cls)
@classmethod
def from_response(
cls,
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
mode: Mode = Mode.TOOLS,
) -> BaseModel:
"""Execute the function from the response of an openai chat completion
Parameters:
completion (openai.ChatCompletion): The response from an openai chat completion
strict (bool): Whether to use strict json parsing
mode (Mode): The openai completion mode
Returns:
cls (OpenAISchema): An instance of the class
"""
if mode in {Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_REASONING_TOOLS}:
return cls.parse_anthropic_tools(completion, validation_context, strict)
if mode == Mode.ANTHROPIC_JSON:
return cls.parse_anthropic_json(completion, validation_context, strict)
if mode == Mode.BEDROCK_JSON:
return cls.parse_bedrock_json(completion, validation_context, strict)
if mode == Mode.BEDROCK_TOOLS:
return cls.parse_bedrock_tools(completion, validation_context, strict)
if mode in {Mode.VERTEXAI_TOOLS, Mode.GEMINI_TOOLS}:
return cls.parse_vertexai_tools(completion, validation_context)
if mode == Mode.VERTEXAI_JSON:
return cls.parse_vertexai_json(completion, validation_context, strict)
if mode == Mode.COHERE_TOOLS:
return cls.parse_cohere_tools(completion, validation_context, strict)
if mode == Mode.GEMINI_JSON:
return cls.parse_gemini_json(completion, validation_context, strict)
if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
return cls.parse_genai_structured_outputs(
completion, validation_context, strict
)
if mode == Mode.GEMINI_TOOLS:
return cls.parse_gemini_tools(completion, validation_context, strict)
if mode == Mode.GENAI_TOOLS:
return cls.parse_genai_tools(completion, validation_context, strict)
if mode == Mode.COHERE_JSON_SCHEMA:
return cls.parse_cohere_json_schema(completion, validation_context, strict)
if mode == Mode.WRITER_TOOLS:
return cls.parse_writer_tools(completion, validation_context, strict)
if mode == Mode.WRITER_JSON:
return cls.parse_writer_json(completion, validation_context, strict)
if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
return cls.parse_responses_tools(
completion,
validation_context,
strict,
)
if not completion.choices:
# This helps catch errors from OpenRouter
if hasattr(completion, "error"):
raise ResponseParsingError(
f"LLM provider returned error: {completion.error}",
mode=str(mode),
raw_response=completion,
)
raise ResponseParsingError(
"No completion choices found in LLM response",
mode=str(mode),
raw_response=completion,
)
if completion.choices[0].finish_reason == "length":
raise IncompleteOutputException(last_completion=completion)
if mode == Mode.FUNCTIONS:
Mode.warn_mode_functions_deprecation()
return cls.parse_functions(completion, validation_context, strict)
if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
return cls.parse_mistral_structured_outputs(
completion, validation_context, strict
)
if mode in {
Mode.TOOLS,
Mode.MISTRAL_TOOLS,
Mode.TOOLS_STRICT,
Mode.CEREBRAS_TOOLS,
Mode.FIREWORKS_TOOLS,
}:
return cls.parse_tools(completion, validation_context, strict)
if mode in {
Mode.JSON,
Mode.JSON_SCHEMA,
Mode.MD_JSON,
Mode.JSON_O1,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
Mode.OPENROUTER_STRUCTURED_OUTPUTS,
}:
return cls.parse_json(completion, validation_context, strict)
raise ConfigurationError(
f"Invalid or unsupported mode: {mode}. This mode may not be implemented for response parsing."
)
@classmethod
def parse_genai_structured_outputs(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
from google.genai import types
if (
hasattr(completion, "candidates")
and completion.candidates
and completion.candidates[0].finish_reason == types.FinishReason.MAX_TOKENS
):
raise IncompleteOutputException(last_completion=completion)
return cls.model_validate_json(
completion.text, context=validation_context, strict=strict
)
@classmethod
def parse_genai_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
from google.genai import types
assert isinstance(completion, types.GenerateContentResponse)
assert len(completion.candidates) == 1
# Filter out thought parts (parts with thought: true)
parts = completion.candidates[0].content.parts
non_thought_parts = [
part for part in parts if not (hasattr(part, "thought") and part.thought)
]
assert len(non_thought_parts) == 1, (
f"Instructor does not support multiple function calls, use List[Model] instead"
)
function_call = non_thought_parts[0].function_call
assert function_call is not None, (
f"Please return your response as a function call with the schema {cls.openai_schema} and the name {cls.openai_schema['name']}"
)
assert function_call.name == cls.openai_schema["name"]
return cls.model_validate(
obj=function_call.args, context=validation_context, strict=strict
)
@classmethod
def parse_cohere_json_schema(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
):
# Handle both V1 and V2 response structures
if hasattr(completion, "text"):
# V1 format: direct text access
text = completion.text
elif hasattr(completion, "message") and hasattr(completion.message, "content"):
# V2 format: nested structure (message.content[].text)
# V2 responses may have multiple content items (thinking, text, etc.)
content_items = completion.message.content
if content_items and len(content_items) > 0:
# Find the text content item (skip thinking/other types)
# TODO handle these other content types
text = None
for item in content_items:
if (
hasattr(item, "type")
and item.type == "text"
and hasattr(item, "text")
):
text = item.text
break
if text is None:
raise ResponseParsingError(
"Cohere V2 response has no text content item",
mode="COHERE_JSON_SCHEMA",
raw_response=completion,
)
else:
raise ResponseParsingError(
"Cohere V2 response has no content",
mode="COHERE_JSON_SCHEMA",
raw_response=completion,
)
else:
raise ResponseParsingError(
f"Unsupported Cohere response format. Expected 'text' (V1) or "
f"'message.content[].text' (V2), got: {type(completion)}",
mode="COHERE_JSON_SCHEMA",
raw_response=completion,
)
return cls.model_validate_json(text, context=validation_context, strict=strict)
@classmethod
def parse_anthropic_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
from anthropic.types import Message
if isinstance(completion, Message) and completion.stop_reason == "max_tokens":
raise IncompleteOutputException(last_completion=completion)
# Anthropic returns arguments as a dict, dump to json for model validation below
tool_calls = [
json.dumps(c.input) for c in completion.content if c.type == "tool_use"
] # TODO update with anthropic specific types
tool_calls_validator = TypeAdapter(
Annotated[list[Any], Field(min_length=1, max_length=1)]
)
tool_call = tool_calls_validator.validate_python(tool_calls)[0]
return cls.model_validate_json(
tool_call, context=validation_context, strict=strict
)
@classmethod
def parse_anthropic_json(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
from anthropic.types import Message
last_block = None
if hasattr(completion, "choices"):
completion = completion.choices[0]
if completion.finish_reason == "length":
raise IncompleteOutputException(last_completion=completion)
text = completion.message.content
else:
assert isinstance(completion, Message)
if completion.stop_reason == "max_tokens":
raise IncompleteOutputException(last_completion=completion)
# Find the last text block in the completion
# this is because the completion is a list of blocks
# and the last block is the one that contains the text ideally
# this could happen due to things like multiple tool calls
# read: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#response
text_blocks = [c for c in completion.content if c.type == "text"]
last_block = text_blocks[-1]
text = last_block.text
extra_text = extract_json_from_codeblock(text)
if strict:
model = cls.model_validate_json(
extra_text, context=validation_context, strict=True
)
else:
# Allow control characters to pass through by using the non-strict JSON parser.
parsed = json.loads(extra_text, strict=False)
# Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
model = cls.model_validate(parsed, context=validation_context, strict=False)
return model
@classmethod
def parse_bedrock_json(
cls: type[BaseModel],
completion: Any,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
if isinstance(completion, dict):
# OpenAI will send the first content to be 'reasoningText', and then 'text'
content = completion["output"]["message"]["content"]
text_content = next((c for c in content if "text" in c), None)
if not text_content:
raise ResponseParsingError(
"Unexpected format. No text content found in Bedrock response.",
mode="BEDROCK_JSON",
raw_response=completion,
)
text = text_content["text"]
match = re.search(r"```?json(.*?)```?", text, re.DOTALL)
if match:
text = match.group(1).strip()
text = re.sub(r"```?json|\\n", "", text).strip()
else:
text = completion.text
return cls.model_validate_json(text, context=validation_context, strict=strict)
@classmethod
def parse_bedrock_tools(
cls: type[BaseModel],
completion: Any,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
if isinstance(completion, dict):
# Extract the tool use from Bedrock response
message = completion.get("output", {}).get("message", {})
content = message.get("content", [])
# Find the tool use content block
for content_block in content:
if "toolUse" in content_block:
tool_use = content_block["toolUse"]
assert tool_use.get("name") == cls.__name__, (
f"Tool name mismatch: expected {cls.__name__}, got {tool_use.get('name')}"
)
return cls.model_validate(
tool_use.get("input", {}),
context=validation_context,
strict=strict,
)
raise ResponseParsingError(
"No tool use found in Bedrock response",
mode="BEDROCK_TOOLS",
raw_response=completion,
)
else:
# Fallback for other response formats
return cls.model_validate_json(
completion.text, context=validation_context, strict=strict
)
@classmethod
def parse_gemini_json(
cls: type[BaseModel],
completion: Any,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
try:
text = completion.text
except ValueError:
logger.debug(
f"Error response: {completion.result.candidates[0].finish_reason}\n\n{completion.result.candidates[0].safety_ratings}"
)
try:
extra_text = extract_json_from_codeblock(text) # type: ignore
except UnboundLocalError:
raise ResponseParsingError(
"Unable to extract JSON from completion text. The response may have been blocked or empty.",
mode="GEMINI_JSON",
raw_response=completion,
) from None
if strict:
return cls.model_validate_json(
extra_text, context=validation_context, strict=True
)
else:
# Allow control characters.
parsed = json.loads(extra_text, strict=False)
# Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
return cls.model_validate(parsed, context=validation_context, strict=False)
@classmethod
def parse_vertexai_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
) -> BaseModel:
tool_call = completion.candidates[0].content.parts[0].function_call.args # type: ignore
model = {}
for field in tool_call: # type: ignore
model[field] = tool_call[field]
# We enable strict=False because the conversion from protobuf -> dict often results in types like ints being cast to floats, as a result in order for model.validate to work we need to disable strict mode.
return cls.model_validate(model, context=validation_context, strict=False)
@classmethod
def parse_vertexai_json(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
return cls.model_validate_json(
completion.text, context=validation_context, strict=strict
)
@classmethod
def parse_cohere_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
"""
Parse Cohere tools response.
Supports:
- V1 native tool calls: completion.tool_calls[0].parameters
- V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string)
- V1 text-based: completion.text (prompt-based approach)
- V2 text-based: completion.message.content[].text (prompt-based approach)
"""
# First, check for native Cohere tool calls (V1 and V2)
# V1: completion.tool_calls with tc.parameters (dict)
if hasattr(completion, "tool_calls") and completion.tool_calls:
# V1 tool call format
tool_call = completion.tool_calls[0]
# Parameters in V1 are already a dict
return cls.model_validate(
tool_call.parameters, context=validation_context, strict=strict
)
# V2: completion.message.tool_calls with tc.function.arguments (JSON string)
if (
hasattr(completion, "message")
and hasattr(completion.message, "tool_calls")
and completion.message.tool_calls
):
# V2 tool call format
tool_call = completion.message.tool_calls[0]
# Arguments in V2 are a JSON string
import json
arguments = json.loads(tool_call.function.arguments)
return cls.model_validate(
arguments, context=validation_context, strict=strict
)
# Fallback to text-based extraction (current prompt-based approach)
# Handle both V1 and V2 text response structures
if hasattr(completion, "text"):
# V1 format: direct text access
text = completion.text
elif hasattr(completion, "message") and hasattr(completion.message, "content"):
# V2 format: nested structure (message.content[].text)
# V2 responses may have multiple content items (thinking, text, etc.)
content_items = completion.message.content
if content_items and len(content_items) > 0:
# Find the text content item (skip thinking/other types)
text = None
for item in content_items:
if (
hasattr(item, "type")
and item.type == "text"
and hasattr(item, "text")
):
text = item.text
break
if text is None:
raise ResponseParsingError(
"Cohere V2 response has no text content item",
mode="COHERE_TOOLS",
raw_response=completion,
)
else:
raise ResponseParsingError(
"Cohere V2 response has no content",
mode="COHERE_TOOLS",
raw_response=completion,
)
else:
raise ResponseParsingError(
f"Unsupported Cohere response format. Expected tool_calls or text content. "
f"Got: {type(completion)}",
mode="COHERE_TOOLS",
raw_response=completion,
)
# Extract JSON from text (for prompt-based approach)
extra_text = extract_json_from_codeblock(text)
return cls.model_validate_json(
extra_text, context=validation_context, strict=strict
)
@classmethod
def parse_writer_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
message = completion.choices[0].message
tool_calls = message.tool_calls if message.tool_calls else "{}"
assert len(tool_calls) == 1, (
"Instructor does not support multiple tool calls, use List[Model] instead"
)
assert tool_calls[0].function.name == cls.openai_schema["name"], (
"Tool name does not match"
)
loaded_args = json.loads(tool_calls[0].function.arguments)
return cls.model_validate_json(
json.dumps(loaded_args) if isinstance(loaded_args, dict) else loaded_args,
context=validation_context,
strict=strict,
)
@classmethod
def parse_writer_json(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
_handle_incomplete_output(completion)
message = completion.choices[0].message.content or ""
json_content = extract_json_from_codeblock(message)
if strict:
return cls.model_validate_json(
json_content, context=validation_context, strict=True
)
else:
parsed = json.loads(json_content, strict=False)
return cls.model_validate(parsed, context=validation_context, strict=False)
@classmethod
def parse_functions(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
message = completion.choices[0].message
assert (
message.function_call.name == cls.openai_schema["name"] # type: ignore[index]
), "Function name does not match"
return cls.model_validate_json(
message.function_call.arguments, # type: ignore[attr-defined]
context=validation_context,
strict=strict,
)
@classmethod
def parse_responses_tools(
cls: type[BaseModel],
completion: Any,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
from openai.types.responses import ResponseFunctionToolCall
tool_call_message = None
for message in completion.output:
if isinstance(message, ResponseFunctionToolCall):
if message.name == cls.openai_schema["name"]:
tool_call_message = message
break
if not tool_call_message:
raise ResponseParsingError(
f"Required tool call '{cls.openai_schema['name']}' not found in response",
mode="RESPONSES_TOOLS",
raw_response=completion,
)
return cls.model_validate_json(
tool_call_message.arguments, # type: ignore[attr-defined]
context=validation_context,
strict=strict,
)
@classmethod
def parse_tools(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
message = completion.choices[0].message
# this field seems to be missing when using instructor with some other tools (e.g. litellm)
# trying to fix this by adding a check
if hasattr(message, "refusal"):
assert message.refusal is None, (
f"Unable to generate a response due to {message.refusal}"
)
assert len(message.tool_calls or []) == 1, (
f"Instructor does not support multiple tool calls, use List[Model] instead"
)
tool_call = message.tool_calls[0] # type: ignore
assert (
tool_call.function.name == cls.openai_schema["name"] # type: ignore[index]
), "Tool name does not match"
return cls.model_validate_json(
tool_call.function.arguments, # type: ignore
context=validation_context,
strict=strict,
)
@classmethod
def parse_mistral_structured_outputs(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
if not completion.choices or len(completion.choices) > 1:
raise ConfigurationError(
"Instructor does not support multiple tool calls in MISTRAL_STRUCTURED_OUTPUTS mode. "
"Use list[Model] instead to handle multiple items."
)
message = completion.choices[0].message
return cls.model_validate_json(
message.content, context=validation_context, strict=strict
)
@classmethod
def parse_json(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
"""Parse JSON mode responses using the optimized extraction and validation."""
# Check for incomplete output
_handle_incomplete_output(completion)
# Extract text from the response
message = _extract_text_content(completion)
if not message:
# Fallback for OpenAI format if _extract_text_content doesn't handle it
message = completion.choices[0].message.content or ""
# Extract JSON from the text
json_content = extract_json_from_codeblock(message)
# Validate the model from the JSON
return _validate_model_from_json(cls, json_content, validation_context, strict)
def openai_schema(cls: type[BaseModel]) -> OpenAISchema:
"""
Wrap a Pydantic model class to add OpenAISchema functionality.
"""
if not issubclass(cls, BaseModel):
raise ConfigurationError(
f"response_model must be a Pydantic BaseModel subclass, got {type(cls).__name__}"
)
# Create the wrapped model
schema = wraps(cls, updated=())(
create_model(
cls.__name__ if hasattr(cls, "__name__") else str(cls),
__base__=(cls, OpenAISchema),
)
)
return cast(OpenAISchema, schema)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,714 @@
"""
This module serves as the central dispatcher for processing responses from various LLM providers
(OpenAI, Anthropic, Google, Cohere, etc.) and transforming them into structured Pydantic models.
It handles different response formats, streaming responses, validation, and error recovery.
The module supports 40+ different modes across providers, each with specific handling logic
for request formatting and response parsing. It also provides retry mechanisms (reask) for
handling validation errors gracefully.
Key Components:
- Response processing functions for sync/async operations
- Mode-based response model handlers for different providers
- Error recovery and retry logic for validation failures
- Support for streaming, partial, parallel, and iterable response models
Example:
```python
from instructor.process_response import process_response
from ..mode import Mode
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
# Process an OpenAI response
processed = process_response(
response=openai_response,
response_model=User,
mode=Mode.TOOLS,
stream=False
)
```
"""
from __future__ import annotations
import inspect
import logging
from typing import Any, TypeVar, TYPE_CHECKING, cast
from collections.abc import AsyncGenerator, Callable
from openai.types.chat import ChatCompletion
from pydantic import BaseModel
from typing_extensions import ParamSpec
from instructor.core.exceptions import InstructorError, ConfigurationError
from ..dsl.iterable import IterableBase
from ..dsl.parallel import ParallelBase
from ..dsl.partial import PartialBase
from ..dsl.response_list import ListResponse
from ..dsl.simple_type import AdapterBase
if TYPE_CHECKING:
from .function_calls import OpenAISchema
from ..mode import Mode
from .multimodal import convert_messages
from ..utils.core import prepare_response_model
_SENSITIVE_KEYS: frozenset[str] = frozenset({"api_key", "api_secret", "authorization", "token"})
def _redact_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
"""Return a shallow copy of kwargs with sensitive keys replaced by '[redacted]'."""
return {k: "[redacted]" if k in _SENSITIVE_KEYS else v for k, v in kwargs.items()}
# Anthropic utils
from ..providers.anthropic.utils import (
handle_anthropic_json,
handle_anthropic_parallel_tools,
handle_anthropic_reasoning_tools,
handle_anthropic_tools,
reask_anthropic_json,
reask_anthropic_tools,
)
# Bedrock utils
from ..providers.bedrock.utils import (
handle_bedrock_json,
handle_bedrock_tools,
reask_bedrock_json,
reask_bedrock_tools,
)
# Cerebras utils
from ..providers.cerebras.utils import (
handle_cerebras_json,
handle_cerebras_tools,
reask_cerebras_tools,
)
# Cohere utils
from ..providers.cohere.utils import (
handle_cohere_json_schema,
handle_cohere_tools,
reask_cohere_tools,
)
# Fireworks utils
from ..providers.fireworks.utils import (
handle_fireworks_json,
handle_fireworks_tools,
reask_fireworks_json,
reask_fireworks_tools,
)
# Google/Gemini/VertexAI utils
from ..providers.gemini.utils import (
handle_gemini_json,
handle_gemini_tools,
handle_genai_structured_outputs,
handle_genai_tools,
handle_vertexai_json,
handle_vertexai_parallel_tools,
handle_vertexai_tools,
reask_gemini_json,
reask_gemini_tools,
reask_genai_structured_outputs,
reask_genai_tools,
reask_vertexai_json,
reask_vertexai_tools,
)
# Mistral utils
from ..providers.mistral.utils import (
handle_mistral_structured_outputs,
handle_mistral_tools,
reask_mistral_structured_outputs,
reask_mistral_tools,
)
# OpenAI utils
from ..providers.openai.utils import (
handle_functions,
handle_json_modes,
handle_json_o1,
handle_openrouter_structured_outputs,
handle_parallel_tools,
handle_responses_tools,
handle_responses_tools_with_inbuilt_tools,
handle_tools,
handle_tools_strict,
reask_default,
reask_md_json,
reask_responses_tools,
reask_tools,
)
# Perplexity utils
from ..providers.perplexity.utils import (
handle_perplexity_json,
reask_perplexity_json,
)
# Writer utils
from ..providers.writer.utils import (
handle_writer_json,
handle_writer_tools,
reask_writer_json,
reask_writer_tools,
)
# XAI utils
from ..providers.xai.utils import (
handle_xai_json,
handle_xai_tools,
reask_xai_json,
reask_xai_tools,
)
logger = logging.getLogger("instructor")
T_Model = TypeVar("T_Model", bound=BaseModel)
T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")
T = TypeVar("T")
async def process_response_async(
response: ChatCompletion,
*,
response_model: type[T_Model | OpenAISchema | BaseModel] | None,
stream: bool = False,
validation_context: dict[str, Any] | None = None,
strict: bool | None = None,
mode: Mode = Mode.TOOLS,
on_event: Callable[..., Any] | None = None,
) -> Any:
"""Asynchronously process and transform LLM responses into structured models.
This function is the async entry point for converting raw LLM responses into validated
Pydantic models. It handles various response formats from different providers and
supports special response types like streaming, partial objects, and parallel tool calls.
Args:
response (ChatCompletion or Similar API Response): The raw response from the LLM API. Despite the type hint,
this can be responses from any supported provider (OpenAI, Anthropic, Google, etc.)
response_model (type[T_Model | BaseModel] | None): The target Pydantic
model to parse the response into. If None, returns the raw response unchanged.
Can also be special DSL types like ParallelBase for parallel tool calls, or IterableBase and PartialBase for streaming.
stream (bool): Whether this is a streaming response. Required for proper handling
of IterableBase and PartialBase models. Defaults to False.
validation_context (dict[str, Any] | None): Additional context passed to Pydantic
validators during model validation. Useful for dynamic validation logic. The context
is also used to format templated responses. Defaults to None.
strict (bool | None): Whether to enforce strict JSON parsing. When True, the response
must exactly match the model schema. When False, allows minor deviations.
mode (Mode): The provider/format mode that determines how to parse the response.
Examples: Mode.TOOLS (OpenAI), Mode.ANTHROPIC_JSON, Mode.GEMINI_TOOLS.
Defaults to Mode.TOOLS.
Returns:
T_Model | ChatCompletion: The processed response. Return type depends on inputs:
- If response_model is None: returns raw response unchanged
- If response_model is IterableBase with stream=True: returns list of models
- If response_model is AdapterBase: returns the adapted content
- Otherwise: returns instance of response_model with _raw_response attached
Raises:
ValidationError: If the response doesn't match the expected model schema
IncompleteOutputException: If the response was truncated due to token limits
ValueError: If an invalid mode is specified
Note:
The function automatically detects special response model types (Iterable, Partial,
Parallel, Adapter) and applies appropriate processing logic for each.
"""
logger.debug(
f"Instructor Raw Response: {response}",
)
if response_model is None:
return response
if (
inspect.isclass(response_model)
and issubclass(response_model, IterableBase)
and stream
):
# Preserve streaming behavior for `create_iterable()` (async for).
return response_model.from_streaming_response_async( # type: ignore[return-value,arg-type]
cast(AsyncGenerator[Any, None], response),
mode=mode,
)
if (
inspect.isclass(response_model)
and issubclass(response_model, PartialBase)
and stream
):
# Return the AsyncGenerator directly for streaming Partial responses.
return response_model.from_streaming_response_async( # type: ignore[return-value,arg-type]
cast(AsyncGenerator[Any, None], response),
mode=mode,
on_event=on_event,
)
model = response_model.from_response( # type: ignore
response,
validation_context=validation_context,
strict=strict,
mode=mode,
)
# ? This really hints at the fact that we need a better way of
# ? attaching usage data and the raw response to the model we return.
if isinstance(model, IterableBase):
logger.debug(f"Returning takes from IterableBase")
return ListResponse.from_list( # type: ignore[return-value]
[task for task in model.tasks],
raw_response=response,
)
if isinstance(response_model, ParallelBase):
logger.debug(f"Returning model from ParallelBase")
return ListResponse.from_list( # type: ignore[return-value]
list(model),
raw_response=response,
)
if isinstance(model, AdapterBase):
logger.debug(f"Returning model from AdapterBase")
return model.content
model._raw_response = response
return model
def process_response(
response: T_Model,
*,
response_model: type[OpenAISchema | BaseModel] | None = None,
stream: bool,
validation_context: dict[str, Any] | None = None,
strict=None,
mode: Mode = Mode.TOOLS,
on_event: Callable[..., Any] | None = None,
) -> Any:
"""Process and transform LLM responses into structured models (synchronous).
This is the main entry point for converting raw LLM responses into validated Pydantic
models. It acts as a dispatcher that handles various response formats from 40+ different
provider modes and transforms them according to the specified response model type.
Args:
response (T_Model): The raw response from the LLM API. The actual type varies by
provider (ChatCompletion for OpenAI, Message for Anthropic, etc.)
response_model (type[OpenAISchema | BaseModel] | None): The target Pydantic model
class to parse the response into. Special DSL types supported:
- IterableBase: For streaming multiple objects from a single response
- PartialBase: For incomplete/streaming partial objects
- ParallelBase: For parallel tool/function calls
- AdapterBase: For simple type adaptations (e.g., str, int)
If None, returns the raw response unchanged.
stream (bool): Whether this is a streaming response. Required to be True for
proper handling of IterableBase and PartialBase models.
validation_context (dict[str, Any] | None): Additional context passed to Pydantic
validators. Useful for runtime validation logic based on external state.
strict (bool | None): Controls JSON parsing strictness:
- True: Enforce exact schema matching (no extra fields)
- False/None: Allow minor deviations and extra fields
mode (Mode): The provider/format mode that determines parsing strategy.
Each mode corresponds to a specific provider and format combination:
- Tool modes: TOOLS, ANTHROPIC_TOOLS, GEMINI_TOOLS, etc.
- JSON modes: JSON, ANTHROPIC_JSON, VERTEXAI_JSON, etc.
- Special modes: PARALLEL_TOOLS, MD_JSON, JSON_SCHEMA, etc.
Returns:
T_Model | list[T_Model] | None: The processed response:
- If response_model is None: Original response unchanged
- If IterableBase: List of extracted model instances
- If ParallelBase: Special parallel response object
- If AdapterBase: The adapted simple type (str, int, etc.)
- Otherwise: Single instance of response_model with _raw_response attached
Raises:
ValidationError: Response doesn't match the expected model schema
IncompleteOutputException: Response truncated due to token limits
ValueError: Invalid mode specified or mode not supported
JSONDecodeError: Malformed JSON in response (for JSON modes)
Note:
The function preserves the raw response by attaching it to the parsed model
as `_raw_response`. This allows access to metadata like token usage, model
info, and other provider-specific fields after parsing.
"""
logger.debug(
f"Instructor Raw Response: {response}",
)
if response_model is None:
logger.debug("No response model, returning response as is")
return response
if (
inspect.isclass(response_model)
and issubclass(response_model, IterableBase)
and stream
):
# Preserve streaming behavior for `create_iterable()` (for/async for).
return response_model.from_streaming_response( # type: ignore[return-value]
response,
mode=mode,
)
if (
inspect.isclass(response_model)
and issubclass(response_model, PartialBase)
and stream
):
# Collect partial stream to surface validation errors inside retry logic.
return list(
response_model.from_streaming_response( # type: ignore
response,
mode=mode,
on_event=on_event,
)
)
model = response_model.from_response( # type: ignore
response,
validation_context=validation_context,
strict=strict,
mode=mode,
)
# ? This really hints at the fact that we need a better way of
# ? attaching usage data and the raw response to the model we return.
if isinstance(model, IterableBase):
logger.debug(f"Returning takes from IterableBase")
return ListResponse.from_list( # type: ignore[return-value]
[task for task in model.tasks],
raw_response=response,
)
if isinstance(response_model, ParallelBase):
logger.debug(f"Returning model from ParallelBase")
return ListResponse.from_list( # type: ignore[return-value]
list(model),
raw_response=response,
)
if isinstance(model, AdapterBase):
logger.debug(f"Returning model from AdapterBase")
return model.content
model._raw_response = response
return model
def is_typed_dict(cls) -> bool:
return (
isinstance(cls, type)
and issubclass(cls, dict)
and hasattr(cls, "__annotations__")
)
def handle_response_model(
response_model: type[T] | None, mode: Mode = Mode.TOOLS, **kwargs: Any
) -> tuple[type[T] | None, dict[str, Any]]:
"""
Handles the response model based on the specified mode and prepares the kwargs for the API call.
This really should be named 'prepare_create_kwargs' as its job is to map the openai create kwargs
to the correct format for the API call based on the mode.
Args:
response_model (type[T] | None): The response model to be used for parsing the API response.
mode (Mode): The mode to use for handling the response model. Defaults to Mode.TOOLS.
**kwargs: Additional keyword arguments to be passed to the API call.
Returns:
tuple[type[T] | None, dict[str, Any]]: A tuple containing the processed response model and the updated kwargs.
This function prepares the response model and modifies the kwargs based on the specified mode.
It handles various modes like TOOLS, JSON, FUNCTIONS, etc., and applies the appropriate
transformations to the response model and kwargs.
"""
new_kwargs = kwargs.copy()
# Extract autodetect_images for message conversion
autodetect_images = new_kwargs.pop("autodetect_images", False)
PARALLEL_MODES = {
Mode.PARALLEL_TOOLS: handle_parallel_tools,
Mode.VERTEXAI_PARALLEL_TOOLS: handle_vertexai_parallel_tools,
Mode.ANTHROPIC_PARALLEL_TOOLS: handle_anthropic_parallel_tools,
}
if mode in PARALLEL_MODES:
response_model, new_kwargs = PARALLEL_MODES[mode](response_model, new_kwargs) # type: ignore
_safe_kwargs = _redact_kwargs(new_kwargs)
logger.debug(
f"Instructor Request: {mode.value=}, {response_model=}, new_kwargs={_safe_kwargs!r}",
extra={
"mode": mode.value,
"response_model": (
response_model.__name__
if response_model is not None
and hasattr(response_model, "__name__")
else str(response_model)
),
"new_kwargs": _safe_kwargs,
},
)
return response_model, new_kwargs
# Only prepare response_model if it's not None
if response_model is not None:
response_model = prepare_response_model(response_model)
mode_handlers = { # type: ignore
Mode.FUNCTIONS: handle_functions,
Mode.TOOLS_STRICT: handle_tools_strict,
Mode.TOOLS: handle_tools,
Mode.MISTRAL_TOOLS: handle_mistral_tools,
Mode.MISTRAL_STRUCTURED_OUTPUTS: handle_mistral_structured_outputs,
Mode.JSON_O1: handle_json_o1,
Mode.JSON: lambda rm, nk: handle_json_modes(rm, nk, Mode.JSON), # type: ignore
Mode.MD_JSON: lambda rm, nk: handle_json_modes(rm, nk, Mode.MD_JSON), # type: ignore
Mode.JSON_SCHEMA: lambda rm, nk: handle_json_modes(rm, nk, Mode.JSON_SCHEMA), # type: ignore
Mode.ANTHROPIC_TOOLS: handle_anthropic_tools,
Mode.ANTHROPIC_REASONING_TOOLS: handle_anthropic_reasoning_tools,
Mode.ANTHROPIC_JSON: handle_anthropic_json,
Mode.COHERE_JSON_SCHEMA: handle_cohere_json_schema,
Mode.COHERE_TOOLS: handle_cohere_tools,
Mode.GEMINI_JSON: handle_gemini_json,
Mode.GEMINI_TOOLS: handle_gemini_tools,
Mode.GENAI_TOOLS: lambda rm, nk: handle_genai_tools(rm, nk, autodetect_images),
Mode.GENAI_STRUCTURED_OUTPUTS: lambda rm, nk: handle_genai_structured_outputs(
rm, nk, autodetect_images
),
Mode.VERTEXAI_TOOLS: handle_vertexai_tools,
Mode.VERTEXAI_JSON: handle_vertexai_json,
Mode.CEREBRAS_JSON: handle_cerebras_json,
Mode.CEREBRAS_TOOLS: handle_cerebras_tools,
Mode.FIREWORKS_JSON: handle_fireworks_json,
Mode.FIREWORKS_TOOLS: handle_fireworks_tools,
Mode.WRITER_TOOLS: handle_writer_tools,
Mode.WRITER_JSON: handle_writer_json,
Mode.BEDROCK_JSON: handle_bedrock_json,
Mode.BEDROCK_TOOLS: handle_bedrock_tools,
Mode.PERPLEXITY_JSON: handle_perplexity_json,
Mode.OPENROUTER_STRUCTURED_OUTPUTS: handle_openrouter_structured_outputs,
Mode.RESPONSES_TOOLS: handle_responses_tools,
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS: handle_responses_tools_with_inbuilt_tools,
Mode.XAI_JSON: handle_xai_json,
Mode.XAI_TOOLS: handle_xai_tools,
}
if mode in mode_handlers:
response_model, new_kwargs = mode_handlers[mode](response_model, new_kwargs) # type: ignore
else:
raise ConfigurationError(
f"Invalid or unsupported mode: {mode}. "
f"This mode may not be implemented. "
f"Available modes: {', '.join(str(m) for m in mode_handlers.keys())}"
)
# Handle message conversion for modes that don't already handle it
if "messages" in new_kwargs:
new_kwargs["messages"] = convert_messages(
new_kwargs["messages"],
mode,
autodetect_images=autodetect_images,
)
_safe_kwargs = _redact_kwargs(new_kwargs)
logger.debug(
f"Instructor Request: {mode.value=}, {response_model=}, new_kwargs={_safe_kwargs!r}",
extra={
"mode": mode.value,
"response_model": (
response_model.__name__
if response_model is not None and hasattr(response_model, "__name__")
else str(response_model)
),
"new_kwargs": _safe_kwargs,
},
)
return response_model, new_kwargs
def handle_reask_kwargs(
kwargs: dict[str, Any],
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).
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 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. 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 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 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
- 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 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 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
Mode.FUNCTIONS: reask_default,
Mode.TOOLS_STRICT: reask_tools,
Mode.TOOLS: reask_tools,
Mode.JSON_O1: reask_default,
Mode.JSON: reask_md_json,
Mode.MD_JSON: reask_md_json,
Mode.JSON_SCHEMA: reask_md_json,
Mode.PARALLEL_TOOLS: reask_tools,
Mode.RESPONSES_TOOLS: reask_responses_tools,
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS: reask_responses_tools,
# Mistral modes
Mode.MISTRAL_TOOLS: reask_mistral_tools,
Mode.MISTRAL_STRUCTURED_OUTPUTS: reask_mistral_structured_outputs,
# Anthropic modes
Mode.ANTHROPIC_TOOLS: reask_anthropic_tools,
Mode.ANTHROPIC_REASONING_TOOLS: reask_anthropic_tools,
Mode.ANTHROPIC_JSON: reask_anthropic_json,
Mode.ANTHROPIC_PARALLEL_TOOLS: reask_anthropic_tools,
# Cohere modes
Mode.COHERE_TOOLS: reask_cohere_tools,
Mode.COHERE_JSON_SCHEMA: reask_cohere_tools,
# Gemini/Google modes
Mode.GEMINI_TOOLS: reask_gemini_tools,
Mode.GEMINI_JSON: reask_gemini_json,
Mode.GENAI_TOOLS: reask_genai_tools,
Mode.GENAI_STRUCTURED_OUTPUTS: reask_genai_structured_outputs,
# VertexAI modes
Mode.VERTEXAI_TOOLS: reask_vertexai_tools,
Mode.VERTEXAI_JSON: reask_vertexai_json,
Mode.VERTEXAI_PARALLEL_TOOLS: reask_vertexai_tools,
# Cerebras modes
Mode.CEREBRAS_TOOLS: reask_cerebras_tools,
Mode.CEREBRAS_JSON: reask_default,
# Fireworks modes
Mode.FIREWORKS_TOOLS: reask_fireworks_tools,
Mode.FIREWORKS_JSON: reask_fireworks_json,
# Writer modes
Mode.WRITER_TOOLS: reask_writer_tools,
Mode.WRITER_JSON: reask_writer_json,
# Bedrock modes
Mode.BEDROCK_TOOLS: reask_bedrock_tools,
Mode.BEDROCK_JSON: reask_bedrock_json,
# Perplexity modes
Mode.PERPLEXITY_JSON: reask_perplexity_json,
# OpenRouter modes
Mode.OPENROUTER_STRUCTURED_OUTPUTS: reask_md_json,
# XAI modes
Mode.XAI_JSON: reask_xai_json,
Mode.XAI_TOOLS: reask_xai_tools,
}
if mode in REASK_HANDLERS:
return REASK_HANDLERS[mode](kwargs_copy, response, exception)
else:
return reask_default(kwargs_copy, response, exception)

View File

@@ -0,0 +1,133 @@
"""
Standalone schema generation utilities for different LLM providers.
This module provides provider-agnostic functions to generate schemas from Pydantic models
without requiring inheritance from OpenAISchema or use of decorators.
"""
from __future__ import annotations
import functools
import warnings
from typing import Any, cast
from docstring_parser import parse
from pydantic import BaseModel
from ..providers.gemini.utils import map_to_gemini_function_schema
__all__ = [
"generate_openai_schema",
"generate_anthropic_schema",
"generate_gemini_schema",
]
@functools.lru_cache(maxsize=256)
def generate_openai_schema(model: type[BaseModel]) -> dict[str, Any]:
"""
Generate OpenAI function schema from a Pydantic model.
Args:
model: A Pydantic BaseModel subclass
Returns:
A dictionary in the format of OpenAI's function schema
Note:
The model's docstring will be used for the function description.
Parameter descriptions from the docstring will enrich field descriptions.
"""
schema = model.model_json_schema()
docstring = parse(model.__doc__ or "")
parameters = {k: v for k, v in schema.items() if k not in ("title", "description")}
# Enrich parameter descriptions from docstring
for param in docstring.params:
if (name := param.arg_name) in parameters["properties"] and (
description := param.description
):
if "description" not in parameters["properties"][name]:
parameters["properties"][name]["description"] = description
parameters["required"] = sorted(
k for k, v in parameters["properties"].items() if "default" not in v
)
if "description" not in schema:
if docstring.short_description:
schema["description"] = docstring.short_description
else:
schema["description"] = (
f"Correctly extracted `{model.__name__}` with all "
f"the required parameters with correct types"
)
return {
"name": schema["title"],
"description": schema["description"],
"parameters": parameters,
}
@functools.lru_cache(maxsize=256)
def generate_anthropic_schema(model: type[BaseModel]) -> dict[str, Any]:
"""
Generate Anthropic tool schema from a Pydantic model.
Args:
model: A Pydantic BaseModel subclass
Returns:
A dictionary in the format of Anthropic's tool schema
"""
# Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
openai_schema = generate_openai_schema(model)
return {
"name": openai_schema["name"],
"description": openai_schema["description"],
"input_schema": model.model_json_schema(),
}
@functools.lru_cache(maxsize=256)
def generate_gemini_schema(model: type[BaseModel]) -> Any:
"""
Generate Gemini function schema from a Pydantic model.
Args:
model: A Pydantic BaseModel subclass
Returns:
A Gemini FunctionDeclaration object
Note:
This function is deprecated. The google-generativeai library is being replaced by google-genai.
"""
# This is kept for backward compatibility but deprecated
warnings.warn(
"generate_gemini_schema is deprecated. The google-generativeai library is being replaced by google-genai.",
DeprecationWarning,
stacklevel=2,
)
try:
import importlib
genai_types = cast(Any, importlib.import_module("google.generativeai.types"))
# Use OpenAI schema
openai_schema = generate_openai_schema(model)
# Transform to Gemini format
function = genai_types.FunctionDeclaration(
name=openai_schema["name"],
description=openai_schema["description"],
parameters=map_to_gemini_function_schema(openai_schema["parameters"]),
)
return function
except ImportError as e:
raise ImportError(
"google-generativeai is deprecated. Please install google-genai instead: pip install google-genai"
) from e

View File

@@ -0,0 +1,26 @@
"""Validators that extend OpenAISchema for structured outputs."""
from typing import Optional
from pydantic import Field
from .function_calls import OpenAISchema
class Validator(OpenAISchema):
"""
Validate if an attribute is correct and if not,
return a new value with an error message
"""
is_valid: bool = Field(
description="Whether the attribute is valid based on the requirements",
)
reason: Optional[str] = Field(
default=None,
description="The error message if the attribute is not valid, otherwise None",
)
fixed_value: Optional[str] = Field(
default=None,
description="If the attribute is not valid, suggest a new value for the attribute",
)