mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(core): avoid mutation in bedrock converse standard content (#40022)
Co-authored-by: Zhewen Tan <127607634+tandede@users.noreply.github.com>
This commit is contained in:
1 parent
1e1e238703
commit
36f0d10348
2 files changed
+90
-10
No files matched your search
@@ -159,11 +159,16 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
# Converse outputs multiple chunks containing response metadata
|
||||
return []
|
||||
|
||||
if isinstance(message.content, str):
|
||||
message.content = [{"type": "text", "text": message.content}]
|
||||
# Translated through a local list so that reading `content_blocks` leaves
|
||||
# `message.content` untouched.
|
||||
content: list[str | dict[str, Any]] = (
|
||||
[{"type": "text", "text": message.content}]
|
||||
if isinstance(message.content, str)
|
||||
else message.content
|
||||
)
|
||||
|
||||
def _iter_blocks() -> Iterator[types.ContentBlock]:
|
||||
for block in message.content:
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
block_type = block.get("type")
|
||||
@@ -269,12 +274,17 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
yield tool_call_chunk
|
||||
|
||||
else:
|
||||
new_block: types.NonStandardContentBlock = {
|
||||
"type": "non_standard",
|
||||
"value": block,
|
||||
}
|
||||
if "index" in new_block["value"]:
|
||||
new_block["index"] = new_block["value"].pop("index")
|
||||
if "index" in block:
|
||||
# Copy before lifting `index` off the block.
|
||||
value = block.copy()
|
||||
index = value.pop("index")
|
||||
new_block: types.NonStandardContentBlock = {
|
||||
"type": "non_standard",
|
||||
"value": value,
|
||||
"index": index,
|
||||
}
|
||||
else:
|
||||
new_block = {"type": "non_standard", "value": block}
|
||||
yield new_block
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
@@ -119,10 +124,12 @@ def test_convert_to_v1_from_bedrock_converse() -> None:
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
]
|
||||
original_content = deepcopy(message.content)
|
||||
|
||||
assert message.content_blocks == expected_content
|
||||
|
||||
# Check no mutation
|
||||
assert message.content != expected_content
|
||||
assert message.content == original_content
|
||||
|
||||
|
||||
def test_convert_to_v1_from_converse_chunk() -> None:
|
||||
@@ -377,3 +384,66 @@ def test_convert_to_v1_from_converse_input() -> None:
|
||||
]
|
||||
|
||||
assert message.content_blocks == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_class", [AIMessage, AIMessageChunk])
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
"hello",
|
||||
[{"type": "custom", "payload": "value", "index": 3}],
|
||||
[{"type": "custom", "payload": "value"}],
|
||||
[{"type": "text", "text": "hello", "index": 0}],
|
||||
],
|
||||
)
|
||||
def test_content_blocks_does_not_mutate_content(
|
||||
message_class: type[AIMessage],
|
||||
content: str | list[str | dict[Any, Any]],
|
||||
) -> None:
|
||||
"""Reading `content_blocks` must leave `message.content` unchanged."""
|
||||
message = message_class(
|
||||
content=deepcopy(content),
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
)
|
||||
original_content = deepcopy(message.content)
|
||||
|
||||
first_read = message.content_blocks
|
||||
|
||||
assert message.content == original_content
|
||||
|
||||
# Repeated reads of an unchanged message must agree
|
||||
assert message.content_blocks == first_read
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_class", [AIMessage, AIMessageChunk])
|
||||
def test_non_standard_block_index_is_lifted_not_removed(
|
||||
message_class: type[AIMessage],
|
||||
) -> None:
|
||||
"""`index` moves onto the standardized block without leaving the source."""
|
||||
message = message_class(
|
||||
content=[{"type": "custom", "payload": "value", "index": 3}],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
)
|
||||
|
||||
assert message.content_blocks == [
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "custom", "payload": "value"},
|
||||
"index": 3,
|
||||
}
|
||||
]
|
||||
assert message.content == [{"type": "custom", "payload": "value", "index": 3}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_class", [AIMessage, AIMessageChunk])
|
||||
def test_string_content_is_not_replaced_with_blocks(
|
||||
message_class: type[AIMessage],
|
||||
) -> None:
|
||||
"""String content is translated through a local list, not written back."""
|
||||
message = message_class(
|
||||
content="hello",
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
)
|
||||
|
||||
assert message.content_blocks == [{"type": "text", "text": "hello"}]
|
||||
assert message.content == "hello"
|
||||
Reference in new issue
Block a user