跳至内容

钩子

钩子提供了一种强大的机制,用于在 Instructor 库的完成和解析过程中拦截和处理事件。它们允许你在 API 交互的不同阶段添加自定义行为、日志记录或错误处理。

概述

Instructor 中的钩子系统基于 Hooks 类,该类管理事件注册和发射。它支持几种预定义事件,这些事件对应于完成和解析过程的不同阶段。

支持的钩子事件

completion:kwargs

当提供完成参数时发射此钩子。它接收传递给完成函数的所有参数。这些参数将包含 modelmessagestools,在任何 response_modelvalidation_context 参数被转换为各自的值之后。

def handler(*args, **kwargs) -> None: ...

completion:response

当收到完成响应时发射此钩子。它接收来自完成 API 的原始响应对象。

def handler(response) -> None: ...

completion:error

在完成过程中发生错误时发射此钩子,这发生在任何重试尝试之前,并且响应被解析为 pydantic 模型之前。

def handler(error) -> None: ...

parse:error

当将响应解析为 pydantic 模型时发生错误时发射此钩子。这可能发生在响应无效或 pydantic 模型与响应不兼容时。

def handler(error) -> None: ...

completion:last_attempt

当进行最后一次重试尝试时发射此钩子。

def handler(error) -> None: ...

实现细节

钩子系统在 instructor/hooks.py 文件中实现。Hooks 类处理钩子事件的注册和发射。你可以参考此文件来查看钩子在幕后是如何工作的。使用钩子的重试逻辑在 instructor/retry.py 文件中实现。这展示了在完成过程中发生错误后如何使用钩子进行重试。

钩子类型

钩子系统使用类型化的 Protocol 类来为处理函数提供更好的类型安全。

from typing import Any, Protocol


# Handler protocol types for type safety
class CompletionKwargsHandler(Protocol):
    """Protocol for completion kwargs handlers."""

    def __call__(self, *args: Any, **kwargs: Any) -> None: ...


class CompletionResponseHandler(Protocol):
    """Protocol for completion response handlers."""

    def __call__(self, response: Any) -> None: ...


class CompletionErrorHandler(Protocol):
    """Protocol for completion error and last attempt handlers."""

    def __call__(self, error: Exception) -> None: ...


class ParseErrorHandler(Protocol):
    """Protocol for parse error handlers."""

    def __call__(self, error: Exception) -> None: ...

这些 Protocol 类型有助于确保你的处理函数对每种类型的钩子都有正确的签名。

钩子名称

钩子名称可以指定为枚举值 (HookName.COMPLETION_KWARGS) 或字符串 ("completion:kwargs")。

from instructor.hooks import HookName

# Using enum
client.on(HookName.COMPLETION_KWARGS, handler)

# Using string
client.on("completion:kwargs", handler)

注册钩子

你可以使用 Instructor 客户端或 Hooks 实例的 on 方法注册钩子。这里有一个示例:

import instructor
import openai
import pprint

client = instructor.from_openai(openai.OpenAI())


def log_completion_kwargs(*args, **kwargs):
    pprint.pprint({"args": args, "kwargs": kwargs})


client.on("completion:kwargs", log_completion_kwargs)

resp = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "Hello, world!"}],
    response_model=str,
)
print(resp)
#> Hello, user! How can I assist you today?

发射事件

事件由 Instructor 库在适当的时间自动发射。在大多数情况下,你不需要手动发射事件。在内部,所有发射方法都使用一个通用的 emit 方法,该方法处理错误捕获并提供一致的行为。

移除钩子

你可以使用 off 方法移除特定的钩子:

import instructor
import openai
import pprint

client = instructor.from_openai(openai.OpenAI())


def log_completion_kwargs(*args, **kwargs):
    pprint.pprint({"args": args, "kwargs": kwargs})


# Register the hook
client.on("completion:kwargs", log_completion_kwargs)

# Then later, remove it when no longer needed
client.off("completion:kwargs", log_completion_kwargs)

清除钩子

要移除某个特定事件的所有钩子或所有事件的所有钩子:

