참고소스 수정본
This commit is contained in:
30
참고/instructor-main/instructor/processing/__init__.py
Normal file
30
참고/instructor-main/instructor/processing/__init__.py
Normal 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",
|
||||
]
|
||||
816
참고/instructor-main/instructor/processing/function_calls.py
Normal file
816
참고/instructor-main/instructor/processing/function_calls.py
Normal 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)
|
||||
1127
참고/instructor-main/instructor/processing/multimodal.py
Normal file
1127
참고/instructor-main/instructor/processing/multimodal.py
Normal file
File diff suppressed because it is too large
Load Diff
714
참고/instructor-main/instructor/processing/response.py
Normal file
714
참고/instructor-main/instructor/processing/response.py
Normal 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)
|
||||
133
참고/instructor-main/instructor/processing/schema.py
Normal file
133
참고/instructor-main/instructor/processing/schema.py
Normal 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
|
||||
26
참고/instructor-main/instructor/processing/validators.py
Normal file
26
참고/instructor-main/instructor/processing/validators.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user