Files
567-labs--instructor/instructor/v2/core/patch.py
T
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

402 lines
13 KiB
Python

"""v2 patch mechanism using hierarchical registry.
Simplified patching logic that uses the v2 mode registry for handler dispatch.
"""
from __future__ import annotations
import logging
import warnings
from collections.abc import Awaitable
from functools import wraps
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast, overload
from pydantic import BaseModel
from instructor.v2.core.mode import Mode
from instructor.v2.core.providers import Provider
from instructor.v2.core.hooks import Hooks
from instructor.v2.core.templating import handle_templating
from instructor.v2.core.utils import is_async
from instructor.v2.core.exceptions import RegistryValidationMixin
from instructor.v2.core.registry import mode_registry
from instructor.v2.core.response_model import prepare_response_model
from instructor.v2.core.retry import retry_async_v2, retry_sync_v2
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from openai import AsyncOpenAI, OpenAI
from tenacity import AsyncRetrying, Retrying
logger = logging.getLogger("instructor.v2")
T_Model = TypeVar("T_Model", bound=BaseModel)
T_Retval = TypeVar("T_Retval")
class InstructorChatCompletionCreate(Protocol):
def __call__(
self,
response_model: type[T_Model] | None = None,
context: dict[str, Any] | None = None,
max_retries: int | Retrying = 1,
*args: Any,
**kwargs: Any,
) -> T_Model: ...
class AsyncInstructorChatCompletionCreate(Protocol):
async def __call__(
self,
response_model: type[T_Model] | None = None,
context: dict[str, Any] | None = None,
max_retries: int | AsyncRetrying = 1,
*args: Any,
**kwargs: Any,
) -> T_Model: ...
@overload
def patch_v2(
func: Callable[..., Awaitable[Any]],
provider: Provider,
mode: Mode,
default_model: str | None = None,
) -> Callable[..., Awaitable[T_Model]]: ...
@overload
def patch_v2(
func: Callable[..., Any],
provider: Provider,
mode: Mode,
default_model: str | None = None,
) -> Callable[..., T_Model]: ...
def patch_v2(
func: Callable[..., Any],
provider: Provider,
mode: Mode,
default_model: str | None = None,
) -> Callable[..., Any]:
"""Patch a function to use v2 registry for structured outputs.
Args:
func: Function to patch (e.g., client.messages.create)
provider: Provider enum value
mode: Mode enum value
default_model: Default model to inject if not provided in request
Returns:
Patched function that supports response_model parameter
Raises:
RegistryError: If mode is not registered for provider
"""
logger.debug(f"Patching with v2 registry: {provider=}, {mode=}, {default_model=}")
# Validate mode registration
RegistryValidationMixin.validate_mode_registration(provider, mode)
func_is_async = is_async(func)
if func_is_async:
return _create_async_wrapper(func, provider, mode, default_model)
return _create_sync_wrapper(func, provider, mode, default_model)
@overload
def patch(
client: OpenAI,
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> OpenAI: ...
@overload
def patch(
client: AsyncOpenAI,
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> AsyncOpenAI: ...
@overload
def patch(
create: Callable[..., T_Retval],
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> InstructorChatCompletionCreate: ...
@overload
def patch(
create: Awaitable[T_Retval],
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> InstructorChatCompletionCreate: ...
def patch(
client: OpenAI | AsyncOpenAI | None = None,
create: Callable[..., T_Retval] | None = None,
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> OpenAI | AsyncOpenAI | InstructorChatCompletionCreate:
"""Patch chat-completion create methods with v2 registry handlers."""
logger.debug(f"Patching `client.chat.completions.create` with {mode=}")
if create is not None:
func = create
elif client is not None:
func = client.chat.completions.create
else:
raise ValueError("Either client or create must be provided")
new_create = patch_v2(func=func, provider=provider, mode=mode)
if client is not None:
cast(Any, client.chat.completions).create = new_create
return client
return new_create
def apatch(
client: AsyncOpenAI,
mode: Mode = Mode.TOOLS,
provider: Provider = Provider.OPENAI,
) -> AsyncOpenAI:
"""Deprecated alias for :func:`patch`."""
warnings.warn(
"apatch is deprecated, use patch instead",
DeprecationWarning,
stacklevel=2,
)
return patch(client, mode=mode, provider=provider)
def _create_sync_wrapper(
func: Callable[..., Any],
provider: Provider,
mode: Mode,
default_model: str | None = None,
) -> Callable[..., T_Model]:
"""Create synchronous wrapper for patched function."""
@wraps(func)
def new_create_sync(
response_model: type[T_Model] | None = None,
context: dict[str, Any] | None = None,
max_retries: int | Retrying = 1,
strict: bool = True,
hooks: Hooks | None = None,
*args: Any,
**kwargs: Any,
) -> T_Model:
"""Patched synchronous create function."""
autodetect_images = bool(kwargs.get("autodetect_images", False))
cache = kwargs.pop("cache", None)
cache_ttl_raw = kwargs.pop("cache_ttl", None)
cache_ttl = cache_ttl_raw if isinstance(cache_ttl_raw, int) else None
# Inject default model if not provided and available
if default_model is not None and "model" not in kwargs:
kwargs["model"] = default_model
# Get handlers from registry
handlers = mode_registry.get_handlers(provider, mode)
if response_model is not None:
response_model = prepare_response_model(response_model)
# Prepare request kwargs using registry handler
response_model, new_kwargs = handlers.request_handler(
response_model=response_model, kwargs=kwargs
)
new_kwargs.pop("autodetect_images", None)
if handlers.message_converter and "messages" in new_kwargs:
new_kwargs["messages"] = handlers.message_converter(
new_kwargs["messages"],
autodetect_images=autodetect_images,
)
# Handle templating
new_kwargs = handle_templating(
new_kwargs,
mode=mode,
provider=provider,
context=context,
)
# Attempt cache lookup before retry layer
if cache is not None and response_model is not None:
from instructor.cache import BaseCache, make_cache_key, load_cached_response
if isinstance(cache, BaseCache):
key = make_cache_key(
messages=new_kwargs.get("messages")
or new_kwargs.get("contents")
or new_kwargs.get("chat_history"),
model=new_kwargs.get("model"),
response_model=response_model,
mode=str(mode.value),
)
cached = load_cached_response(cache, key, response_model)
if cached is not None:
return cached # type: ignore[return-value]
# Use v2 retry logic with registry handlers
response = retry_sync_v2(
func=func,
response_model=response_model,
provider=provider,
mode=mode,
context=context,
max_retries=max_retries,
args=args,
kwargs=new_kwargs,
strict=strict,
hooks=hooks,
)
# Store in cache after successful call
if cache is not None and response_model is not None:
try:
from instructor.cache import (
BaseCache,
make_cache_key,
store_cached_response,
)
from pydantic import BaseModel as _BM # type: ignore[import-not-found]
if isinstance(cache, BaseCache) and isinstance(response, _BM):
key = make_cache_key(
messages=new_kwargs.get("messages")
or new_kwargs.get("contents")
or new_kwargs.get("chat_history"),
model=new_kwargs.get("model"),
response_model=response_model,
mode=str(mode.value),
)
store_cached_response(cache, key, response, ttl=cache_ttl)
except ModuleNotFoundError:
pass
return response # type: ignore[return-value]
return new_create_sync # type: ignore[return-value]
def _create_async_wrapper(
func: Callable[..., Awaitable[Any]],
provider: Provider,
mode: Mode,
default_model: str | None = None,
) -> Callable[..., Awaitable[T_Model]]:
"""Create asynchronous wrapper for patched function."""
@wraps(func)
async def new_create_async(
response_model: type[T_Model] | None = None,
context: dict[str, Any] | None = None,
max_retries: int | AsyncRetrying = 1,
strict: bool = True,
hooks: Hooks | None = None,
*args: Any,
**kwargs: Any,
) -> T_Model:
"""Patched asynchronous create function."""
autodetect_images = bool(kwargs.get("autodetect_images", False))
cache = kwargs.pop("cache", None)
cache_ttl_raw = kwargs.pop("cache_ttl", None)
cache_ttl = cache_ttl_raw if isinstance(cache_ttl_raw, int) else None
# Inject default model if not provided and available
if default_model is not None and "model" not in kwargs:
kwargs["model"] = default_model
# Get handlers from registry
handlers = mode_registry.get_handlers(provider, mode)
if response_model is not None:
response_model = prepare_response_model(response_model)
# Prepare request kwargs using registry handler
response_model, new_kwargs = handlers.request_handler(
response_model=response_model, kwargs=kwargs
)
new_kwargs.pop("autodetect_images", None)
if handlers.message_converter and "messages" in new_kwargs:
new_kwargs["messages"] = handlers.message_converter(
new_kwargs["messages"],
autodetect_images=autodetect_images,
)
# Handle templating
new_kwargs = handle_templating(
new_kwargs,
mode=mode,
provider=provider,
context=context,
)
# Attempt cache lookup before retry layer
if cache is not None and response_model is not None:
from instructor.cache import BaseCache, make_cache_key, load_cached_response
if isinstance(cache, BaseCache):
key = make_cache_key(
messages=new_kwargs.get("messages")
or new_kwargs.get("contents")
or new_kwargs.get("chat_history"),
model=new_kwargs.get("model"),
response_model=response_model,
mode=str(mode.value),
)
cached = load_cached_response(cache, key, response_model)
if cached is not None:
return cached # type: ignore[return-value]
# Use v2 retry logic with registry handlers
response = await retry_async_v2(
func=func,
response_model=response_model,
provider=provider,
mode=mode,
context=context,
max_retries=max_retries,
args=args,
kwargs=new_kwargs,
strict=strict,
hooks=hooks,
)
# Store in cache after successful call
if cache is not None and response_model is not None:
try:
from instructor.cache import (
BaseCache,
make_cache_key,
store_cached_response,
)
from pydantic import BaseModel as _BM # type: ignore[import-not-found]
if isinstance(cache, BaseCache) and isinstance(response, _BM):
key = make_cache_key(
messages=new_kwargs.get("messages")
or new_kwargs.get("contents")
or new_kwargs.get("chat_history"),
model=new_kwargs.get("model"),
response_model=response_model,
mode=str(mode.value),
)
store_cached_response(cache, key, response, ttl=cache_ttl)
except ModuleNotFoundError:
pass
return response # type: ignore[return-value]
return new_create_async # type: ignore[return-value]