mirror of
https://github.com/hwchase17/langchain.git
synced 2025-08-15 07:36:08 +00:00
plan
This commit is contained in:
parent
9507d0f21c
commit
0148b1a03c
635
v1_implementation_plan_v2.md
Normal file
635
v1_implementation_plan_v2.md
Normal file
@ -0,0 +1,635 @@
|
|||||||
|
# Provider V1 Content Support Implementation Plan
|
||||||
|
|
||||||
|
Implementation plan for adding messages with standard content block types (v1) to any chat model provider.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### V1 Chat Model Foundation
|
||||||
|
|
||||||
|
The v1 architecture uses:
|
||||||
|
|
||||||
|
- **Separate File Structure**: Create `base_v1.py` alongside existing `base.py`
|
||||||
|
- **BaseChatModelV1 Inheritance**: Inherit from `langchain_core.language_models.v1.chat_models.BaseChatModelV1`
|
||||||
|
- **Native V1 Messages**: Use `AIMessageV1`, `HumanMessageV1`, etc. throughout
|
||||||
|
- **No `output_version` Field**: Always return v1 format (no conditional logic) - this is a deviation from our prior plan
|
||||||
|
- **Standardized Content Blocks**: Native support for `TextContentBlock`, `ImageContentBlock`, `ToolCallContentBlock`, etc. (defined in `libs/core/langchain_core/messages/content_blocks.py`)
|
||||||
|
|
||||||
|
### Implementation Pattern
|
||||||
|
|
||||||
|
Based on successful implementations, we have both:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Existing v0 implementation (unchanged)
|
||||||
|
libs/partners/{provider}/langchain_{provider}/chat_models/base.py
|
||||||
|
class ChatProvider(BaseChatModel): # v0 base class
|
||||||
|
output_version: str = "v0" # conditional v1 support - REMOVE????
|
||||||
|
|
||||||
|
# NEW FILE: v1 implementation
|
||||||
|
libs/partners/{provider}/langchain_{provider}/chat_models/base_v1.py
|
||||||
|
class ChatProviderV1(BaseChatModelV1): # v1 base class
|
||||||
|
# Inherits from BaseChatModelV1
|
||||||
|
```
|
||||||
|
|
||||||
|
### V1 Message Flow Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_core.messages.v1 import HumanMessage as HumanMessageV1
|
||||||
|
from langchain_core.messages.content_blocks import TextContentBlock, ImageContentBlock
|
||||||
|
|
||||||
|
user_message = HumanMessageV1(content=[
|
||||||
|
TextContentBlock(type="text", text="Hello"),
|
||||||
|
ImageContentBlock(type="image", mime_type="image/jpeg", base64="...")
|
||||||
|
])
|
||||||
|
|
||||||
|
# Internal: Convert v1 to provider's native format
|
||||||
|
# (For API calls, either direct or via SDK)
|
||||||
|
provider_request = convert_from_v1_to_provider_api(user_message)
|
||||||
|
provider_response = provider_api.chat(provider_request)
|
||||||
|
|
||||||
|
# Output: Convert provider response to v1 format
|
||||||
|
ai_message_v1 = convert_to_v1_from_provider_response(provider_response)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Framework
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure
|
||||||
|
|
||||||
|
#### 1.1 Create Separate V1 File Structure
|
||||||
|
|
||||||
|
**New File:** `langchain_{provider}/chat_models/base_v1.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Provider chat model v1 implementation.
|
||||||
|
|
||||||
|
As much as possible, this should be a direct copy of the v0
|
||||||
|
implementation, but with the following changes:
|
||||||
|
|
||||||
|
- Inherit from BaseChatModelV1
|
||||||
|
- Use native v1 messages (AIMessageV1, HumanMessageV1, etc.)
|
||||||
|
- `content` is a list of ContentBlock objects (TextContentBlock, ImageContentBlock, etc.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from langchain_core.language_models.v1.chat_models import (
|
||||||
|
BaseChatModelV1,
|
||||||
|
agenerate_from_stream,
|
||||||
|
generate_from_stream,
|
||||||
|
)
|
||||||
|
from langchain_core.messages.v1 import (
|
||||||
|
AIMessage as AIMessageV1,
|
||||||
|
AIMessageChunk as AIMessageChunkV1,
|
||||||
|
HumanMessage as HumanMessageV1,
|
||||||
|
MessageV1,
|
||||||
|
SystemMessage as SystemMessageV1,
|
||||||
|
ToolMessage as ToolMessageV1,
|
||||||
|
ResponseMetadata,
|
||||||
|
)
|
||||||
|
from langchain_core.messages import content_blocks as types
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
class BaseChatProviderV1(BaseChatModelV1):
|
||||||
|
"""Base class for provider v1 chat models."""
|
||||||
|
|
||||||
|
model_name: str = Field(default="default-model", alias="model")
|
||||||
|
"""Model name to use."""
|
||||||
|
|
||||||
|
# Provider-specific configuration fields
|
||||||
|
# ... (copy from existing base.py but adapt for v1 where applicable)
|
||||||
|
|
||||||
|
class ChatProviderV1(BaseChatProviderV1):
|
||||||
|
"""Provider chat model with v1 messages."""
|
||||||
|
|
||||||
|
def _generate_stream(
|
||||||
|
self,
|
||||||
|
messages: list[MessageV1],
|
||||||
|
stop: Optional[list[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Iterator[AIMessageChunkV1]:
|
||||||
|
"""Generate streaming response with v1 messages."""
|
||||||
|
|
||||||
|
def _agenerate_stream(
|
||||||
|
self,
|
||||||
|
messages: list[MessageV1],
|
||||||
|
stop: Optional[list[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> AsyncIterator[AIMessageChunkV1]:
|
||||||
|
"""Generate async streaming response with v1 messages."""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 Update Package Exports
|
||||||
|
|
||||||
|
Include the new v1 chat model in the package exports:
|
||||||
|
|
||||||
|
**File:** `langchain_{provider}/chat_models/__init__.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_{provider}.chat_models.base import ChatProvider
|
||||||
|
from langchain_{provider}.chat_models.base_v1 import ChatProvider as ChatProviderV1
|
||||||
|
|
||||||
|
__all__ = ["ChatProvider", "ChatProviderV1"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 Create Conversion Utilities
|
||||||
|
|
||||||
|
**File:** `langchain_{provider}/chat_models/_compat.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""V1 message conversion utilities."""
|
||||||
|
|
||||||
|
from typing import Any, Union, cast
|
||||||
|
from langchain_core.messages.v1 import MessageV1, AIMessageV1
|
||||||
|
from langchain_core.messages import content_blocks as types
|
||||||
|
|
||||||
|
def _convert_from_v1_to_provider_format(message: MessageV1) -> dict[str, Any]:
|
||||||
|
"""Convert v1 message to provider API format."""
|
||||||
|
|
||||||
|
def _convert_to_v1_from_provider_format(response: dict[str, Any]) -> AIMessageV1:
|
||||||
|
"""Convert provider API response to v1 message(s)."""
|
||||||
|
|
||||||
|
def _format_v1_message_content(content: list[ContentBlock]) -> Any:
|
||||||
|
"""Format v1 content blocks for provider API."""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Message Processing
|
||||||
|
|
||||||
|
#### 2.1 Input Message Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _convert_from_v1_to_provider_format(message: MessageV1) -> dict[str, Any]:
|
||||||
|
"""Convert v1 message to provider API format."""
|
||||||
|
if isinstance(message, HumanMessageV1):
|
||||||
|
return _convert_human_message_v1(message)
|
||||||
|
elif isinstance(message, AIMessageV1):
|
||||||
|
return _convert_ai_message_v1(message)
|
||||||
|
elif isinstance(message, SystemMessageV1):
|
||||||
|
return _convert_system_message_v1(message)
|
||||||
|
elif isinstance(message, ToolMessageV1):
|
||||||
|
return _convert_tool_message_v1(message)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported message type: {type(message)}")
|
||||||
|
|
||||||
|
def _convert_content_blocks_to_provider_format(content: list[ContentBlock]) -> list[dict]:
|
||||||
|
"""Convert v1 content blocks to provider API format.
|
||||||
|
|
||||||
|
Shared across all message types since they all support the same content blocks.
|
||||||
|
"""
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
|
for block in content:
|
||||||
|
block_type = block.get("type")
|
||||||
|
if block_type == "text":
|
||||||
|
# The format here will vary depending on the provider's API
|
||||||
|
content_parts.append({
|
||||||
|
"type": "text",
|
||||||
|
"text": block.get("text", "")
|
||||||
|
})
|
||||||
|
elif block_type == "image":
|
||||||
|
content_parts.append(_convert_image_block_to_provider(block))
|
||||||
|
elif block_type == "audio":
|
||||||
|
content_parts.append(_convert_audio_block_to_provider(block))
|
||||||
|
elif block_type == "tool_call":
|
||||||
|
# Skip tool calls - handled separately via tool_calls property
|
||||||
|
continue
|
||||||
|
# Add other content block types...
|
||||||
|
|
||||||
|
return content_parts
|
||||||
|
|
||||||
|
def _convert_human_message_v1(message: HumanMessageV1) -> dict[str, Any]:
|
||||||
|
"""Convert HumanMessageV1 to provider format."""
|
||||||
|
# The format here will vary depending on the provider's API
|
||||||
|
return {
|
||||||
|
"role": "user",
|
||||||
|
"content": _convert_content_blocks_to_provider_format(message.content),
|
||||||
|
"name": message.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _convert_ai_message_v1(message: AIMessageV1) -> dict[str, Any]:
|
||||||
|
"""Convert AIMessageV1 to provider format."""
|
||||||
|
# Extract text content for main content field
|
||||||
|
# The format here will vary depending on the provider's API
|
||||||
|
text_content = ""
|
||||||
|
for block in message.content:
|
||||||
|
if block.get("type") == "text":
|
||||||
|
text_content += block.get("text", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": text_content,
|
||||||
|
"tool_calls": [_convert_tool_call_to_provider(tc) for tc in message.tool_calls],
|
||||||
|
"name": message.name,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Output Message Generation
|
||||||
|
|
||||||
|
Convert provider responses directly to v1 format:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_core.messages.content_blocks import (
|
||||||
|
TextContentBlock,
|
||||||
|
ToolCall,
|
||||||
|
ReasoningContentBlock,
|
||||||
|
AudioContentBlock,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
|
||||||
|
def _convert_to_v1_from_provider_format(response: dict[str, Any]) -> AIMessageV1:
|
||||||
|
"""Convert provider response to AIMessageV1."""
|
||||||
|
# The format here will vary depending on the provider's API
|
||||||
|
# (This is a dummy implementation)
|
||||||
|
content: list[ContentBlock] = []
|
||||||
|
|
||||||
|
if text_content := response.get("content"):
|
||||||
|
# For example, if the text response from the provider comes in as `content`:
|
||||||
|
if isinstance(text_content, str) and text_content:
|
||||||
|
content.append(TextContentBlock(type="text", text=text_content))
|
||||||
|
elif isinstance(text_content, list):
|
||||||
|
# If the content is a list of text items
|
||||||
|
for item in text_content:
|
||||||
|
if item.get("type") == "text":
|
||||||
|
content.append(TextContentBlock(
|
||||||
|
type="text",
|
||||||
|
text=item.get("text", "")
|
||||||
|
))
|
||||||
|
|
||||||
|
if tool_calls := response.get("tool_calls"):
|
||||||
|
# Similarly, if the provider returns tool calls under `tool_calls`:
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
content.append(ToolCall(
|
||||||
|
type="tool_call",
|
||||||
|
id=tool_call.get("id", ""),
|
||||||
|
name=tool_call.get("function", {}).get("name", ""),
|
||||||
|
args=tool_call.get("function", {}).get("arguments", {}),
|
||||||
|
))
|
||||||
|
|
||||||
|
# Some providers call this `reasoning`, `thoughts`, `thinking`
|
||||||
|
if reasoning := response.get("reasoning"):
|
||||||
|
# May opt to insert reasoning in specific order depending on API design
|
||||||
|
content.insert(0, ReasoningContentBlock(
|
||||||
|
type="reasoning",
|
||||||
|
reasoning=reasoning
|
||||||
|
))
|
||||||
|
|
||||||
|
if audio := response.get("audio"):
|
||||||
|
content.append(AudioContentBlock(
|
||||||
|
type="audio",
|
||||||
|
# Provider-specific fields via PEP 728 TypedDict extra items (common for multimodal)
|
||||||
|
))
|
||||||
|
|
||||||
|
return AIMessageV1(
|
||||||
|
content=content,
|
||||||
|
response_metadata=ResponseMetadata(
|
||||||
|
model_name=response.get("model"), # or whatever key the provider uses for model name
|
||||||
|
usage=response.get("usage", {}),
|
||||||
|
# Other provider-specific metadata
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Streaming Implementation
|
||||||
|
|
||||||
|
Implement streaming that yields `AIMessageChunkV1` directly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _generate_stream(
|
||||||
|
self,
|
||||||
|
messages: list[MessageV1],
|
||||||
|
stop: Optional[list[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Iterator[AIMessageChunkV1]:
|
||||||
|
"""Generate streaming response with native v1 chunks."""
|
||||||
|
|
||||||
|
# Convert v1 messages to provider format
|
||||||
|
provider_messages = [
|
||||||
|
_convert_from_v1_to_provider_format(msg) for msg in messages
|
||||||
|
]
|
||||||
|
|
||||||
|
# Stream from provider API
|
||||||
|
for chunk in self._provider_stream(provider_messages, **kwargs):
|
||||||
|
# Convert each chunk to v1 format
|
||||||
|
v1_chunk = _convert_chunk_to_v1(chunk)
|
||||||
|
yield v1_chunk
|
||||||
|
|
||||||
|
def _convert_chunk_to_v1(chunk: dict[str, Any]) -> AIMessageChunkV1:
|
||||||
|
"""Convert provider chunk to AIMessageChunkV1."""
|
||||||
|
content: list[ContentBlock] = []
|
||||||
|
|
||||||
|
if delta := chunk.get("delta"):
|
||||||
|
if text := delta.get("content"):
|
||||||
|
content.append(types.TextContentBlock(type="text", text=text))
|
||||||
|
|
||||||
|
if tool_calls := delta.get("tool_calls"):
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
if tool_call.get("id"):
|
||||||
|
content.append(types.ToolCallContentBlock(
|
||||||
|
type="tool_call",
|
||||||
|
id=tool_call["id"],
|
||||||
|
name=tool_call.get("function", {}).get("name", ""),
|
||||||
|
args=tool_call.get("function", {}).get("arguments", ""),
|
||||||
|
))
|
||||||
|
|
||||||
|
return AIMessageChunkV1(
|
||||||
|
content=content,
|
||||||
|
response_metadata=ResponseMetadata(
|
||||||
|
model_name=chunk.get("model"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: _convert_chunk_to_v1 does NOT handle summing - that's handled by AIMessageChunkV1.__add__ automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Content Block Support
|
||||||
|
|
||||||
|
#### 4.1 Standard Content Block Types
|
||||||
|
|
||||||
|
Support all standard v1 content blocks as defined in `libs/core/langchain_core/messages/content_blocks.py`
|
||||||
|
|
||||||
|
#### 4.2 Provider-Specific Extensions
|
||||||
|
|
||||||
|
Use **PEP 728 TypedDict with Typed Extra Items** for provider-specific content within standard blocks:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Provider-specific fields within standard content blocks
|
||||||
|
# PEP 728 allows extra keys beyond the defined TypedDict structure
|
||||||
|
text_block_with_extras: TextContentBlock = {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hello world",
|
||||||
|
"provider_confidence": 0.95, # Extra field: provider-specific confidence score
|
||||||
|
"provider_metadata": { # Extra field: nested provider metadata
|
||||||
|
"model_tier": "premium",
|
||||||
|
"processing_time_ms": 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**About [PEP 728](https://peps.python.org/pep-0728/):** extends TypedDict to support typed extra items beyond the explicitly defined keys. This allows providers to add custom fields while maintaining type safety for the standard fields.
|
||||||
|
|
||||||
|
**Extra Item Types:** Standard content blocks support extra keys via PEP 728 with `extra_items=Any`, meaning provider-specific fields can be of any type. This provides:
|
||||||
|
|
||||||
|
- **Core fields** (like `type`, `text`, `id`) are **strongly typed** according to the TypedDict definition
|
||||||
|
- **Extra fields** (provider-specific extensions) are typed as `Any`, allowing complete flexibility
|
||||||
|
- **Type safety** is maintained for the standard fields while allowing arbitrary extensions
|
||||||
|
|
||||||
|
This is the most flexible approach - providers can add any kind of metadata, configuration, or custom data they need without breaking the type system or requiring changes to the core LangChain types.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _handle_provider_specific_content(block: dict[str, Any]) -> ContentBlock:
|
||||||
|
"""Handle provider-specific content blocks."""
|
||||||
|
|
||||||
|
# For known provider extensions, create typed blocks
|
||||||
|
if block.get("type") == "provider_specific_type":
|
||||||
|
return cast(types.ContentBlock, ProviderSpecificContentBlock(...))
|
||||||
|
|
||||||
|
# For unknown types, use NonStandardContentBlock
|
||||||
|
return cast(types.ContentBlock, types.NonStandardContentBlock(
|
||||||
|
type="non_standard",
|
||||||
|
content=block
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Testing Framework
|
||||||
|
|
||||||
|
#### 5.1 V1 Tests
|
||||||
|
|
||||||
|
Create comprehensive tests for v1 functionality:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_core.messages.v1 import HumanMessage as HumanMessageV1
|
||||||
|
from langchain_core.messages.content_blocks import TextContentBlock, ImageContentBlock, AudioContentBlock
|
||||||
|
|
||||||
|
def test_v1_native_message_handling():
|
||||||
|
"""Test native v1 message processing."""
|
||||||
|
llm = ChatProviderV1(model="test-model")
|
||||||
|
|
||||||
|
message = HumanMessageV1(content=[
|
||||||
|
TextContentBlock(type="text", text="Hello"),
|
||||||
|
ImageContentBlock(type="image", mime_type="image/jpeg", base64="base64data...")
|
||||||
|
])
|
||||||
|
|
||||||
|
response = llm.invoke([message])
|
||||||
|
|
||||||
|
assert isinstance(response, AIMessageV1)
|
||||||
|
assert isinstance(response.content, list)
|
||||||
|
assert all(isinstance(block, ContentBlock) for block in response.content)
|
||||||
|
|
||||||
|
# Verify content block structure and content
|
||||||
|
text_blocks = [b for b in response.content if b.get("type") == "text"]
|
||||||
|
assert len(text_blocks) >= 1, "Response should contain at least one text block"
|
||||||
|
assert text_blocks[0]["text"], "Text block should contain non-empty text content"
|
||||||
|
assert isinstance(text_blocks[0]["text"], str), "Text content should be a string"
|
||||||
|
|
||||||
|
def test_v1_streaming_consistency():
|
||||||
|
"""Test that streaming and non-streaming produce equivalent results."""
|
||||||
|
llm = ChatProviderV1(model="test-model")
|
||||||
|
|
||||||
|
message = HumanMessageV1(content=[
|
||||||
|
TextContentBlock(type="text", text="Hello"),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Non-streaming
|
||||||
|
non_stream = llm.invoke([message])
|
||||||
|
|
||||||
|
# Streaming
|
||||||
|
stream_chunks = list(llm.stream([message]))
|
||||||
|
stream_combined = AIMessageV1(content=[])
|
||||||
|
for chunk in stream_chunks:
|
||||||
|
stream_combined = stream_combined + chunk
|
||||||
|
|
||||||
|
# Should be equivalent
|
||||||
|
assert non_stream.content == stream_combined.content
|
||||||
|
|
||||||
|
def test_v1_content_block_types():
|
||||||
|
"""Test all supported content block types."""
|
||||||
|
llm = ChatProviderV1(model="test-model")
|
||||||
|
|
||||||
|
# Test each content block type
|
||||||
|
test_cases = [
|
||||||
|
TextContentBlock(type="text", text="Hello"),
|
||||||
|
ImageContentBlock(type="image", mime_type="image/jpeg", base64="base64data..."),
|
||||||
|
AudioContentBlock(type="audio", mime_type="audio/wav", base64="audiodata..."),
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
for block in test_cases:
|
||||||
|
message = HumanMessageV1(content=[block])
|
||||||
|
response = llm.invoke([message])
|
||||||
|
assert isinstance(response, AIMessageV1)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 Migration Tests
|
||||||
|
|
||||||
|
Test compatibility between v0 and v1 implementations:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_core.messages.content_blocks import TextContentBlock
|
||||||
|
|
||||||
|
def test_v0_v1_feature_parity():
|
||||||
|
"""Test that v1 implementation has feature parity with v0."""
|
||||||
|
llm_v0 = ChatProvider(model="test-model")
|
||||||
|
llm_v1 = ChatProviderV1(model="test-model")
|
||||||
|
|
||||||
|
# Test basic functionality
|
||||||
|
v0_response = llm_v0.invoke("Hello")
|
||||||
|
v1_response = llm_v1.invoke([HumanMessageV1("Hello")
|
||||||
|
])
|
||||||
|
v1_response = llm_v1.invoke([HumanMessageV1(content=[
|
||||||
|
TextContentBlock(type="text", text="Hello")
|
||||||
|
])])
|
||||||
|
|
||||||
|
# Extract text content for comparison
|
||||||
|
v0_text = v0_response.content
|
||||||
|
v1_text = "".join(
|
||||||
|
block.get("text", "") for block in v1_response.content
|
||||||
|
if block.get("type") == "text"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should produce equivalent text output
|
||||||
|
assert v0_text == v1_text
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: Documentation and Migration
|
||||||
|
|
||||||
|
#### 6.1 V1 Documentation
|
||||||
|
|
||||||
|
Document the v1 implementation separately:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ChatProviderV1(BaseChatProvider):
|
||||||
|
"""Provider chat model with native v1 content block support.
|
||||||
|
|
||||||
|
This implementation provides native support for structured content blocks
|
||||||
|
and always returns AIMessageV1 format responses.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Basic text conversation:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from langchain_{provider}.chat_models import ChatProviderV1
|
||||||
|
from langchain_core.messages.v1 import HumanMessage
|
||||||
|
|
||||||
|
llm = ChatProviderV1(model="provider-model")
|
||||||
|
response = llm.invoke([
|
||||||
|
HumanMessage(content=[
|
||||||
|
TextContentBlock(type="text", text="Hello!")
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
# Response is always structured
|
||||||
|
print(response.content)
|
||||||
|
# [{"type": "text", "text": "Hello! How can I help?"}] # Type will be TextContentBlock
|
||||||
|
|
||||||
|
Multi-modal input:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response = llm.invoke([
|
||||||
|
HumanMessage(content=[
|
||||||
|
TextContentBlock(type="text", text="Describe this image:"),
|
||||||
|
ImageContentBlock(
|
||||||
|
type="image",
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
base64="base64_encoded_image"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 Migration Guide
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Migrating to V1 Chat Models
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
V1 chat models provide native support for standard content blocks and always return `AIMessageV1` format responses.
|
||||||
|
|
||||||
|
## Key Differences
|
||||||
|
|
||||||
|
### Import Changes
|
||||||
|
```python
|
||||||
|
# V0 implementation (conditional v1 support)
|
||||||
|
from langchain_{provider} import ChatProvider
|
||||||
|
llm = ChatProvider(output_version="v1")
|
||||||
|
|
||||||
|
# V1 implementation (v1 support)
|
||||||
|
from langchain_{provider}.chat_models import ChatProviderV1
|
||||||
|
llm = ChatProviderV1()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Format
|
||||||
|
|
||||||
|
```python
|
||||||
|
# V0 mixed format (strings or lists)
|
||||||
|
message = HumanMessage(content="Hello") # or content=[...]
|
||||||
|
|
||||||
|
# V1 structured format (always lists)
|
||||||
|
from langchain_core.messages.v1 import HumanMessage as HumanMessageV1
|
||||||
|
message = HumanMessageV1(content=[{"type": "text", "text": "Hello"}])
|
||||||
|
```
|
||||||
|
|
||||||
|
# Checklist
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Core Infrastructure
|
||||||
|
- [ ] Create `base_v1.py` file with `BaseChatModelV1` inheritance
|
||||||
|
- [ ] Implement `ChatProviderV1` class with native v1 support
|
||||||
|
- [ ] Create `_compat.py` with v1 conversion utilities
|
||||||
|
- [ ] Update package `__init__.py` exports
|
||||||
|
|
||||||
|
Message Processing
|
||||||
|
- [ ] Implement `_convert_from_v1_to_provider_format()` for API requests
|
||||||
|
- [ ] Implement `_convert_to_v1_from_provider_format()` for responses
|
||||||
|
- [ ] Add streaming support with `AIMessageChunkV1`
|
||||||
|
- [ ] Handle all standard content block types
|
||||||
|
|
||||||
|
Content Block Support
|
||||||
|
- [ ] Support `TextContentBlock` for text content
|
||||||
|
- [ ] Support `ImageContentBlock` for images (where applicable)
|
||||||
|
- [ ] Support `AudioContentBlock` for audio (where applicable)
|
||||||
|
- [ ] Support `ToolCallContentBlock` for tool calls
|
||||||
|
- [ ] Support `ReasoningContentBlock` for reasoning (where applicable)
|
||||||
|
- [ ] Support other multimodal content blocks (where applicable)
|
||||||
|
- [ ] Handle provider-specific fields with `extra_items`
|
||||||
|
- [ ] Handle provider-specific blocks by returning `NonStandardContentBlock`
|
||||||
|
|
||||||
|
Testing
|
||||||
|
- [ ] Create comprehensive unit tests for v1 functionality
|
||||||
|
- [ ] Add streaming vs non-streaming consistency tests
|
||||||
|
- [ ] Test all supported content block types
|
||||||
|
- [ ] Add migration compatibility tests
|
||||||
|
- [ ] Performance benchmarks vs v0 implementation
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
- [ ] Update class docstrings with v1 examples
|
||||||
|
- [ ] Create migration guide from v0 to v1
|
||||||
|
- [ ] Document provider-specific content block support
|
||||||
|
- [ ] Add troubleshooting section
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. **Native V1 Support**: Full `BaseChatModelV1` implementation
|
||||||
|
2. **Zero Conversion Overhead**: No runtime format conversion
|
||||||
|
3. **Feature Complete**: All provider capabilities available natively
|
||||||
|
4. **Type Safe**: Full typing for all content blocks and operations
|
||||||
|
5. **Well Documented**: Clear migration path and usage examples
|
||||||
|
6. **Performance Optimized**: Better performance than v0 conditional approach
|
||||||
|
|
||||||
|
## Provider-Specific Considerations
|
||||||
|
|
||||||
|
When implementing v1 support for your specific provider:
|
||||||
|
|
||||||
|
### Content Block Mapping
|
||||||
|
- Map your provider's native content types to standard v1 content blocks
|
||||||
|
- Use `NonStandardContentBlock` for provider-specific content that doesn't map to standard types
|
||||||
|
- Leverage PEP 728 TypedDict extra items for provider-specific fields/metadata within LangChain's standard blocks
|
||||||
|
|
||||||
|
### Tool Call Handling
|
||||||
|
- Map your provider's tool calling format to v1 `ToolCall` content blocks
|
||||||
|
- Handle both standard function calls and provider-specific built-in tools
|
||||||
|
- Preserve provider-specific tool call metadata using extra fields
|
||||||
|
|
||||||
|
### Streaming Implementation
|
||||||
|
- Ensure streaming chunks are properly typed as `AIMessageChunkV1`
|
||||||
|
- Implement proper chunk merging using the `+` operator
|
||||||
|
- Handle provider-specific streaming features (like reasoning) appropriately
|
Loading…
Reference in New Issue
Block a user