mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-15 07:00:38 +00:00
start on duplicate content
This commit is contained in:
@@ -202,11 +202,16 @@ class AIMessage(BaseMessage):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify content as a positional arg or content_blocks for typing support."""
|
||||
if content_blocks is not None:
|
||||
if content is not None and content_blocks is None:
|
||||
super().__init__(content=content, **kwargs)
|
||||
elif content is None and content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
content_blocks=content_blocks,
|
||||
**kwargs,
|
||||
)
|
||||
elif content is not None and content_blocks is not None:
|
||||
super().__init__(content=content, content_blocks=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
@@ -218,32 +223,45 @@ class AIMessage(BaseMessage):
|
||||
"invalid_tool_calls": self.invalid_tool_calls,
|
||||
}
|
||||
|
||||
@property
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
"""Return content blocks of the message."""
|
||||
blocks = super().content_blocks
|
||||
@model_validator(mode="after")
|
||||
def _init_content_blocks(self) -> Self:
|
||||
"""Assign the content as a list of standard ContentBlocks.
|
||||
|
||||
# Add from tool_calls if missing from content
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in self.content
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in self.tool_calls:
|
||||
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
if "index" in tool_call:
|
||||
tool_call_block["index"] = tool_call["index"]
|
||||
if "extras" in tool_call:
|
||||
tool_call_block["extras"] = tool_call["extras"]
|
||||
blocks.append(tool_call_block)
|
||||
To use this property, the corresponding chat model must support
|
||||
``message_version="v1"`` or higher:
|
||||
|
||||
return blocks
|
||||
.. code-block:: python
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
llm = init_chat_model("...", message_version="v1")
|
||||
|
||||
otherwise, does best-effort parsing to standard types.
|
||||
"""
|
||||
if not self.content_blocks:
|
||||
self.content_blocks = self._init_text_content(self.content)
|
||||
|
||||
if self.tool_calls or self.invalid_tool_calls:
|
||||
# Add from tool_calls if missing from content
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in self.content_blocks
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in self.tool_calls:
|
||||
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
if "index" in tool_call:
|
||||
tool_call_block["index"] = tool_call["index"]
|
||||
if "extras" in tool_call:
|
||||
tool_call_block["extras"] = tool_call["extras"]
|
||||
self.content_blocks.append(tool_call_block)
|
||||
|
||||
return self
|
||||
|
||||
# TODO: remove this logic if possible, reducing breaking nature of changes
|
||||
@model_validator(mode="before")
|
||||
|
||||
@@ -4,7 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
from pydantic import ConfigDict, Field, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.messages import content_blocks as types
|
||||
@@ -27,6 +28,9 @@ class BaseMessage(Serializable):
|
||||
content: Union[str, list[Union[str, dict]]]
|
||||
"""The string contents of the message."""
|
||||
|
||||
content_blocks: list[types.ContentBlock] = Field(default_factory=list)
|
||||
"""The content of the message as a list of standard ContentBlocks."""
|
||||
|
||||
additional_kwargs: dict = Field(default_factory=dict)
|
||||
"""Reserved for additional payload data associated with the message.
|
||||
|
||||
@@ -84,8 +88,14 @@ class BaseMessage(Serializable):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify content as a positional arg or content_blocks for typing support."""
|
||||
if content_blocks is not None:
|
||||
super().__init__(content=content_blocks, **kwargs)
|
||||
if content is not None and content_blocks is None:
|
||||
super().__init__(content=content, **kwargs)
|
||||
elif content is None and content_blocks is not None:
|
||||
super().__init__(
|
||||
content=content_blocks, content_blocks=content_blocks, **kwargs
|
||||
)
|
||||
elif content is not None and content_blocks is not None:
|
||||
super().__init__(content=content, content_blocks=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
@@ -106,9 +116,30 @@ class BaseMessage(Serializable):
|
||||
"""
|
||||
return ["langchain", "schema", "messages"]
|
||||
|
||||
@property
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
"""Return the content as a list of standard ContentBlocks.
|
||||
@staticmethod
|
||||
def _init_text_content(
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Parse string content into a list of ContentBlocks."""
|
||||
blocks: list[types.ContentBlock] = []
|
||||
content = [content] if isinstance(content, str) and content else content
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
blocks.append({"type": "text", "text": item})
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type not in types.KNOWN_BLOCK_TYPES:
|
||||
blocks.append({"type": "non_standard", "value": item})
|
||||
else:
|
||||
blocks.append(cast("types.ContentBlock", item))
|
||||
else:
|
||||
pass
|
||||
|
||||
return blocks
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_content_blocks(self) -> Self:
|
||||
"""Assign the content as a list of standard ContentBlocks.
|
||||
|
||||
To use this property, the corresponding chat model must support
|
||||
``message_version="v1"`` or higher:
|
||||
@@ -120,29 +151,11 @@ class BaseMessage(Serializable):
|
||||
|
||||
otherwise, does best-effort parsing to standard types.
|
||||
"""
|
||||
blocks: list[types.ContentBlock] = []
|
||||
content = (
|
||||
[self.content]
|
||||
if isinstance(self.content, str) and self.content
|
||||
else self.content
|
||||
)
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
blocks.append({"type": "text", "text": item})
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type not in types.KNOWN_BLOCK_TYPES:
|
||||
msg = (
|
||||
f"Non-standard content block type '{item_type}'. Ensure "
|
||||
"the model supports `output_version='v1'` or higher and "
|
||||
"that this attribute is set on initialization."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
blocks.append(cast("types.ContentBlock", item))
|
||||
else:
|
||||
pass
|
||||
if not self.content_blocks:
|
||||
blocks = self._init_text_content(self.content)
|
||||
self.content_blocks = blocks
|
||||
|
||||
return blocks
|
||||
return self
|
||||
|
||||
def text(self) -> str:
|
||||
"""Get the text content of the message.
|
||||
|
||||
@@ -64,11 +64,16 @@ class HumanMessage(BaseMessage):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify content as a positional arg or content_blocks for typing support."""
|
||||
if content_blocks is not None:
|
||||
if content is not None and content_blocks is None:
|
||||
super().__init__(content=content, **kwargs)
|
||||
elif content is None and content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
content_blocks=content_blocks,
|
||||
**kwargs,
|
||||
)
|
||||
elif content is not None and content_blocks is not None:
|
||||
super().__init__(content=content, content_blocks=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
@@ -57,11 +57,16 @@ class SystemMessage(BaseMessage):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify content as a positional arg or content_blocks for typing support."""
|
||||
if content_blocks is not None:
|
||||
if content is not None and content_blocks is None:
|
||||
super().__init__(content=content, **kwargs)
|
||||
elif content is None and content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
content_blocks=content_blocks,
|
||||
**kwargs,
|
||||
)
|
||||
elif content is not None and content_blocks is not None:
|
||||
super().__init__(content=content, content_blocks=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
@@ -158,11 +158,16 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify content as a positional arg or content_blocks for typing support."""
|
||||
if content_blocks is not None:
|
||||
if content is not None and content_blocks is None:
|
||||
super().__init__(content=content, **kwargs)
|
||||
elif content is None and content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
content_blocks=content_blocks,
|
||||
**kwargs,
|
||||
)
|
||||
elif content is not None and content_blocks is not None:
|
||||
super().__init__(content=content, content_blocks=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
@@ -443,19 +443,11 @@ def test_trace_images_in_openai_format() -> None:
|
||||
]
|
||||
tracer = FakeChatModelStartTracer()
|
||||
response = llm.invoke(messages, config={"callbacks": [tracer]})
|
||||
assert tracer.messages == [
|
||||
[
|
||||
[
|
||||
HumanMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
assert tracer.messages[0][0][0].content == [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
}
|
||||
]
|
||||
# Test no mutation
|
||||
assert response.content == [
|
||||
@@ -486,23 +478,15 @@ def test_trace_content_blocks_with_no_type_key() -> None:
|
||||
]
|
||||
tracer = FakeChatModelStartTracer()
|
||||
response = llm.invoke(messages, config={"callbacks": [tracer]})
|
||||
assert tracer.messages == [
|
||||
[
|
||||
[
|
||||
HumanMessage(
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
},
|
||||
{
|
||||
"type": "cachePoint",
|
||||
"cachePoint": {"type": "default"},
|
||||
},
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
assert tracer.messages[0][0][0].content == [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
},
|
||||
{
|
||||
"type": "cachePoint",
|
||||
"cachePoint": {"type": "default"},
|
||||
},
|
||||
]
|
||||
# Test no mutation
|
||||
assert response.content == [
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from typing_extensions import get_args
|
||||
|
||||
from langchain_core.documents import Document
|
||||
@@ -1267,10 +1268,14 @@ def test_typed_init() -> None:
|
||||
|
||||
# Test we get type errors for malformed blocks (type checker will complain if
|
||||
# below type-ignores are unused).
|
||||
_ = AIMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
_ = HumanMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
_ = SystemMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
_ = ToolMessage(
|
||||
content_blocks=[{"type": "text", "bad": "Hello"}], # type: ignore[list-item]
|
||||
tool_call_id="abc123",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
_ = AIMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
with pytest.raises(ValidationError):
|
||||
_ = HumanMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
with pytest.raises(ValidationError):
|
||||
_ = SystemMessage(content_blocks=[{"type": "text", "bad": "Hello"}]) # type: ignore[list-item]
|
||||
with pytest.raises(ValidationError):
|
||||
_ = ToolMessage(
|
||||
content_blocks=[{"type": "text", "bad": "Hello"}], # type: ignore[list-item]
|
||||
tool_call_id="abc123",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user