From 679a9e7c8f3e77da8bb46735de346eb63f19cab7 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 11 Jul 2025 14:05:45 -0400 Subject: [PATCH] implement beta_content --- libs/core/langchain_core/messages/ai.py | 74 +++++++++++++++++++ libs/core/langchain_core/messages/base.py | 3 +- .../tests/unit_tests/messages/test_imports.py | 9 +++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/libs/core/langchain_core/messages/ai.py b/libs/core/langchain_core/messages/ai.py index c62a03345d2..cdd8611fbe2 100644 --- a/libs/core/langchain_core/messages/ai.py +++ b/libs/core/langchain_core/messages/ai.py @@ -8,6 +8,7 @@ from typing import Any, Literal, Optional, Union, cast from pydantic import model_validator from typing_extensions import NotRequired, Self, TypedDict, override +from langchain_core.messages import content_blocks as types from langchain_core.messages.base import ( BaseMessage, BaseMessageChunk, @@ -196,6 +197,79 @@ class AIMessage(BaseMessage): "invalid_tool_calls": self.invalid_tool_calls, } + @property + def beta_content(self) -> list[types.ContentBlock]: + """Return the content as a list of standard ContentBlocks. + + To use this property, the corresponding chat model must support + ``output_version="v1"`` or higher: + + .. code-block:: python + + from langchain.chat_models import init_chat_model + + llm = init_chat_model("...", output_version="v1") + + otherwise, does best-effort parsing to standard types. + """ + blocks: list[types.ContentBlock] = [] + if isinstance(self.content, str): + if self.content: + blocks.append({"type": "text", "text": self.content}) + else: + pass + + elif isinstance(self.content, list): + for item in self.content: + if isinstance(item, str): + blocks.append({"type": "text", "text": item}) + + elif isinstance(item, dict): + item_type = item.get("type") + if item_type == "text": + blocks.append(cast("types.TextContentBlock", item)) + elif item_type == "tool_call": + blocks.append(cast("types.ToolCallContentBlock", item)) + elif item_type == "reasoning": + blocks.append(cast("types.ReasoningContentBlock", item)) + elif item_type == "non_standard": + blocks.append(cast("types.NonStandardContentBlock", item)) + elif source_type := item.get("source_type"): + if source_type == "url": + blocks.append(cast("types.URLContentBlock", item)) + elif source_type == "base64": + blocks.append(cast("types.Base64ContentBlock", item)) + elif source_type == "text": + blocks.append(cast("types.PlainTextContentBlock", item)) + elif source_type == "id": + blocks.append(cast("types.IDContentBlock", item)) + else: + msg = f"Unknown source_type {source_type} in content block." + raise ValueError(msg) + else: + msg = f"Unknown content block type {item_type}." + raise ValueError(msg) + else: + pass + else: + pass + + # 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.ToolCallContentBlock = { + "type": "tool_call", + "id": id_, + } + blocks.append(tool_call_block) + + return blocks + # TODO: remove this logic if possible, reducing breaking nature of changes @model_validator(mode="before") @classmethod diff --git a/libs/core/langchain_core/messages/base.py b/libs/core/langchain_core/messages/base.py index 3974fb00476..ba976286b75 100644 --- a/libs/core/langchain_core/messages/base.py +++ b/libs/core/langchain_core/messages/base.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any, Optional, Union, cast from pydantic import ConfigDict, Field from langchain_core.load.serializable import Serializable -from langchain_core.messages import ContentBlock from langchain_core.utils import get_bolded_text from langchain_core.utils._merge import merge_dicts, merge_lists from langchain_core.utils.interactive_env import is_interactive_env @@ -24,7 +23,7 @@ class BaseMessage(Serializable): Messages are the inputs and outputs of ChatModels. """ - content: Union[str, list[Union[str, ContentBlock, dict]]] + content: Union[str, list[Union[str, dict]]] """The string contents of the message.""" additional_kwargs: dict = Field(default_factory=dict) diff --git a/libs/core/tests/unit_tests/messages/test_imports.py b/libs/core/tests/unit_tests/messages/test_imports.py index ff9fbf92fc7..4615db28c6a 100644 --- a/libs/core/tests/unit_tests/messages/test_imports.py +++ b/libs/core/tests/unit_tests/messages/test_imports.py @@ -6,21 +6,30 @@ EXPECTED_ALL = [ "AIMessage", "AIMessageChunk", "AnyMessage", + "Base64ContentBlock", "BaseMessage", "BaseMessageChunk", + "ContentBlock", "ChatMessage", "ChatMessageChunk", + "DocumentCitation", "FunctionMessage", "FunctionMessageChunk", "HumanMessage", "HumanMessageChunk", "InvalidToolCall", + "NonStandardAnnotation", + "NonStandardContentBlock", "SystemMessage", "SystemMessageChunk", + "TextContentBlock", "ToolCall", "ToolCallChunk", + "ToolCallContentBlock", "ToolMessage", "ToolMessageChunk", + "UrlCitation", + "ReasoningContentBlock", "RemoveMessage", "convert_to_messages", "get_buffer_string",