import instructor
import openai

client = instructor.from_openai(openai.OpenAI())

# Define a simple handler
def log_completion_kwargs(*args, **kwargs):
    print("Logging completion kwargs...")

# Register the hook
client.on("completion:kwargs", log_completion_kwargs)

# Make a request that triggers the hook
resp = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "Hello, world!"}],
    response_model=str,
)

# Clear hooks for a specific event
client.clear("completion:kwargs")

# Register another handler for a different event
def log_response(response):
    print("Logging response...")

client.on("completion:response", log_response)

# Clear all hooks
client.clear()

示例:日志记录和调试

这是一个综合示例,演示了如何使用钩子进行日志记录和调试:

import instructor
import openai
import pydantic


def log_completion_kwargs(*args, **kwargs) -> None:
    print("## Completion kwargs:")
    print(kwargs)
    # Example output:
    # {
    #     "messages": [
    #         {
    #             "role": "user",
    #             "content": "Extract the user name and age from the following text: 'John is 20 years old'",
    #         }
    #     ],
    #     "model": "gpt-4o-mini",
    #     "tools": [
    #         {
    #             "type": "function",
    #             "function": {
    #                 "name": "User",
    #                 "description": "Correctly extracted `User` with all the required parameters with correct types",
    #                 "parameters": {
    #                     "properties": {
    #                         "name": {"title": "Name", "type": "string"},
    #                         "age": {"title": "Age", "type": "integer"},
    #                     },
    #                     "required": ["age", "name"],
    #                     "type": "object",
    #                 },
    #             },
    #         }
    #     ],
    #     "tool_choice": {"type": "function", "function": {"name": "User"}},
    # }


def log_completion_response(response) -> None:
    print("## Completion response:")
    # Example output:
    # {
    #     'id': 'chatcmpl-AWl4Mj5Jrv7m7JkOTIiHXSldQIOFm',
    #     'choices': [
    #         {
    #             'finish_reason': 'stop',
    #             'index': 0,
    #             'logprobs': None,
    #             'message': {
    #                 'content': None,
    #                 'refusal': None,
    #                 'role': 'assistant',
    #                 'audio': None,
    #                 'function_call': None,
    #                 'tool_calls': [
    #                     {
    #                         'id': 'call_6oQ9WXxeSiVEV71B9IYtsbIE',
    #                         'function': {
    #                             'arguments': '{"name":"John","age":-1}',
    #                             'name': 'User',
    #                         },
    #                         'type': 'function',
    #                     }
    #                 ],
    #             },
    #         }
    #     ],
    #     'created': 1732370794,
    #     'model': 'gpt-4o-mini-2024-07-18',
    #     'object': 'chat.completion',
    #     'service_tier': None,
    #     'system_fingerprint': 'fp_0705bf87c0',
    #     'usage': {
    #         'completion_tokens': 10,
    #         'prompt_tokens': 87,
    #         'total_tokens': 97,
    #         'completion_tokens_details': {
    #             'audio_tokens': 0,
    #             'reasoning_tokens': 0,
    #             'accepted_prediction_tokens': 0,
    #             'rejected_prediction_tokens': 0,
    #         },
    #         'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0},
    #     },
    # }
    print(response.model_dump())


def handle_completion_error(error: Exception) -> None:
    print(f"## Completion error: {error}")
    print(f"Type: {type(error).__name__}")
    print(f"Message: {str(error)}")


def log_parse_error(error: Exception) -> None:
    print(f"## Parse error: {error}")
    print(f"Type: {type(error).__name__}")
    print(f"Message: {str(error)}")


# Handler for a custom logger that records how many errors have occurred
class ErrorCounter:
    def __init__(self) -> None:
        self.error_count = 0

    def count_error(self, error: Exception) -> None:
        self.error_count += 1
        print(f"Error count: {self.error_count}")


client = instructor.from_openai(openai.OpenAI())

# Register the hooks
client.on("completion:kwargs", log_completion_kwargs)
client.on("completion:response", log_completion_response)
client.on("completion:error", handle_completion_error)
client.on("parse:error", log_parse_error)

