mirror of
https://github.com/hwchase17/langchain.git
synced 2025-08-26 21:11:25 +00:00
ehhh
This commit is contained in:
parent
20f4736999
commit
10349b019a
@ -339,6 +339,37 @@ def _get_image_from_data_content_block(block: dict) -> str:
|
||||
raise ValueError(f"Blocks of type {block['type']} not supported.")
|
||||
```
|
||||
|
||||
#### 4.2 NonStandardContentBlock Support
|
||||
|
||||
Unknown content block types should be converted to `NonStandardContentBlock` instead of raising errors. This enables forward compatibility and provider-specific extensions.
|
||||
|
||||
Update message processing to handle unknown content blocks:
|
||||
|
||||
```python
|
||||
from langchain_core.messages.content_blocks import NonStandardContentBlock
|
||||
|
||||
def _convert_unknown_content_block_to_non_standard(block: dict) -> NonStandardContentBlock:
|
||||
"""Convert unknown content block to NonStandardContentBlock format."""
|
||||
return NonStandardContentBlock(
|
||||
type="non_standard",
|
||||
value=block
|
||||
)
|
||||
```
|
||||
|
||||
In `_convert_messages_to_ollama_messages()`, replace error handling for unknown blocks:
|
||||
|
||||
```python
|
||||
# Instead of raising ValueError for unknown blocks:
|
||||
elif is_data_content_block(content_part):
|
||||
image = _get_image_from_data_content_block(content_part)
|
||||
images.append(image)
|
||||
else:
|
||||
# Convert unknown blocks to NonStandardContentBlock
|
||||
non_standard_block = _convert_unknown_content_block_to_non_standard(content_part)
|
||||
# Should we convert non-standard blocks to a string or keep them as-is?
|
||||
continue
|
||||
```
|
||||
|
||||
#### 4.2 Update Message Conversion
|
||||
|
||||
Enhance `_convert_messages_to_ollama_messages()` to handle v1 content:
|
||||
|
167
libs/partners/ollama/langchain_ollama/_compat.py
Normal file
167
libs/partners/ollama/langchain_ollama/_compat.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""Compatibility module for handling v1 message format conversions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, Optional, cast
|
||||
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
is_data_content_block,
|
||||
)
|
||||
from langchain_core.messages.content_blocks import (
|
||||
NonStandardContentBlock,
|
||||
ReasoningContentBlock,
|
||||
TextContentBlock,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class ToolCallReferenceBlock(TypedDict):
|
||||
"""Reference to a tool call (metadata only).
|
||||
|
||||
This is used in v1 content blocks to reference tool calls
|
||||
without duplicating the full tool call data.
|
||||
"""
|
||||
|
||||
type: Literal["tool_call"]
|
||||
id: Optional[str]
|
||||
|
||||
|
||||
def _convert_unknown_content_block_to_non_standard(
|
||||
block: dict,
|
||||
) -> NonStandardContentBlock:
|
||||
"""Convert unknown content block to NonStandardContentBlock format.
|
||||
|
||||
This enables forward compatibility by preserving unknown content block types
|
||||
instead of raising errors.
|
||||
|
||||
Args:
|
||||
block: Unknown content block dictionary.
|
||||
|
||||
Returns:
|
||||
NonStandardContentBlock containing the original block data.
|
||||
"""
|
||||
return NonStandardContentBlock(type="non_standard", value=block)
|
||||
|
||||
|
||||
def _convert_from_v1_message(message: AIMessage) -> AIMessage:
|
||||
"""Convert a v1 message to Ollama-compatible request format.
|
||||
|
||||
Returns AIMessage with v0-style content and reasoning in ``additional_kwargs``.
|
||||
|
||||
If input is already v0 format, returns unchanged.
|
||||
|
||||
Args:
|
||||
message: The message to convert, potentially in v1 format.
|
||||
|
||||
Returns:
|
||||
AIMessage in v0 format suitable for Ollama API.
|
||||
"""
|
||||
if not isinstance(message.content, list):
|
||||
# Already v0 format or string content (determined by content type)
|
||||
return message
|
||||
|
||||
# Extract components from v1 content blocks
|
||||
text_content = ""
|
||||
reasoning_content = None
|
||||
|
||||
for block in message.content:
|
||||
if isinstance(block, dict):
|
||||
block_type = block.get("type")
|
||||
if block_type == "text":
|
||||
text_content += block.get("text", "")
|
||||
elif block_type == "reasoning":
|
||||
# Extract reasoning for additional_kwargs
|
||||
reasoning_content = block.get("reasoning", "")
|
||||
elif block_type == "tool_call":
|
||||
# Skip - handled via tool_calls property
|
||||
continue
|
||||
elif is_data_content_block(block):
|
||||
# Keep data blocks as-is (images already supported)
|
||||
continue
|
||||
else:
|
||||
# Convert unknown content blocks to NonStandardContentBlock
|
||||
# TODO what to do from here?
|
||||
_convert_unknown_content_block_to_non_standard(block)
|
||||
continue
|
||||
|
||||
# Update message with extracted content
|
||||
updates: dict[str, Any] = {"content": text_content if text_content else ""}
|
||||
if reasoning_content:
|
||||
additional_kwargs = dict(message.additional_kwargs)
|
||||
additional_kwargs["reasoning_content"] = reasoning_content
|
||||
updates["additional_kwargs"] = additional_kwargs
|
||||
|
||||
return message.model_copy(update=updates)
|
||||
|
||||
|
||||
def _convert_to_v1_message(message: AIMessage) -> AIMessage:
|
||||
"""Convert an Ollama message to v1 format.
|
||||
|
||||
Args:
|
||||
message: AIMessage in v0 format from Ollama.
|
||||
|
||||
Returns:
|
||||
AIMessage in v1 format with content blocks.
|
||||
"""
|
||||
new_content: list[Any] = []
|
||||
|
||||
# Handle reasoning content first (from additional_kwargs)
|
||||
additional_kwargs = dict(message.additional_kwargs)
|
||||
if reasoning_content := additional_kwargs.pop("reasoning_content", None):
|
||||
reasoning_block = ReasoningContentBlock(
|
||||
type="reasoning", reasoning=reasoning_content
|
||||
)
|
||||
new_content.append(reasoning_block)
|
||||
|
||||
# Convert text content to content blocks
|
||||
if isinstance(message.content, str) and message.content:
|
||||
text_block = TextContentBlock(type="text", text=message.content)
|
||||
new_content.append(text_block)
|
||||
|
||||
# Convert tool calls to content blocks
|
||||
for tool_call in message.tool_calls:
|
||||
if tool_call_id := tool_call.get("id"):
|
||||
# Create a tool call reference block (metadata only)
|
||||
tool_call_block = ToolCallReferenceBlock(type="tool_call", id=tool_call_id)
|
||||
new_content.append(dict(tool_call_block))
|
||||
|
||||
# Handle any non-standard content blocks that might be stored in additional_kwargs
|
||||
if non_standard_blocks := additional_kwargs.pop(
|
||||
"non_standard_content_blocks", None
|
||||
):
|
||||
new_content.extend(non_standard_blocks)
|
||||
|
||||
return message.model_copy(
|
||||
update={"content": new_content, "additional_kwargs": additional_kwargs}
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_v1_chunk(chunk: AIMessageChunk) -> AIMessageChunk:
|
||||
"""Convert an Ollama chunk to v1 format.
|
||||
|
||||
Args:
|
||||
chunk: AIMessageChunk in v0 format from Ollama.
|
||||
|
||||
Returns:
|
||||
AIMessageChunk in v1 format with content blocks.
|
||||
"""
|
||||
result = _convert_to_v1_message(cast(AIMessage, chunk))
|
||||
return cast(AIMessageChunk, result)
|
||||
|
||||
|
||||
def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
||||
"""Convert a LangChain v0.3 AIMessage to v1 format.
|
||||
|
||||
This handles compatibility for users migrating stored/cached AIMessage objects
|
||||
from LangChain v0.3.
|
||||
|
||||
Args:
|
||||
message: AIMessage potentially in v0.3 format.
|
||||
|
||||
Returns:
|
||||
AIMessage in v1 format.
|
||||
"""
|
||||
# For now, treat v0.3 messages the same as v0 messages
|
||||
return _convert_to_v1_message(message)
|
@ -36,6 +36,7 @@ from langchain_core.messages import (
|
||||
is_data_content_block,
|
||||
)
|
||||
from langchain_core.messages.ai import UsageMetadata
|
||||
from langchain_core.messages.content_blocks import NonStandardContentBlock
|
||||
from langchain_core.messages.tool import tool_call
|
||||
from langchain_core.output_parsers import (
|
||||
JsonOutputKeyToolsParser,
|
||||
@ -57,7 +58,12 @@ from pydantic.json_schema import JsonSchemaValue
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Self, is_typeddict
|
||||
|
||||
from ._utils import validate_model
|
||||
from langchain_ollama._compat import (
|
||||
_convert_from_v1_message,
|
||||
_convert_to_v1_chunk,
|
||||
_convert_to_v1_message,
|
||||
)
|
||||
from langchain_ollama._utils import validate_model
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -208,6 +214,23 @@ def _get_image_from_data_content_block(block: dict) -> str:
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def _convert_unknown_content_block_to_non_standard(
|
||||
block: dict,
|
||||
) -> NonStandardContentBlock:
|
||||
"""Convert unknown content block to NonStandardContentBlock format.
|
||||
|
||||
This enables forward compatibility by preserving unknown content block types
|
||||
instead of raising errors.
|
||||
|
||||
Args:
|
||||
block: Unknown content block dictionary.
|
||||
|
||||
Returns:
|
||||
NonStandardContentBlock containing the original block data.
|
||||
"""
|
||||
return NonStandardContentBlock(type="non_standard", value=block)
|
||||
|
||||
|
||||
def _is_pydantic_class(obj: Any) -> bool:
|
||||
return isinstance(obj, type) and is_basemodel_subclass(obj)
|
||||
|
||||
@ -418,12 +441,52 @@ class ChatOllama(BaseChatModel):
|
||||
|
||||
AIMessage(content='The word "strawberry" contains **three \'r\' letters**. Here\'s a breakdown for clarity:\n\n- The spelling of "strawberry" has two parts ... be 3.\n\nTo be thorough, let\'s confirm with an online source or common knowledge.\n\nI can recall that "strawberry" has: s-t-r-a-w-b-e-r-r-y — yes, three r\'s.\n\nPerhaps it\'s misspelled by some, but standard is correct.\n\nSo I think the response should be 3.\n'}, response_metadata={'model': 'deepseek-r1:8b', 'created_at': '2025-07-08T19:33:55.891269Z', 'done': True, 'done_reason': 'stop', 'total_duration': 98232561292, 'load_duration': 28036792, 'prompt_eval_count': 10, 'prompt_eval_duration': 40171834, 'eval_count': 3615, 'eval_duration': 98163832416, 'model_name': 'deepseek-r1:8b'}, id='run--18f8269f-6a35-4a7c-826d-b89d52c753b3-0', usage_metadata={'input_tokens': 10, 'output_tokens': 3615, 'total_tokens': 3625})
|
||||
|
||||
V1 Output Format:
|
||||
.. code-block:: python
|
||||
|
||||
llm = ChatOllama(model="llama3.1", output_version="v1")
|
||||
response = llm.invoke("Hello")
|
||||
|
||||
# Response content is now a list of standard output content blocks:
|
||||
response.content
|
||||
# [{"type": "text", "text": "Hello! How can I help you?"}] # follows TextContentBlock format
|
||||
|
||||
# With reasoning enabled:
|
||||
llm_reasoning = ChatOllama(
|
||||
model="deepseek-r1:8b",
|
||||
output_version="v1",
|
||||
reasoning=True
|
||||
)
|
||||
response = llm_reasoning.invoke("What is 2+2?")
|
||||
|
||||
# Response includes reasoning and text blocks:
|
||||
response.content
|
||||
# [
|
||||
# {"type": "reasoning", "reasoning": "I need to add 2+2..."}, # ReasoningContentBlock
|
||||
# {"type": "text", "text": "2+2 equals 4."} # TextContentBlock
|
||||
# ]
|
||||
|
||||
""" # noqa: E501, pylint: disable=line-too-long
|
||||
|
||||
model: str
|
||||
"""Model name to use."""
|
||||
|
||||
output_version: str = "v0"
|
||||
"""Version of AIMessage output format to use.
|
||||
|
||||
This field is used to roll-out new output formats for chat model AIMessages
|
||||
in a backwards-compatible way.
|
||||
|
||||
Supported values:
|
||||
|
||||
- ``"v0"``: AIMessage format as of langchain-ollama 0.x.x.
|
||||
- ``"v1"``: v1 of LangChain cross-provider standard.
|
||||
|
||||
``output_version="v1"`` is recommended.
|
||||
|
||||
.. versionadded:: 0.4.0
|
||||
"""
|
||||
|
||||
reasoning: Optional[bool] = None
|
||||
"""Controls the reasoning/thinking mode for
|
||||
`supported models <https://ollama.com/search?c=thinking>`__.
|
||||
@ -627,6 +690,10 @@ class ChatOllama(BaseChatModel):
|
||||
) -> Sequence[Message]:
|
||||
ollama_messages: list = []
|
||||
for message in messages:
|
||||
# Handle v1 format messages in input
|
||||
if isinstance(message, AIMessage) and isinstance(message.content, list):
|
||||
# This is v1 format message (content is list) - convert for Ollama API
|
||||
message = _convert_from_v1_message(message)
|
||||
role: str
|
||||
tool_call_id: Optional[str] = None
|
||||
tool_calls: Optional[list[dict[str, Any]]] = None
|
||||
@ -663,6 +730,12 @@ class ChatOllama(BaseChatModel):
|
||||
content += f"\n{content_part['text']}"
|
||||
elif content_part.get("type") == "tool_use":
|
||||
continue
|
||||
elif content_part.get("type") == "tool_call":
|
||||
# Skip - handled by tool_calls property
|
||||
continue
|
||||
elif content_part.get("type") == "reasoning":
|
||||
# Skip - handled by reasoning parameter
|
||||
continue
|
||||
elif content_part.get("type") == "image_url":
|
||||
image_url = None
|
||||
temp_image_url = content_part.get("image_url")
|
||||
@ -692,12 +765,10 @@ class ChatOllama(BaseChatModel):
|
||||
image = _get_image_from_data_content_block(content_part)
|
||||
images.append(image)
|
||||
else:
|
||||
msg = (
|
||||
"Unsupported message content type. "
|
||||
"Must either have type 'text' or type 'image_url' "
|
||||
"with a string 'image_url' field."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# Convert unknown content blocks to NonStandardContentBlock
|
||||
# TODO what to do with these?
|
||||
_convert_unknown_content_block_to_non_standard(content_part)
|
||||
continue
|
||||
# Should convert to ollama.Message once role includes tool,
|
||||
# and tool_call_id is in Message
|
||||
msg_: dict = {
|
||||
@ -820,13 +891,18 @@ class ChatOllama(BaseChatModel):
|
||||
messages, stop, run_manager, verbose=self.verbose, **kwargs
|
||||
)
|
||||
generation_info = final_chunk.generation_info
|
||||
chat_generation = ChatGeneration(
|
||||
message=AIMessage(
|
||||
ai_message = AIMessage(
|
||||
content=final_chunk.text,
|
||||
usage_metadata=cast(AIMessageChunk, final_chunk.message).usage_metadata,
|
||||
tool_calls=cast(AIMessageChunk, final_chunk.message).tool_calls,
|
||||
additional_kwargs=final_chunk.message.additional_kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
if self.output_version == "v1":
|
||||
ai_message = _convert_to_v1_message(ai_message)
|
||||
|
||||
chat_generation = ChatGeneration(
|
||||
message=ai_message,
|
||||
generation_info=generation_info,
|
||||
)
|
||||
return ChatResult(generations=[chat_generation])
|
||||
@ -890,6 +966,11 @@ class ChatOllama(BaseChatModel):
|
||||
generation_info=generation_info,
|
||||
)
|
||||
|
||||
if self.output_version == "v1":
|
||||
chunk.message = _convert_to_v1_chunk(
|
||||
cast(AIMessageChunk, chunk.message)
|
||||
)
|
||||
|
||||
yield chunk
|
||||
|
||||
def _stream(
|
||||
@ -966,6 +1047,11 @@ class ChatOllama(BaseChatModel):
|
||||
generation_info=generation_info,
|
||||
)
|
||||
|
||||
if self.output_version == "v1":
|
||||
chunk.message = _convert_to_v1_chunk(
|
||||
cast(AIMessageChunk, chunk.message)
|
||||
)
|
||||
|
||||
yield chunk
|
||||
|
||||
async def _astream(
|
||||
@ -994,13 +1080,18 @@ class ChatOllama(BaseChatModel):
|
||||
messages, stop, run_manager, verbose=self.verbose, **kwargs
|
||||
)
|
||||
generation_info = final_chunk.generation_info
|
||||
chat_generation = ChatGeneration(
|
||||
message=AIMessage(
|
||||
ai_message = AIMessage(
|
||||
content=final_chunk.text,
|
||||
usage_metadata=cast(AIMessageChunk, final_chunk.message).usage_metadata,
|
||||
tool_calls=cast(AIMessageChunk, final_chunk.message).tool_calls,
|
||||
additional_kwargs=final_chunk.message.additional_kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
if self.output_version == "v1":
|
||||
ai_message = _convert_to_v1_message(ai_message)
|
||||
|
||||
chat_generation = ChatGeneration(
|
||||
message=ai_message,
|
||||
generation_info=generation_info,
|
||||
)
|
||||
return ChatResult(generations=[chat_generation])
|
||||
|
@ -54,7 +54,6 @@ select = [
|
||||
"C4", # flake8-comprehensions
|
||||
"COM", # flake8-commas
|
||||
"D", # pydocstyle
|
||||
"DOC", # pydoclint
|
||||
"E", # pycodestyle error
|
||||
"EM", # flake8-errmsg
|
||||
"F", # pyflakes
|
||||
|
@ -41,8 +41,6 @@ def test_stream_no_reasoning(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" not in result.content and "</think>" not in result.content
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("model"), [("deepseek-r1:1.5b")])
|
||||
@ -66,8 +64,6 @@ async def test_astream_no_reasoning(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" not in result.content and "</think>" not in result.content
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("model"), [("deepseek-r1:1.5b")])
|
||||
@ -91,6 +87,8 @@ def test_stream_reasoning_none(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" in result.content and "</think>" in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
@ -116,6 +114,8 @@ async def test_astream_reasoning_none(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" in result.content and "</think>" in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
@ -179,8 +179,9 @@ def test_invoke_no_reasoning(model: str) -> None:
|
||||
message = HumanMessage(content=SAMPLE)
|
||||
result = llm.invoke([message])
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" not in result.content and "</think>" not in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
@ -192,8 +193,9 @@ async def test_ainvoke_no_reasoning(model: str) -> None:
|
||||
message = HumanMessage(content=SAMPLE)
|
||||
result = await llm.ainvoke([message])
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" not in result.content and "</think>" not in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
@ -207,6 +209,8 @@ def test_invoke_reasoning_none(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" in result.content and "</think>" in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
@ -220,6 +224,8 @@ async def test_ainvoke_reasoning_none(model: str) -> None:
|
||||
assert result.content
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
assert "<think>" in result.content and "</think>" in result.content
|
||||
# For backward compatibility: if reasoning_content exists, check it
|
||||
if "reasoning_content" in result.additional_kwargs:
|
||||
assert "<think>" not in result.additional_kwargs["reasoning_content"]
|
||||
assert "</think>" not in result.additional_kwargs["reasoning_content"]
|
||||
|
||||
|
@ -0,0 +1,272 @@
|
||||
"""Integration tests for ChatOllama v1 format functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
DEFAULT_MODEL_NAME = "llama3.1"
|
||||
REASONING_MODEL_NAME = "deepseek-r1:8b"
|
||||
|
||||
|
||||
@pytest.mark.requires("ollama")
|
||||
class TestChatOllamaV1Integration:
|
||||
"""Integration tests for ChatOllama v1 format functionality."""
|
||||
|
||||
def test_v1_output_format_basic_chat(self) -> None:
|
||||
"""Test basic chat functionality with v1 output format."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
message = HumanMessage("Say hello")
|
||||
result = llm.invoke([message])
|
||||
|
||||
# Result should be in v1 format (content as list)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) > 0
|
||||
|
||||
# Should have at least one TextContentBlock
|
||||
text_blocks = [
|
||||
block
|
||||
for block in result.content
|
||||
if isinstance(block, dict) and block.get("type") == "text"
|
||||
]
|
||||
assert len(text_blocks) > 0
|
||||
assert "text" in text_blocks[0]
|
||||
|
||||
def test_v1_output_format_streaming(self) -> None:
|
||||
"""Test streaming functionality with v1 output format."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
message = HumanMessage("Count to 3")
|
||||
chunks = list(llm.stream([message]))
|
||||
|
||||
# All chunks should be in v1 format
|
||||
for chunk in chunks:
|
||||
assert isinstance(chunk.content, list)
|
||||
# Each chunk should have content blocks
|
||||
if chunk.content: # Some chunks might be empty
|
||||
for block in chunk.content:
|
||||
assert isinstance(block, dict)
|
||||
assert "type" in block
|
||||
|
||||
def test_v1_input_with_v0_output(self) -> None:
|
||||
"""Test that v1 input works with v0 output format."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v0")
|
||||
|
||||
# Send v1 format message as input
|
||||
v1_message = AIMessage(
|
||||
content=[{"type": "text", "text": "Hello, how are you?"}]
|
||||
)
|
||||
human_message = HumanMessage("Fine, thanks!")
|
||||
|
||||
result = llm.invoke([v1_message, human_message])
|
||||
|
||||
# Output should be in v0 format (content as string)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, str)
|
||||
|
||||
def test_v1_input_with_v1_output(self) -> None:
|
||||
"""Test that v1 input works with v1 output format."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
# Send v1 format message as input
|
||||
v1_message = AIMessage(
|
||||
content=[{"type": "text", "text": "Hello, how are you?"}]
|
||||
)
|
||||
human_message = HumanMessage("Fine, thanks!")
|
||||
|
||||
result = llm.invoke([v1_message, human_message])
|
||||
|
||||
# Output should be in v1 format (content as list)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
def test_v0_input_with_v1_output(self) -> None:
|
||||
"""Test that v0 input works with v1 output format."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
# Send v0 format message as input
|
||||
v0_message = AIMessage(content="Hello, how are you?")
|
||||
human_message = HumanMessage("Fine, thanks!")
|
||||
|
||||
result = llm.invoke([v0_message, human_message])
|
||||
|
||||
# Output should be in v1 format (content as list)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
@pytest.mark.parametrize("output_version", ["v0", "v1"])
|
||||
def test_mixed_message_formats_input(self, output_version: str) -> None:
|
||||
"""Test handling mixed v0 and v1 format messages in input.
|
||||
|
||||
Rare case but you never know...
|
||||
"""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version=output_version)
|
||||
|
||||
messages = [
|
||||
HumanMessage("Hello"),
|
||||
AIMessage(content="Hi there!"), # v0 format
|
||||
HumanMessage("How are you?"),
|
||||
# v1 format
|
||||
AIMessage(content=[{"type": "text", "text": "I'm doing well!"}]),
|
||||
HumanMessage("Great!"),
|
||||
]
|
||||
|
||||
result = llm.invoke(messages)
|
||||
|
||||
# Output format should match output_version setting
|
||||
assert isinstance(result, AIMessage)
|
||||
if output_version == "v0":
|
||||
assert isinstance(result.content, str)
|
||||
else:
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
|
||||
@pytest.mark.requires("ollama")
|
||||
class TestChatOllamaV1WithReasoning:
|
||||
"""Integration tests for ChatOllama v1 format with reasoning functionality."""
|
||||
|
||||
def test_v1_output_with_reasoning_enabled(self) -> None:
|
||||
"""Test v1 output format with reasoning enabled."""
|
||||
# Note: This test requires a reasoning-capable model
|
||||
llm = ChatOllama(
|
||||
model=REASONING_MODEL_NAME,
|
||||
output_version="v1",
|
||||
reasoning=True,
|
||||
)
|
||||
|
||||
message = HumanMessage("What is 2+2? Think step by step.")
|
||||
result = llm.invoke([message])
|
||||
|
||||
# Result should be in v1 format with reasoning block
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
# Should have both reasoning and text blocks
|
||||
reasoning_blocks = [
|
||||
block
|
||||
for block in result.content
|
||||
if isinstance(block, dict) and block.get("type") == "reasoning"
|
||||
]
|
||||
text_blocks = [
|
||||
block
|
||||
for block in result.content
|
||||
if isinstance(block, dict) and block.get("type") == "text"
|
||||
]
|
||||
|
||||
# Should have reasoning content when reasoning=True
|
||||
assert len(reasoning_blocks) > 0 or len(text_blocks) > 0
|
||||
|
||||
# Should be able to use the reasoning property on the AIMessage
|
||||
# TODO
|
||||
|
||||
def test_v1_input_with_reasoning_content(self) -> None:
|
||||
"""Test v1 input format with reasoning content blocks."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
# Send message with reasoning content
|
||||
reasoning_message = AIMessage(
|
||||
content=[
|
||||
{"type": "reasoning", "reasoning": "I need to be helpful and accurate"},
|
||||
{"type": "text", "text": "I'll do my best to help you."},
|
||||
]
|
||||
)
|
||||
human_message = HumanMessage("Thank you!")
|
||||
|
||||
result = llm.invoke([reasoning_message, human_message])
|
||||
|
||||
# Should process the input correctly and return v1 format
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
|
||||
@pytest.mark.requires("ollama")
|
||||
class TestChatOllamaV1WithTools:
|
||||
"""Integration tests for ChatOllama v1 format with tool calling."""
|
||||
|
||||
def test_v1_output_with_tool_calls(self) -> None:
|
||||
"""Test v1 output format with tool calls."""
|
||||
from langchain_core.tools import tool
|
||||
|
||||
@tool
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"The weather in {location} is sunny."
|
||||
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
llm_with_tools = llm.bind_tools([get_weather])
|
||||
|
||||
message = HumanMessage("What's the weather in Paris?")
|
||||
result = llm_with_tools.invoke([message])
|
||||
|
||||
# Result should be in v1 format
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
# If tool calls were made, should have tool_call blocks
|
||||
if result.tool_calls:
|
||||
tool_call_blocks = [
|
||||
block
|
||||
for block in result.content
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
]
|
||||
assert len(tool_call_blocks) == len(result.tool_calls)
|
||||
|
||||
def test_v1_input_with_tool_call_blocks(self) -> None:
|
||||
"""Test v1 input format with tool call content blocks."""
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version="v1")
|
||||
|
||||
# Send message with tool call content
|
||||
tool_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "I'll check the weather for you."},
|
||||
{"type": "tool_call", "id": "call_123"},
|
||||
],
|
||||
tool_calls=[
|
||||
{"name": "get_weather", "args": {"location": "Paris"}, "id": "call_123"}
|
||||
],
|
||||
)
|
||||
human_message = HumanMessage("Thanks!")
|
||||
|
||||
result = llm.invoke([tool_message, human_message])
|
||||
|
||||
# Should process the input correctly and return v1 format
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, list)
|
||||
|
||||
|
||||
@pytest.mark.requires("ollama")
|
||||
class TestV1BackwardsCompatibility:
|
||||
"""Test backwards compatibility when using v1 format."""
|
||||
|
||||
def test_existing_v0_code_still_works(self) -> None:
|
||||
"""Test that existing v0 code continues to work unchanged."""
|
||||
# This is the default behavior - should not break existing code
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME) # defaults to v0
|
||||
|
||||
message = HumanMessage("Hello")
|
||||
result = llm.invoke([message])
|
||||
|
||||
# Should return v0 format (string content)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert isinstance(result.content, str)
|
||||
|
||||
def test_gradual_migration_v0_to_v1(self) -> None:
|
||||
"""Test gradual migration from v0 to v1 format."""
|
||||
# Test that the same code works with both formats
|
||||
for output_version in ["v0", "v1"]:
|
||||
llm = ChatOllama(model=DEFAULT_MODEL_NAME, output_version=output_version)
|
||||
|
||||
message = HumanMessage("Hello")
|
||||
result = llm.invoke([message])
|
||||
|
||||
assert isinstance(result, AIMessage)
|
||||
if output_version == "v0":
|
||||
assert isinstance(result.content, str)
|
||||
else:
|
||||
assert isinstance(result.content, list)
|
||||
# Should have at least one content block
|
||||
assert len(result.content) > 0
|
381
libs/partners/ollama/tests/unit_tests/test_chat_models_v1.py
Normal file
381
libs/partners/ollama/tests/unit_tests/test_chat_models_v1.py
Normal file
@ -0,0 +1,381 @@
|
||||
"""Test chat model v1 format conversion."""
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, HumanMessage
|
||||
|
||||
from langchain_ollama._compat import (
|
||||
_convert_from_v1_message,
|
||||
_convert_to_v1_chunk,
|
||||
_convert_to_v1_message,
|
||||
_convert_unknown_content_block_to_non_standard,
|
||||
)
|
||||
from langchain_ollama.chat_models import (
|
||||
ChatOllama,
|
||||
)
|
||||
from langchain_ollama.chat_models import (
|
||||
_convert_unknown_content_block_to_non_standard as chat_convert_unknown,
|
||||
)
|
||||
|
||||
|
||||
class TestV1MessageConversion:
|
||||
"""Test v1 message format conversion functions."""
|
||||
|
||||
def test_convert_from_v1_message_with_text_content(self) -> None:
|
||||
"""Test converting v1 message with text content to v0 format."""
|
||||
# Create a v1 message with TextContentBlock
|
||||
v1_message = AIMessage(content=[{"type": "text", "text": "Hello world"}])
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == "Hello world"
|
||||
assert isinstance(result.content, str)
|
||||
|
||||
def test_convert_from_v1_message_with_reasoning_content(self) -> None:
|
||||
"""Test converting v1 message with reasoning content to v0 format."""
|
||||
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{ # ReasoningContentBlock
|
||||
"type": "reasoning",
|
||||
"reasoning": "I need to think about this",
|
||||
},
|
||||
# TextContentBlock
|
||||
{"type": "text", "text": "Hello world"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == "Hello world"
|
||||
assert (
|
||||
result.additional_kwargs["reasoning_content"]
|
||||
== "I need to think about this"
|
||||
)
|
||||
|
||||
def test_convert_from_v1_message_with_tool_call_content(self) -> None:
|
||||
"""Test converting v1 message with tool call content to v0 format."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Let me search for that"},
|
||||
{"type": "tool_call", "id": "tool_123"}, # ToolCallContentBlock
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == "Let me search for that"
|
||||
# Tool calls should be handled via tool_calls property, not content
|
||||
assert "tool_call" not in str(result.content)
|
||||
|
||||
def test_convert_from_v1_message_with_mixed_content(self) -> None:
|
||||
"""Test converting v1 message with mixed content types."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "reasoning", "reasoning": "Let me think"},
|
||||
{"type": "text", "text": "A"},
|
||||
{"type": "text", "text": "B"},
|
||||
{"type": "tool_call", "id": "tool_456"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == "AB"
|
||||
assert result.additional_kwargs["reasoning_content"] == "Let me think"
|
||||
|
||||
def test_convert_from_v1_message_preserves_v0_format(self) -> None:
|
||||
"""Test that v0 format messages are preserved unchanged."""
|
||||
v0_message = AIMessage(content="Hello world")
|
||||
|
||||
result = _convert_from_v1_message(v0_message)
|
||||
|
||||
assert result == v0_message
|
||||
assert result.content == "Hello world"
|
||||
|
||||
def test_convert_to_v1_message_with_text_content(self) -> None:
|
||||
"""Test converting v0 message with text content to v1 format."""
|
||||
v0_message = AIMessage(content="Hello world")
|
||||
|
||||
result = _convert_to_v1_message(v0_message)
|
||||
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 1
|
||||
assert result.content[0] == {"type": "text", "text": "Hello world"}
|
||||
|
||||
def test_convert_to_v1_message_with_reasoning_content(self) -> None:
|
||||
"""Test converting v0 message with reasoning to v1 format."""
|
||||
v0_message = AIMessage(
|
||||
content="Hello world",
|
||||
additional_kwargs={"reasoning_content": "I need to be helpful"},
|
||||
)
|
||||
|
||||
result = _convert_to_v1_message(v0_message)
|
||||
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 2
|
||||
expected_reasoning_block = {
|
||||
"type": "reasoning",
|
||||
"reasoning": "I need to be helpful",
|
||||
}
|
||||
assert result.content[0] == expected_reasoning_block
|
||||
assert result.content[1] == {"type": "text", "text": "Hello world"}
|
||||
assert "reasoning_content" not in result.additional_kwargs
|
||||
|
||||
def test_convert_to_v1_message_with_tool_calls(self) -> None:
|
||||
"""Test converting v0 message with tool calls to v1 format."""
|
||||
v0_message = AIMessage(
|
||||
content="Let me search for that",
|
||||
tool_calls=[
|
||||
{"name": "search", "args": {"query": "test"}, "id": "tool_123"}
|
||||
],
|
||||
)
|
||||
|
||||
result = _convert_to_v1_message(v0_message)
|
||||
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 2
|
||||
assert result.content[0] == {"type": "text", "text": "Let me search for that"}
|
||||
assert result.content[1] == {"type": "tool_call", "id": "tool_123"}
|
||||
|
||||
def test_convert_to_v1_message_with_empty_content(self) -> None:
|
||||
"""Test converting v0 message with empty content to v1 format."""
|
||||
v0_message = AIMessage(content="")
|
||||
|
||||
result = _convert_to_v1_message(v0_message)
|
||||
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 0
|
||||
|
||||
def test_convert_to_v1_chunk(self) -> None:
|
||||
"""Test converting v0 chunk to v1 format."""
|
||||
v0_chunk = AIMessageChunk(content="Hello")
|
||||
|
||||
result = _convert_to_v1_chunk(v0_chunk)
|
||||
|
||||
assert isinstance(result, AIMessageChunk)
|
||||
assert isinstance(result.content, list)
|
||||
assert result.content == [{"type": "text", "text": "Hello"}]
|
||||
|
||||
|
||||
class TestNonStandardContentBlockHandling:
|
||||
"""Test handling of unknown content blocks via NonStandardContentBlock."""
|
||||
|
||||
def test_convert_unknown_content_block_to_non_standard(self) -> None:
|
||||
"""Test conversion of unknown content block to NonStandardContentBlock."""
|
||||
unknown_block = {
|
||||
"type": "custom_block_type",
|
||||
"custom_field": "some_value",
|
||||
"data": {"nested": "content"},
|
||||
}
|
||||
|
||||
result = _convert_unknown_content_block_to_non_standard(unknown_block)
|
||||
|
||||
assert result["type"] == "non_standard"
|
||||
assert result["value"] == unknown_block
|
||||
|
||||
def test_chat_models_convert_unknown_content_block_to_non_standard(self) -> None:
|
||||
"""Test conversion of unknown content block in chat_models module."""
|
||||
unknown_block = {
|
||||
"type": "audio_transcript",
|
||||
"transcript": "Hello world",
|
||||
"confidence": 0.95,
|
||||
}
|
||||
|
||||
result = chat_convert_unknown(unknown_block)
|
||||
|
||||
assert result["type"] == "non_standard"
|
||||
assert result["value"] == unknown_block
|
||||
|
||||
def test_v1_content_with_unknown_type_creates_non_standard_block(self) -> None:
|
||||
"""Test v1 content with unknown block type creates NonStandardContentBlock."""
|
||||
unknown_block = {
|
||||
"type": "future_block_type",
|
||||
"future_field": "future_value",
|
||||
}
|
||||
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
unknown_block,
|
||||
{"type": "text", "text": " world"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
# Unknown types should be converted to NonStandardContentBlock
|
||||
# and skipped in content processing, text content should be preserved
|
||||
assert result.content == "Hello world"
|
||||
|
||||
def test_multiple_unknown_blocks_handled_gracefully(self) -> None:
|
||||
"""Test multiple unknown content blocks are handled gracefully."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Start"},
|
||||
{"type": "unknown_1", "data": "first"},
|
||||
{"type": "text", "text": " middle"},
|
||||
{"type": "unknown_2", "data": "second"},
|
||||
{"type": "text", "text": " end"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
# All unknown blocks should be converted and skipped
|
||||
assert result.content == "Start middle end"
|
||||
|
||||
def test_non_standard_content_block_structure(self) -> None:
|
||||
"""Test that NonStandardContentBlock has correct structure."""
|
||||
original_block = {
|
||||
"type": "custom_provider_block",
|
||||
"provider_specific_field": "value",
|
||||
"metadata": {"version": "1.0"},
|
||||
}
|
||||
|
||||
non_standard_block = _convert_unknown_content_block_to_non_standard(
|
||||
original_block
|
||||
)
|
||||
|
||||
# Verify it matches NonStandardContentBlock structure
|
||||
assert isinstance(non_standard_block, dict)
|
||||
assert non_standard_block["type"] == "non_standard"
|
||||
assert "value" in non_standard_block
|
||||
assert non_standard_block["value"] == original_block
|
||||
|
||||
def test_empty_unknown_block_handling(self) -> None:
|
||||
"""Test handling of empty unknown blocks."""
|
||||
empty_block = {"type": "empty_block"}
|
||||
|
||||
result = _convert_unknown_content_block_to_non_standard(empty_block)
|
||||
|
||||
assert result["type"] == "non_standard"
|
||||
assert result["value"] == empty_block
|
||||
|
||||
|
||||
class TestChatOllamaV1Integration:
|
||||
"""Test ChatOllama integration with v1 format."""
|
||||
|
||||
def test_chat_ollama_default_output_version(self) -> None:
|
||||
"""Test that ChatOllama defaults to v0 output format."""
|
||||
llm = ChatOllama(model="test-model")
|
||||
assert llm.output_version == "v0"
|
||||
|
||||
def test_chat_ollama_v1_output_version_setting(self) -> None:
|
||||
"""Test setting ChatOllama to v1 output format."""
|
||||
llm = ChatOllama(model="test-model", output_version="v1")
|
||||
assert llm.output_version == "v1"
|
||||
|
||||
def test_convert_messages_handles_v1_input(self) -> None:
|
||||
"""Test that _convert_messages_to_ollama_messages handles v1 input."""
|
||||
llm = ChatOllama(model="test-model", output_version="v0")
|
||||
|
||||
# Create a v1 format AIMessage
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "reasoning", "reasoning": "Let me think"},
|
||||
{"type": "text", "text": "Hello world"},
|
||||
]
|
||||
)
|
||||
|
||||
messages = [HumanMessage("Hi"), v1_message]
|
||||
result = llm._convert_messages_to_ollama_messages(messages)
|
||||
|
||||
# Should have processed both messages
|
||||
assert len(result) == 2
|
||||
|
||||
# The AI message should have been converted to v0 format for Ollama API
|
||||
ai_msg = result[1]
|
||||
assert ai_msg["content"] == "Hello world"
|
||||
|
||||
def test_convert_messages_preserves_v0_input(self) -> None:
|
||||
"""Test that _convert_messages_to_ollama_messages preserves v0 input."""
|
||||
llm = ChatOllama(model="test-model", output_version="v0")
|
||||
|
||||
messages = [HumanMessage("Hi"), AIMessage(content="Hello world")]
|
||||
result = llm._convert_messages_to_ollama_messages(messages)
|
||||
|
||||
# Should have processed both messages normally
|
||||
assert len(result) == 2
|
||||
assert result[1]["content"] == "Hello world"
|
||||
|
||||
|
||||
class TestV1BackwardsCompatibility:
|
||||
"""Test backwards compatibility with v0 format."""
|
||||
|
||||
def test_v0_messages_unchanged_with_v0_output(self) -> None:
|
||||
"""Test that v0 messages are unchanged when output_version=v0."""
|
||||
llm = ChatOllama(model="test-model", output_version="v0")
|
||||
|
||||
# This test would require mocking the actual ollama calls
|
||||
# TODO complete this test with a mock or fixture
|
||||
assert llm.output_version == "v0"
|
||||
|
||||
def test_v1_input_works_with_v0_output(self) -> None:
|
||||
"""Test that v1 input messages work even when output_version=v0."""
|
||||
llm = ChatOllama(model="test-model", output_version="v0")
|
||||
|
||||
v1_message = AIMessage(content=[{"type": "text", "text": "Hello"}])
|
||||
|
||||
messages: list[BaseMessage] = [v1_message]
|
||||
result = llm._convert_messages_to_ollama_messages(messages)
|
||||
|
||||
# Should handle v1 input regardless of output_version setting
|
||||
assert len(result) == 1
|
||||
assert result[0]["content"] == "Hello"
|
||||
|
||||
|
||||
class TestV1EdgeCases:
|
||||
"""Test edge cases in v1 format handling."""
|
||||
|
||||
def test_empty_v1_content_list(self) -> None:
|
||||
"""Test handling empty v1 content list."""
|
||||
v1_message = AIMessage(content=[])
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == ""
|
||||
|
||||
def test_v1_content_with_unknown_type(self) -> None:
|
||||
"""Test v1 content with unknown block type converted to NonStandard."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{"type": "unknown_type", "data": "converted_to_non_standard"},
|
||||
{"type": "text", "text": " world"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
# Unknown types should be converted to NonStandardContentBlock and skipped
|
||||
# in content processing, text content should be preserved
|
||||
assert result.content == "Hello world"
|
||||
|
||||
def test_v1_content_with_malformed_blocks(self) -> None:
|
||||
"""Test v1 content with malformed blocks."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{"type": "text"}, # Missing 'text' field
|
||||
{"type": "reasoning"}, # Missing 'reasoning' field
|
||||
{"type": "text", "text": " world"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
# Should handle malformed blocks gracefully
|
||||
assert result.content == "Hello world"
|
||||
|
||||
def test_non_dict_blocks_ignored(self) -> None:
|
||||
"""Test that non-dict items in content list are ignored."""
|
||||
v1_message = AIMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
"invalid_block", # Not a dict
|
||||
{"type": "text", "text": " world"},
|
||||
]
|
||||
)
|
||||
|
||||
result = _convert_from_v1_message(v1_message)
|
||||
|
||||
assert result.content == "Hello world"
|
Loading…
Reference in New Issue
Block a user