|
| 1 | +# V2 Core Architecture |
| 2 | + |
| 3 | +This document covers the v2 core infrastructure, including the registry-based design, exception handling, and component interactions. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The v2 architecture uses a hierarchical registry system for managing provider modes and their corresponding handlers. It replaces the monolithic v1 approach with modular, composable components: |
| 8 | + |
| 9 | +- **Registry**: Central mode/handler management |
| 10 | +- **Handlers**: Pluggable request/response/reask handlers per mode |
| 11 | +- **Patch**: Unified function patching mechanism |
| 12 | +- **Retry**: Intelligent retry with registry-based handling |
| 13 | +- **Exceptions**: Organized, centralized error handling |
| 14 | + |
| 15 | +## Core Components |
| 16 | + |
| 17 | +### Protocols (`instructor/v2/core/protocols.py`) |
| 18 | + |
| 19 | +Type-safe interfaces for handlers: |
| 20 | + |
| 21 | +- `RequestHandler` - Prepares request kwargs for a mode |
| 22 | +- `ResponseParser` - Parses API response into Pydantic model |
| 23 | +- `ReaskHandler` - Handles validation failures for retry |
| 24 | + |
| 25 | +### Mode Registry (`instructor/v2/core/registry.py`) |
| 26 | + |
| 27 | +The mode registry manages all available modes for each provider. It maps `(Provider, Mode)` tuples to their handler implementations. |
| 28 | + |
| 29 | +**Key Features**: |
| 30 | + |
| 31 | +- Provider/mode combination lookup |
| 32 | +- Handler registration and retrieval |
| 33 | +- Mode listing and discovery |
| 34 | +- Fast O(1) lookups for handler dispatch |
| 35 | + |
| 36 | +**Registry API**: |
| 37 | + |
| 38 | +```python |
| 39 | +from instructor.v2.core.registry import mode_registry |
| 40 | +from instructor import Provider, Mode |
| 41 | + |
| 42 | +# Get handlers (preferred) |
| 43 | +handlers = mode_registry.get_handlers(Provider.ANTHROPIC, Mode.TOOLS) |
| 44 | + |
| 45 | +# Query |
| 46 | +modes = mode_registry.get_modes_for_provider(Provider.ANTHROPIC) |
| 47 | +is_registered = mode_registry.is_registered(Provider.ANTHROPIC, Mode.TOOLS) |
| 48 | +``` |
| 49 | + |
| 50 | +Handlers are registered via `@register_mode_handler` decorator (see Handler Registration). |
| 51 | + |
| 52 | +### Patch Mechanism (`instructor/v2/core/patch.py`) |
| 53 | + |
| 54 | +Wraps provider API functions to add structured output support. Auto-detects sync/async, validates mode registration, injects default models, and integrates with registry handlers. |
| 55 | + |
| 56 | +```python |
| 57 | +from instructor.v2.core.patch import patch_v2 |
| 58 | + |
| 59 | +patched_create = patch_v2( |
| 60 | + client.messages.create, |
| 61 | + provider=Provider.ANTHROPIC, |
| 62 | + mode=Mode.TOOLS, |
| 63 | + default_model="claude-3-5-sonnet-20241022" |
| 64 | +) |
| 65 | +``` |
| 66 | + |
| 67 | +### Retry Logic (`instructor/v2/core/retry.py`) |
| 68 | + |
| 69 | +Handles retries with registry-based reask logic. On `ValidationError`, uses registry handlers to generate reask prompts and retries up to `max_retries` times. |
| 70 | + |
| 71 | +## Exception Handling |
| 72 | + |
| 73 | +V2 exceptions inherit from `instructor.core.exceptions.InstructorError`: |
| 74 | + |
| 75 | +- `RegistryError` - Mode not registered or handler lookup failure |
| 76 | +- `ValidationContextError` - Conflicting `context`/`validation_context` parameters |
| 77 | +- `InstructorRetryException` - Max retries exceeded with full attempt context |
| 78 | + |
| 79 | +`RegistryValidationMixin` provides validation utilities used internally. |
| 80 | + |
| 81 | +## Handler System |
| 82 | + |
| 83 | +Handlers are pluggable components that implement provider-specific logic. They can be implemented as classes (using `ModeHandler` ABC) or as standalone functions (using Protocols). |
| 84 | + |
| 85 | +### Handler Base Class (`instructor/v2/core/handler.py`) |
| 86 | + |
| 87 | +The `ModeHandler` abstract base class provides a structured way to implement handlers: |
| 88 | + |
| 89 | +```python |
| 90 | +from instructor.v2.core.handler import ModeHandler |
| 91 | +from pydantic import BaseModel |
| 92 | +from typing import Any |
| 93 | + |
| 94 | +class MyModeHandler(ModeHandler): |
| 95 | + """Handler for a specific mode.""" |
| 96 | + |
| 97 | + def prepare_request( |
| 98 | + self, |
| 99 | + response_model: type[BaseModel] | None, |
| 100 | + kwargs: dict[str, Any], |
| 101 | + ) -> tuple[type[BaseModel] | None, dict[str, Any]]: |
| 102 | + """Prepare request kwargs for this mode.""" |
| 103 | + # Modify kwargs for mode-specific requirements |
| 104 | + return response_model, kwargs |
| 105 | + |
| 106 | + def handle_reask( |
| 107 | + self, |
| 108 | + kwargs: dict[str, Any], |
| 109 | + response: Any, |
| 110 | + exception: Exception, |
| 111 | + ) -> dict[str, Any]: |
| 112 | + """Handle validation failure and prepare retry.""" |
| 113 | + # Modify kwargs for retry attempt |
| 114 | + return kwargs |
| 115 | + |
| 116 | + def parse_response( |
| 117 | + self, |
| 118 | + response: Any, |
| 119 | + response_model: type[BaseModel], |
| 120 | + validation_context: dict[str, Any] | None = None, |
| 121 | + strict: bool | None = None, |
| 122 | + ) -> BaseModel: |
| 123 | + """Parse API response into validated Pydantic model.""" |
| 124 | + # Extract and validate response |
| 125 | + return response_model.model_validate(...) |
| 126 | +``` |
| 127 | + |
| 128 | +### Handler Registration |
| 129 | + |
| 130 | +All handlers must be registered using the `@register_mode_handler` decorator. This is the **only supported way** to register handlers in v2. |
| 131 | + |
| 132 | +```python |
| 133 | +from instructor.v2.core.decorators import register_mode_handler |
| 134 | +from instructor import Provider, Mode |
| 135 | +from instructor.v2.core.handler import ModeHandler |
| 136 | + |
| 137 | +@register_mode_handler(Provider.ANTHROPIC, Mode.TOOLS) |
| 138 | +class AnthropicToolsHandler(ModeHandler): |
| 139 | + """Handler automatically registered on import. |
| 140 | + |
| 141 | + The decorator internally calls mode_registry.register() with the |
| 142 | + handler methods mapped to the protocol functions. |
| 143 | + """ |
| 144 | + |
| 145 | + def prepare_request(self, response_model, kwargs): |
| 146 | + # Implementation |
| 147 | + return response_model, kwargs |
| 148 | + |
| 149 | + def handle_reask(self, kwargs, response, exception): |
| 150 | + # Implementation |
| 151 | + return kwargs |
| 152 | + |
| 153 | + def parse_response(self, response, response_model, **kwargs): |
| 154 | + # Implementation |
| 155 | + return response_model.model_validate(...) |
| 156 | +``` |
| 157 | + |
| 158 | +**How it works**: The decorator instantiates the handler class and calls `mode_registry.register()` with the handler's methods mapped to the protocol functions: |
| 159 | + |
| 160 | +- `handler.prepare_request` → `request_handler` |
| 161 | +- `handler.handle_reask` → `reask_handler` |
| 162 | +- `handler.parse_response` → `response_parser` |
| 163 | + |
| 164 | +**Benefits**: |
| 165 | + |
| 166 | +- Automatic registration on import (no manual calls needed) |
| 167 | +- Clean, declarative syntax |
| 168 | +- Type-safe and consistent with the codebase pattern |
| 169 | +- Used by all v2 providers (see `instructor/v2/providers/anthropic/handlers.py`) |
| 170 | + |
| 171 | +**Important**: Direct calls to `mode_registry.register()` are not supported. All handlers must use the `@register_mode_handler` decorator. |
| 172 | + |
| 173 | +## Execution Flow |
| 174 | + |
| 175 | +### Sync Execution Path |
| 176 | + |
| 177 | +```text |
| 178 | +Client.create() with response_model |
| 179 | + ↓ |
| 180 | +patch_v2() [registry validation] |
| 181 | + ↓ |
| 182 | +new_create_sync() |
| 183 | + ├─ handle_context() [parameter validation] |
| 184 | + └─ retry_sync_v2() [retry logic] |
| 185 | + ├─ validate_mode_registration() |
| 186 | + ├─ For each attempt: |
| 187 | + │ ├─ Call original API |
| 188 | + │ ├─ Get handlers from registry |
| 189 | + │ ├─ Parse response via handler |
| 190 | + │ ├─ On success → return |
| 191 | + │ └─ On ValidationError: |
| 192 | + │ ├─ Record attempt |
| 193 | + │ ├─ Get reask via handler |
| 194 | + │ └─ Retry |
| 195 | + └─ Max retries exceeded → InstructorRetryException |
| 196 | +``` |
| 197 | + |
| 198 | +### Async Execution Path |
| 199 | + |
| 200 | +```text |
| 201 | +AsyncClient.create() with response_model |
| 202 | + ↓ |
| 203 | +patch_v2() [registry validation] |
| 204 | + ↓ |
| 205 | +new_create_async() |
| 206 | + ├─ handle_context() [parameter validation] |
| 207 | + └─ retry_async_v2() [async retry logic] |
| 208 | + ├─ validate_mode_registration() |
| 209 | + ├─ For each attempt: |
| 210 | + │ ├─ Await API call |
| 211 | + │ ├─ Get handlers from registry |
| 212 | + │ ├─ Parse response via handler |
| 213 | + │ ├─ On success → return |
| 214 | + │ └─ On ValidationError: |
| 215 | + │ ├─ Record attempt |
| 216 | + │ ├─ Get reask via handler |
| 217 | + │ └─ Retry |
| 218 | + └─ Max retries exceeded → InstructorRetryException |
| 219 | +``` |
| 220 | + |
| 221 | +## Error Handling Strategy |
| 222 | + |
| 223 | +- **Fail fast**: Mode validation at patch time |
| 224 | +- **Context validation**: `context`/`validation_context` conflict detection |
| 225 | +- **Comprehensive logging**: All stages logged with attempt numbers |
| 226 | +- **Exception chaining**: Full context preserved in exception chain |
| 227 | + |
| 228 | +## Configuration |
| 229 | + |
| 230 | +- **Mode**: Specified when creating client (`from_anthropic(client, mode=Mode.TOOLS)`) |
| 231 | +- **Default Model**: Injected via `patch_v2(..., default_model="...")` if not provided in request |
| 232 | +- **Max Retries**: Per-request via `max_retries=3` or `Retrying(...)` instance |
| 233 | + |
| 234 | +## Adding a New Provider |
| 235 | + |
| 236 | +1. **Add Provider Enum** (`instructor/utils.py`): |
| 237 | + |
| 238 | +```python |
| 239 | +class Provider(Enum): |
| 240 | + YOUR_PROVIDER = "your_provider" |
| 241 | +``` |
| 242 | + |
| 243 | +2. **Create Handler** (`instructor/v2/providers/your_provider/handlers.py`): |
| 244 | + |
| 245 | +```python |
| 246 | +from instructor.v2.core.handler import ModeHandler |
| 247 | +from instructor.v2.core.decorators import register_mode_handler |
| 248 | +from instructor import Provider, Mode |
| 249 | + |
| 250 | +@register_mode_handler(Provider.YOUR_PROVIDER, Mode.TOOLS) |
| 251 | +class YourProviderToolsHandler(ModeHandler): |
| 252 | + def prepare_request(self, response_model, kwargs): |
| 253 | + # Convert response_model to provider tools format |
| 254 | + return response_model, kwargs |
| 255 | + |
| 256 | + def parse_response(self, response, response_model, **kwargs): |
| 257 | + # Extract and validate response |
| 258 | + return response_model.model_validate(...) |
| 259 | + |
| 260 | + def handle_reask(self, kwargs, response, exception): |
| 261 | + # Add error message for retry |
| 262 | + return kwargs |
| 263 | +``` |
| 264 | + |
| 265 | +3. **Create Factory** (`instructor/v2/providers/your_provider/client.py`): |
| 266 | + |
| 267 | +```python |
| 268 | +from instructor.v2.providers.your_provider import handlers # noqa: F401 |
| 269 | +from instructor.v2.core.patch import patch_v2 |
| 270 | +from instructor import Instructor, AsyncInstructor, Mode, Provider |
| 271 | + |
| 272 | +@overload |
| 273 | +def from_your_provider(client: YourProviderClient, mode=Mode.TOOLS) -> Instructor: ... |
| 274 | + |
| 275 | +def from_your_provider(client, mode=Mode.TOOLS): |
| 276 | + patched_create = patch_v2( |
| 277 | + client.messages.create, |
| 278 | + provider=Provider.YOUR_PROVIDER, |
| 279 | + mode=mode, |
| 280 | + ) |
| 281 | + return Instructor(client=client, create=patched_create, mode=mode) |
| 282 | +``` |
| 283 | + |
| 284 | +4. **Export** (`instructor/v2/providers/your_provider/__init__.py`): |
| 285 | + |
| 286 | +```python |
| 287 | +from . import handlers # noqa: F401 |
| 288 | +from .client import from_your_provider |
| 289 | +__all__ = ["from_your_provider"] |
| 290 | +``` |
| 291 | + |
| 292 | +See `instructor/v2/providers/anthropic/` for a complete example. |
| 293 | + |
| 294 | +## Migration from V1 to V2 |
| 295 | + |
| 296 | +**Key Differences**: |
| 297 | + |
| 298 | +- Handler dispatch: Registry lookup (v2) vs direct calls (v1) |
| 299 | +- Mode validation: At patch time (v2) vs runtime (v1) |
| 300 | +- Exception context: Enhanced with attempt tracking (v2) |
| 301 | + |
| 302 | +**Migration**: Add explicit `mode` parameter to factory functions: |
| 303 | + |
| 304 | +```python |
| 305 | +# V1 |
| 306 | +client = from_anthropic(anthropic_client) |
| 307 | + |
| 308 | +# V2 |
| 309 | +client = from_anthropic(anthropic_client, mode=Mode.TOOLS) |
| 310 | +``` |
| 311 | + |
| 312 | +V1 code continues to work during transition period. |
| 313 | + |
| 314 | +## Best Practices |
| 315 | + |
| 316 | +- **New Modes**: Define in `instructor.Mode` enum, create handler, register via decorator |
| 317 | +- **Error Handling**: Validate early, provide context, preserve exception chains |
| 318 | +- **Testing**: Test both success and failure paths, verify registry registration |
| 319 | + |
| 320 | +## Module Organization |
| 321 | + |
| 322 | +```text |
| 323 | +instructor/v2/ |
| 324 | +├── __init__.py # V2 exports (ModeHandler, Protocols, Registry, Providers) |
| 325 | +├── README.md # This document |
| 326 | +├── core/ |
| 327 | +│ ├── __init__.py # Core exports (Protocols, Registry) |
| 328 | +│ ├── decorators.py # @register_mode_handler decorator |
| 329 | +│ ├── exceptions.py # Exception classes & validation utilities |
| 330 | +│ ├── handler.py # ModeHandler abstract base class |
| 331 | +│ ├── patch.py # Patching mechanism |
| 332 | +│ ├── protocols.py # Protocol definitions (RequestHandler, etc.) |
| 333 | +│ ├── registry.py # Mode registry implementation |
| 334 | +│ └── retry.py # Retry logic (sync & async) |
| 335 | +└── providers/ |
| 336 | + ├── __init__.py # Provider exports |
| 337 | + └── anthropic/ # Anthropic provider implementation |
| 338 | + ├── __init__.py # Provider exports |
| 339 | + ├── client.py # from_anthropic factory function |
| 340 | + └── handlers.py # Handler implementations (TOOLS, JSON, etc.) |
| 341 | +``` |
| 342 | + |
| 343 | +## Module Exports |
| 344 | + |
| 345 | +- `instructor.v2`: `ModeHandler`, `mode_registry`, `RequestHandler`, `ReaskHandler`, `ResponseParser`, `from_anthropic` |
| 346 | +- `instructor.v2.core`: Core types and registry |
| 347 | +- `instructor.v2.providers.anthropic`: `from_anthropic` |
0 commit comments