libs/core/langchain_core/messages/base.py PYTHON 519 lines View on github.com → Search inside
1"""Base message."""23from __future__ import annotations45from typing import TYPE_CHECKING, Any, cast, overload67from pydantic import ConfigDict, Field89from langchain_core._api.deprecation import warn_deprecated10from langchain_core.load.serializable import Serializable11from langchain_core.messages import content as types12from langchain_core.utils import get_bolded_text13from langchain_core.utils._merge import merge_dicts, merge_lists14from langchain_core.utils.interactive_env import is_interactive_env1516if TYPE_CHECKING:17    from collections.abc import Sequence1819    from typing_extensions import Self2021    from langchain_core.prompts.chat import ChatPromptTemplate222324def _extract_reasoning_from_additional_kwargs(25    message: BaseMessage,26) -> types.ReasoningContentBlock | None:27    """Extract `reasoning_content` from `additional_kwargs`.2829    Handles reasoning content stored in various formats:30    - `additional_kwargs["reasoning_content"]` (string) - Ollama, DeepSeek, XAI, Groq3132    Args:33        message: The message to extract reasoning from.3435    Returns:36        A `ReasoningContentBlock` if reasoning content is found, None otherwise.37    """38    additional_kwargs = getattr(message, "additional_kwargs", {})3940    reasoning_content = additional_kwargs.get("reasoning_content")41    if reasoning_content is not None and isinstance(reasoning_content, str):42        return {"type": "reasoning", "reasoning": reasoning_content}4344    return None454647class TextAccessor(str):48    """String-like object that supports both property and method access patterns.4950    Exists to maintain backward compatibility while transitioning from method-based to51    property-based text access in message objects. In LangChain <v1.0, message text was52    accessed via `.text()` method calls. In v1.0=<, the preferred pattern is property53    access via `.text`.5455    Rather than breaking existing code immediately, `TextAccessor` allows both56    patterns:57    - Modern property access: `message.text` (returns string directly)58    - Legacy method access: `message.text()` (callable, emits deprecation warning)5960    """6162    __slots__ = ()6364    def __new__(cls, value: str) -> Self:65        """Create new TextAccessor instance."""66        return str.__new__(cls, value)6768    def __call__(self) -> str:69        """Enable method-style text access for backward compatibility.7071        This method exists solely to support legacy code that calls `.text()`72        as a method. New code should use property access (`.text`) instead.7374        !!! deprecated75            As of `langchain-core` 1.0.0, calling `.text()` as a method is deprecated.76            Use `.text` as a property instead. This method will be removed in 2.0.0.7778        Returns:79            The string content, identical to property access.8081        """82        warn_deprecated(83            since="1.0.0",84            message=(85                "Calling .text() as a method is deprecated. "86                "Use .text as a property instead (e.g., message.text)."87            ),88            removal="2.0.0",89        )90        return str(self)919293class BaseMessage(Serializable):94    """Base abstract message class.9596    Messages are the inputs and outputs of a chat model.9798    Examples include [`HumanMessage`][langchain.messages.HumanMessage],99    [`AIMessage`][langchain.messages.AIMessage], and100    [`SystemMessage`][langchain.messages.SystemMessage].101    """102103    content: str | list[str | dict]104    """The contents of the message."""105106    additional_kwargs: dict = Field(default_factory=dict)107    """Reserved for additional payload data associated with the message.108109    For example, for a message from an AI, this could include tool calls as110    encoded by the model provider.111112    """113114    response_metadata: dict = Field(default_factory=dict)115    """Examples: response headers, logprobs, token counts, model name."""116117    type: str118    """The type of the message. Must be a string that is unique to the message type.119120    The purpose of this field is to allow for easy identification of the message type121    when deserializing messages.122123    """124125    name: str | None = None126    """An optional name for the message.127128    This can be used to provide a human-readable name for the message.129130    Usage of this field is optional, and whether it's used or not is up to the131    model implementation.132133    """134135    id: str | None = Field(default=None, coerce_numbers_to_str=True)136    """An optional unique identifier for the message.137138    This should ideally be provided by the provider/model which created the message.139140    """141142    model_config = ConfigDict(143        extra="allow",144    )145146    @overload147    def __init__(148        self,149        content: str | list[str | dict],150        **kwargs: Any,151    ) -> None: ...152153    @overload154    def __init__(155        self,156        content: str | list[str | dict] | None = None,157        content_blocks: list[types.ContentBlock] | None = None,158        **kwargs: Any,159    ) -> None: ...160161    def __init__(162        self,163        content: str | list[str | dict] | None = None,164        content_blocks: list[types.ContentBlock] | None = None,165        **kwargs: Any,166    ) -> None:167        """Initialize a `BaseMessage`.168169        Specify `content` as positional arg or `content_blocks` for typing.170171        Args:172            content: The contents of the message.173            content_blocks: Typed standard content.174            **kwargs: Additional arguments to pass to the parent class.175        """176        if content_blocks is not None:177            super().__init__(content=content_blocks, **kwargs)178        else:179            super().__init__(content=content, **kwargs)180181    @classmethod182    def is_lc_serializable(cls) -> bool:183        """`BaseMessage` is serializable.184185        Returns:186            True187        """188        return True189190    @classmethod191    def get_lc_namespace(cls) -> list[str]:192        """Get the namespace of the LangChain object.193194        Returns:195            `["langchain", "schema", "messages"]`196        """197        return ["langchain", "schema", "messages"]198199    @property200    def content_blocks(self) -> list[types.ContentBlock]:201        r"""Load content blocks from the message content.202203        !!! version-added "Added in `langchain-core` 1.0.0"204205        """206        # Needed here to avoid circular import, as these classes import BaseMessages207        from langchain_core.messages.block_translators.anthropic import (  # noqa: PLC0415208            _convert_to_v1_from_anthropic_input,209        )210        from langchain_core.messages.block_translators.bedrock_converse import (  # noqa: PLC0415211            _convert_to_v1_from_converse_input,212        )213        from langchain_core.messages.block_translators.google_genai import (  # noqa: PLC0415214            _convert_to_v1_from_genai_input,215        )216        from langchain_core.messages.block_translators.langchain_v0 import (  # noqa: PLC0415217            _convert_v0_multimodal_input_to_v1,218        )219        from langchain_core.messages.block_translators.openai import (  # noqa: PLC0415220            _convert_to_v1_from_chat_completions_input,221        )222223        blocks: list[types.ContentBlock] = []224        content = (225            # Transpose string content to list, otherwise assumed to be list226            [self.content]227            if isinstance(self.content, str) and self.content228            else self.content229        )230        for item in content:231            if isinstance(item, str):232                # Plain string content is treated as a text block233                blocks.append({"type": "text", "text": item})234            elif isinstance(item, dict):235                item_type = item.get("type")236                if item_type not in types.KNOWN_BLOCK_TYPES:237                    # Handle all provider-specific or None type blocks as non-standard -238                    # we'll come back to these later239                    blocks.append({"type": "non_standard", "value": item})240                else:241                    # Guard against v0 blocks that share the same `type` keys242                    if "source_type" in item:243                        blocks.append({"type": "non_standard", "value": item})244                        continue245246                    # This can't be a v0 block (since they require `source_type`),247                    # so it's a known v1 block type248                    blocks.append(cast("types.ContentBlock", item))249250        # Subsequent passes: attempt to unpack non-standard blocks.251        # This is the last stop - if we can't parse it here, it is left as non-standard252        for parsing_step in [253            _convert_v0_multimodal_input_to_v1,254            _convert_to_v1_from_chat_completions_input,255            _convert_to_v1_from_anthropic_input,256            _convert_to_v1_from_genai_input,257            _convert_to_v1_from_converse_input,258        ]:259            blocks = parsing_step(blocks)260        return blocks261262    @property263    def text(self) -> TextAccessor:264        """Get the text content of the message as a string.265266        Can be used as both property (`message.text`) and method (`message.text()`).267268        Handles both string and list content types (e.g. for content blocks). Only269        extracts blocks with `type: 'text'`; other block types are ignored.270271        !!! deprecated272            As of `langchain-core` 1.0.0, calling `.text()` as a method is deprecated.273            Use `.text` as a property instead. This method will be removed in 2.0.0.274275        Returns:276            The text content of the message.277278        """279        if isinstance(self.content, str):280            text_value = self.content281        else:282            # Must be a list283            blocks = [284                block285                for block in self.content286                if isinstance(block, str)287                or (block.get("type") == "text" and isinstance(block.get("text"), str))288            ]289            text_value = "".join(290                block if isinstance(block, str) else block["text"] for block in blocks291            )292        return TextAccessor(text_value)293294    def __add__(self, other: Any) -> ChatPromptTemplate:295        """Concatenate this message with another message.296297        Args:298            other: Another message to concatenate with this one.299300        Returns:301            A ChatPromptTemplate containing both messages.302        """303        # Import locally to prevent circular imports.304        from langchain_core.prompts.chat import ChatPromptTemplate  # noqa: PLC0415305306        prompt = ChatPromptTemplate(messages=[self])307        return prompt.__add__(other)308309    def pretty_repr(310        self,311        html: bool = False,  # noqa: FBT001,FBT002312    ) -> str:313        """Get a pretty representation of the message.314315        Args:316            html: Whether to format the message as HTML. If `True`, the message will be317                formatted with HTML tags.318319        Returns:320            A pretty representation of the message.321322        Example:323            ```python324            from langchain_core.messages import HumanMessage325326            msg = HumanMessage(content="What is the capital of France?")327            print(msg.pretty_repr())328            ```329330            Results in:331332            ```txt333            ================================ Human Message =================================334335            What is the capital of France?336            ```337        """  # noqa: E501338        title = get_msg_title_repr(self.type.title() + " Message", bold=html)339        # TODO: handle non-string content.340        if self.name is not None:341            title += f"\nName: {self.name}"342        return f"{title}\n\n{self.content}"343344    def pretty_print(self) -> None:345        """Print a pretty representation of the message.346347        Example:348            ```python349            from langchain_core.messages import AIMessage350351            msg = AIMessage(content="The capital of France is Paris.")352            msg.pretty_print()353            ```354355            Results in:356357            ```txt358            ================================== Ai Message ==================================359360            The capital of France is Paris.361            ```362        """  # noqa: E501363        print(self.pretty_repr(html=is_interactive_env()))  # noqa: T201364365366def merge_content(367    first_content: str | list[str | dict],368    *contents: str | list[str | dict],369) -> str | list[str | dict]:370    """Merge multiple message contents.371372    Args:373        first_content: The first `content`. Can be a string or a list.374        contents: The other `content`s. Can be a string or a list.375376    Returns:377        The merged content.378379    """380    merged: str | list[str | dict]381    merged = "" if first_content is None else first_content382383    for content in contents:384        # If current is a string385        if isinstance(merged, str):386            # If the next chunk is also a string, then merge them naively387            if isinstance(content, str):388                merged += content389            # If the next chunk is a list, add the current to the start of the list390            else:391                merged = [merged, *content]392        elif isinstance(content, list):393            # If both are lists394            merged = merge_lists(cast("list", merged), content)  # type: ignore[assignment]395        # If the first content is a list, and the second content is a string396        # If the last element of the first content is a string397        # Add the second content to the last element398        elif merged and isinstance(merged[-1], str):399            merged[-1] += content400        # If second content is an empty string, treat as a no-op401        elif content == "":402            pass403        # Otherwise, add the second content as a new element of the list404        elif merged:405            merged.append(content)406    return merged407408409class BaseMessageChunk(BaseMessage):410    """Message chunk, which can be concatenated with other Message chunks."""411412    def __add__(self, other: Any) -> BaseMessageChunk:  # type: ignore[override]413        """Message chunks support concatenation with other message chunks.414415        This functionality is useful to combine message chunks yielded from416        a streaming model into a complete message.417418        Args:419            other: Another message chunk to concatenate with this one.420421        Returns:422            A new message chunk that is the concatenation of this message chunk423            and the other message chunk.424425        Raises:426            TypeError: If the other object is not a message chunk.427428        Example:429            ```txt430              AIMessageChunk(content="Hello", ...)431            + AIMessageChunk(content=" World", ...)432            = AIMessageChunk(content="Hello World", ...)433            ```434        """435        if isinstance(other, BaseMessageChunk):436            # If both are (subclasses of) BaseMessageChunk,437            # concat into a single BaseMessageChunk438439            return self.__class__(440                id=self.id,441                type=self.type,442                content=merge_content(self.content, other.content),443                additional_kwargs=merge_dicts(444                    self.additional_kwargs, other.additional_kwargs445                ),446                response_metadata=merge_dicts(447                    self.response_metadata, other.response_metadata448                ),449            )450        if isinstance(other, list) and all(451            isinstance(o, BaseMessageChunk) for o in other452        ):453            content = merge_content(self.content, *(o.content for o in other))454            additional_kwargs = merge_dicts(455                self.additional_kwargs, *(o.additional_kwargs for o in other)456            )457            response_metadata = merge_dicts(458                self.response_metadata, *(o.response_metadata for o in other)459            )460            return self.__class__(  # type: ignore[call-arg]461                id=self.id,462                content=content,463                additional_kwargs=additional_kwargs,464                response_metadata=response_metadata,465            )466        msg = (467            'unsupported operand type(s) for +: "'468            f"{self.__class__.__name__}"469            f'" and "{other.__class__.__name__}"'470        )471        raise TypeError(msg)472473474def message_to_dict(message: BaseMessage) -> dict:475    """Convert a Message to a dictionary.476477    Args:478        message: Message to convert.479480    Returns:481        Message as a dict. The dict will have a `type` key with the message type482        and a `data` key with the message data as a dict.483484    """485    return {"type": message.type, "data": message.model_dump()}486487488def messages_to_dict(messages: Sequence[BaseMessage]) -> list[dict]:489    """Convert a sequence of Messages to a list of dictionaries.490491    Args:492        messages: Sequence of messages (as `BaseMessage`s) to convert.493494    Returns:495        List of messages as dicts.496497    """498    return [message_to_dict(m) for m in messages]499500501def get_msg_title_repr(title: str, *, bold: bool = False) -> str:502    """Get a title representation for a message.503504    Args:505        title: The title.506        bold: Whether to bold the title.507508    Returns:509        The title representation.510511    """512    padded = " " + title + " "513    sep_len = (80 - len(padded)) // 2514    sep = "=" * sep_len515    second_sep = sep + "=" if len(padded) % 2 else sep516    if bold:517        padded = get_bolded_text(padded)518    return f"{sep}{padded}{second_sep}"

Code quality findings 22

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if reasoning_content is not None and isinstance(reasoning_content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.content, str) and self.content
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(item, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(item, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
or (block.get("type") == "text" and isinstance(block.get("text"), str))
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
block if isinstance(block, str) else block["text"] for block in blocks
Ensure functions have docstrings for documentation
missing-docstring
def pretty_repr(
Use logging module for better control and configurability
print-statement
print(msg.pretty_repr())
Use logging module for better control and configurability
print-statement
def pretty_print(self) -> None:
Use logging module for better control and configurability
print-statement
msg.pretty_print()
Use logging module for better control and configurability
print-statement
print(self.pretty_repr(html=is_interactive_env())) # noqa: T201
Ensure functions have docstrings for documentation
missing-docstring
def merge_content(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(merged, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif merged and isinstance(merged[-1], str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(other, BaseMessageChunk):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(other, list) and all(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(o, BaseMessageChunk) for o in other
Use isinstance() for type checking instead of type()
type-check
'unsupported operand type(s) for +: "'

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.