# Example with error counter
error_counter = ErrorCounter()
client.on("completion:error", error_counter.count_error)
client.on("parse:error", error_counter.count_error)

# Define a model for extraction
class User(pydantic.BaseModel):
    name: str
    age: int

# Try extraction with a potentially problematic input
try:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "user",
                "content": "Extract the user name and age: 'John is twenty years old'",
            }
        ],
        response_model=User,
    )
    print(f"Extracted: {resp}")
except Exception as e:
    print(f"Main exception caught: {e}")

# Check the error count
print(f"Total errors recorded: {error_counter.error_count}")

高级:创建自定义钩子

虽然 Instructor 库提供了一些内置钩子,但你可能需要为特定用例创建自定义钩子。你可以通过扩展 HookName 枚举并为你的自定义事件添加处理程序来实现:

from typing import Protocol, Any
from enum import Enum
from instructor.hooks import Hooks, HookName


# Extend the HookName enum
class CustomHookName(str, Enum):
    CUSTOM_EVENT = "custom:event"

    # Make it compatible with the base HookName enum
    COMPLETION_KWARGS = HookName.COMPLETION_KWARGS.value
    COMPLETION_RESPONSE = HookName.COMPLETION_RESPONSE.value
    COMPLETION_ERROR = HookName.COMPLETION_ERROR.value
    PARSE_ERROR = HookName.PARSE_ERROR.value
    COMPLETION_LAST_ATTEMPT = HookName.COMPLETION_LAST_ATTEMPT.value


# Create a hooks instance
hooks = Hooks()


# Define a handler
def custom_handler(data):
    print(f"Custom event: {data}")


# Register the handler
hooks.on(CustomHookName.CUSTOM_EVENT, custom_handler)

# Emit the event
hooks.emit(CustomHookName.CUSTOM_EVENT, {"data": "value"})

使用 Protocol 类型确保类型安全

钩子系统使用 Python 的 Protocol 类型来为处理函数提供更好的类型安全。这有助于在开发阶段捕获错误,并提供更好的 IDE 支持和自动补全。

如果你正在编写自己的处理程序,可以指定适当的类型:

from instructor.hooks import CompletionErrorHandler


def my_error_handler(error: Exception) -> None:
    print(f"Error occurred: {error}")


# Type checking will verify this is a valid error handler
handler: CompletionErrorHandler = my_error_handler

client.on("completion:error", handler)

测试中的钩子

钩子对于测试特别有用,因为它们允许你在不修改应用程序代码的情况下检查参数和响应:

import unittest
from unittest.mock import Mock
import instructor
import openai


class TestMyApp(unittest.TestCase):
    def test_completion(self):
        client = instructor.from_openai(openai.OpenAI())
        mock_handler = Mock()

        client.on("completion:response", mock_handler)

        # Call your code that uses the client
        result = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": "Hello"}],
            response_model=str,
        )

        # Verify the mock was called
        mock_handler.assert_called_once()

        # You can also inspect the arguments
        response_arg = mock_handler.call_args[0][0]
        self.assertEqual(response_arg.model, "gpt-3.5-turbo")

这种方法允许你在不模拟整个客户端的情况下测试你的代码。

使用钩子

from openai import OpenAI
import instructor


# Initialize client
client = instructor.patch(OpenAI())

# Example with all hooks enabled (default)
response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    response_model=str,
    messages=[{"role": "user", "content": "Hello!"}],
)
from enum import Enum, auto
import instructor
from openai import OpenAI


# Define standard hook names
class HookName(Enum):
    COMPLETION_KWARGS = auto()
    COMPLETION_RESPONSE = auto()
    COMPLETION_ERROR = auto()
    COMPLETION_LAST_ATTEMPT = auto()
    PARSE_ERROR = auto()


# Create a new enum for custom hooks
class CustomHookName(Enum):
    MY_CUSTOM_HOOK = "my_custom_hook"
    ANOTHER_HOOK = "another_hook"


# Initialize client with custom hooks
client = instructor.patch(OpenAI())