libs/core/langchain_core/messages/utils.py PYTHON 2,443 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,443.
1"""Module contains utility functions for working with messages.23Some examples of what you can do with these functions include:45* Convert messages to strings (serialization)6* Convert messages from dicts to Message objects (deserialization)7* Filter messages from a list of messages based on name, type or id etc.8"""910from __future__ import annotations1112import base6413import inspect14import json15import logging16import math17from collections.abc import Callable, Iterable, Sequence18from functools import partial, wraps19from typing import (20    TYPE_CHECKING,21    Annotated,22    Any,23    Concatenate,24    Literal,25    ParamSpec,26    Protocol,27    TypeVar,28    cast,29    overload,30)31from xml.sax.saxutils import escape, quoteattr3233from pydantic import Discriminator, Field, Tag3435from langchain_core.exceptions import ErrorCode, create_message36from langchain_core.messages.ai import AIMessage, AIMessageChunk37from langchain_core.messages.base import BaseMessage, BaseMessageChunk38from langchain_core.messages.block_translators.openai import (39    convert_to_openai_data_block,40)41from langchain_core.messages.chat import ChatMessage, ChatMessageChunk42from langchain_core.messages.content import (43    is_data_content_block,44)45from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk46from langchain_core.messages.human import HumanMessage, HumanMessageChunk47from langchain_core.messages.modifier import RemoveMessage48from langchain_core.messages.system import SystemMessage, SystemMessageChunk49from langchain_core.messages.tool import ToolCall, ToolMessage, ToolMessageChunk50from langchain_core.utils.pydantic import model_json_schema as get_model_json_schema5152if TYPE_CHECKING:53    from langchain_core.language_models import BaseLanguageModel54    from langchain_core.prompt_values import PromptValue55    from langchain_core.runnables.base import Runnable56    from langchain_core.tools import BaseTool5758try:59    from langchain_text_splitters import TextSplitter6061    _HAS_LANGCHAIN_TEXT_SPLITTERS = True62except ImportError:63    _HAS_LANGCHAIN_TEXT_SPLITTERS = False6465logger = logging.getLogger(__name__)666768def _get_type(v: Any) -> str:69    """Get the type associated with the object for serialization purposes."""70    if isinstance(v, dict) and "type" in v:71        result = v["type"]72    elif hasattr(v, "type"):73        result = v.type74    else:75        msg = (76            f"Expected either a dictionary with a 'type' key or an object "77            f"with a 'type' attribute. Instead got type {type(v)}."78        )79        raise TypeError(msg)80    if not isinstance(result, str):81        msg = f"Expected 'type' to be a str, got {type(result).__name__}"82        raise TypeError(msg)83    return result848586AnyMessage = Annotated[87    Annotated[AIMessage, Tag(tag="ai")]88    | Annotated[HumanMessage, Tag(tag="human")]89    | Annotated[ChatMessage, Tag(tag="chat")]90    | Annotated[SystemMessage, Tag(tag="system")]91    | Annotated[FunctionMessage, Tag(tag="function")]92    | Annotated[ToolMessage, Tag(tag="tool")]93    | Annotated[AIMessageChunk, Tag(tag="AIMessageChunk")]94    | Annotated[HumanMessageChunk, Tag(tag="HumanMessageChunk")]95    | Annotated[ChatMessageChunk, Tag(tag="ChatMessageChunk")]96    | Annotated[SystemMessageChunk, Tag(tag="SystemMessageChunk")]97    | Annotated[FunctionMessageChunk, Tag(tag="FunctionMessageChunk")]98    | Annotated[ToolMessageChunk, Tag(tag="ToolMessageChunk")],99    Field(discriminator=Discriminator(_get_type)),100]101"""A type representing any defined `Message` or `MessageChunk` type."""102103104def _has_base64_data(block: dict[str, Any]) -> bool:105    """Check if a content block contains base64 encoded data.106107    Args:108        block: A content block dictionary.109110    Returns:111        Whether the block contains base64 data.112    """113    # Check for explicit base64 field (standard content blocks)114    if block.get("base64"):115        return True116117    # Check for data: URL in url field118    url = block.get("url", "")119    if isinstance(url, str) and url.startswith("data:"):120        return True121122    # Check for OpenAI-style image_url with data: URL123    image_url = block.get("image_url", {})124    if isinstance(image_url, dict):125        url = image_url.get("url", "")126        if isinstance(url, str) and url.startswith("data:"):127            return True128129    return False130131132_XML_CONTENT_BLOCK_MAX_LEN = 500133134135def _truncate(text: str, max_len: int = _XML_CONTENT_BLOCK_MAX_LEN) -> str:136    """Truncate text to `max_len` characters, adding ellipsis if truncated."""137    if len(text) <= max_len:138        return text139    return text[:max_len] + "..."140141142def _format_content_block_xml(block: dict[str, Any]) -> str | None:143    """Format a content block as XML.144145    Args:146        block: A LangChain content block.147148    Returns:149        XML string representation of the block, or `None` if the block should be150            skipped.151152    Note:153        Plain text document content, server tool call arguments, and server tool154        result outputs are truncated to 500 characters.155    """156    block_type = block.get("type", "")157158    # Skip blocks with base64 encoded data159    if _has_base64_data(block):160        return None161162    # Text blocks163    if block_type == "text":164        text = block.get("text", "")165        return escape(text) if text else None166167    # Reasoning blocks168    if block_type == "reasoning":169        reasoning = block.get("reasoning", "")170        if reasoning:171            return f"<reasoning>{escape(reasoning)}</reasoning>"172        return None173174    # Image blocks (URL only, base64 already filtered)175    if block_type == "image":176        url = block.get("url")177        file_id = block.get("file_id")178        if url:179            return f"<image url={quoteattr(url)} />"180        if file_id:181            return f"<image file_id={quoteattr(file_id)} />"182        return None183184    # OpenAI-style image_url blocks185    if block_type == "image_url":186        image_url = block.get("image_url", {})187        if isinstance(image_url, dict):188            url = image_url.get("url", "")189            if url and not url.startswith("data:"):190                return f"<image url={quoteattr(url)} />"191        return None192193    # Audio blocks (URL only)194    if block_type == "audio":195        url = block.get("url")196        file_id = block.get("file_id")197        if url:198            return f"<audio url={quoteattr(url)} />"199        if file_id:200            return f"<audio file_id={quoteattr(file_id)} />"201        return None202203    # Video blocks (URL only)204    if block_type == "video":205        url = block.get("url")206        file_id = block.get("file_id")207        if url:208            return f"<video url={quoteattr(url)} />"209        if file_id:210            return f"<video file_id={quoteattr(file_id)} />"211        return None212213    # Plain text document blocks214    if block_type == "text-plain":215        text = block.get("text", "")216        return escape(_truncate(text)) if text else None217218    # Server tool call blocks (from AI messages)219    if block_type == "server_tool_call":220        tc_id = quoteattr(str(block.get("id") or ""))221        tc_name = quoteattr(str(block.get("name") or ""))222        tc_args_json = json.dumps(block.get("args", {}), ensure_ascii=False)223        tc_args = escape(_truncate(tc_args_json))224        return (225            f"<server_tool_call id={tc_id} name={tc_name}>{tc_args}</server_tool_call>"226        )227228    # Server tool result blocks229    if block_type == "server_tool_result":230        tool_call_id = quoteattr(str(block.get("tool_call_id") or ""))231        status = quoteattr(str(block.get("status") or ""))232        output = block.get("output")233        if output:234            output_json = json.dumps(output, ensure_ascii=False)235            output_str = escape(_truncate(output_json))236        else:237            output_str = ""238        return (239            f"<server_tool_result tool_call_id={tool_call_id} status={status}>"240            f"{output_str}</server_tool_result>"241        )242243    # Unknown block type - skip silently244    return None245246247def _get_message_type_str(248    m: BaseMessage,249    human_prefix: str,250    ai_prefix: str,251    system_prefix: str,252    function_prefix: str,253    tool_prefix: str,254) -> str:255    """Get the type string for XML message element.256257    Args:258        m: The message to get the type string for.259        human_prefix: The prefix to use for `HumanMessage`.260        ai_prefix: The prefix to use for `AIMessage`.261        system_prefix: The prefix to use for `SystemMessage`.262        function_prefix: The prefix to use for `FunctionMessage`.263        tool_prefix: The prefix to use for `ToolMessage`.264265    Returns:266        The type string for the message element.267268    Raises:269        ValueError: If an unsupported message type is encountered.270    """271    if isinstance(m, HumanMessage):272        return human_prefix.lower()273    if isinstance(m, AIMessage):274        return ai_prefix.lower()275    if isinstance(m, SystemMessage):276        return system_prefix.lower()277    if isinstance(m, FunctionMessage):278        return function_prefix.lower()279    if isinstance(m, ToolMessage):280        return tool_prefix.lower()281    if isinstance(m, ChatMessage):282        return m.role283    msg = f"Got unsupported message type: {m}"284    raise ValueError(msg)285286287_TOOL_CALL_ID_DISPLAY_LIMIT = 64288289290def _display_tool_call_id(tool_call_id: str) -> str:291    """Abbreviate long tool-call IDs in formatted message strings."""292    if len(tool_call_id) > _TOOL_CALL_ID_DISPLAY_LIMIT:293        return f"{tool_call_id[:_TOOL_CALL_ID_DISPLAY_LIMIT]}..."294    return tool_call_id295296297def get_buffer_string(298    messages: Sequence[BaseMessage],299    human_prefix: str = "Human",300    ai_prefix: str = "AI",301    *,302    system_prefix: str = "System",303    function_prefix: str = "Function",304    tool_prefix: str = "Tool",305    message_separator: str = "\n",306    format: Literal["prefix", "xml"] = "prefix",  # noqa: A002307) -> str:308    r"""Convert a sequence of messages to strings and concatenate them into one string.309310    Args:311        messages: Messages to be converted to strings.312        human_prefix: The prefix to prepend to contents of `HumanMessage`s.313        ai_prefix: The prefix to prepend to contents of `AIMessage`.314        system_prefix: The prefix to prepend to contents of `SystemMessage`s.315        function_prefix: The prefix to prepend to contents of `FunctionMessage`s.316        tool_prefix: The prefix to prepend to contents of `ToolMessage`s.317        message_separator: The separator to use between messages.318        format: The output format. `'prefix'` uses `Role: content` format (default).319            For multimodal messages, only string content and `text` blocks are320            included; non-text blocks such as images, audio, and video are omitted.321322            `'xml'` uses XML-style `<message type='role'>` format with proper323            character escaping, which is useful when message content may324            contain role-like prefixes that could cause ambiguity.325326            Use `'xml'` when you need a structured representation of supported327            multimodal content blocks.328329    Returns:330        A single string concatenation of all input messages.331332    Raises:333        ValueError: If an unsupported message type is encountered.334335    !!! warning336337        If a message is an `AIMessage` and contains both tool calls under `tool_calls`338        and a function call under `additional_kwargs["function_call"]`, only the tool339        calls will be appended to the string representation.340        In XML format, tool-call IDs longer than 64 characters are displayed341        as the first 64 characters followed by `...`; the original messages342        are not changed.343344    !!! note "XML format"345346        When using `format='xml'`:347348        - All messages use uniform `<message type="role">content</message>` format.349        - The `type` attribute uses `human_prefix` (lowercased) for `HumanMessage`,350            `ai_prefix` (lowercased) for `AIMessage`, `system_prefix` (lowercased)351            for `SystemMessage`, `function_prefix` (lowercased) for `FunctionMessage`,352            `tool_prefix` (lowercased) for `ToolMessage`, and the original role353            (unchanged) for `ChatMessage`.354        - Message content is escaped using `xml.sax.saxutils.escape()`.355        - Attribute values are escaped using `xml.sax.saxutils.quoteattr()`.356        - AI messages with tool calls use nested structure with `<content>` and357            `<tool_call>` elements.358        - For multi-modal content (list of content blocks), supported block types359            are: `text`, `reasoning`, `image` (URL/file_id only), `image_url`360            (OpenAI-style, URL only), `audio` (URL/file_id only), `video` (URL/file_id361            only), `text-plain`, `server_tool_call`, and `server_tool_result`.362        - Content blocks with base64-encoded data are skipped (including blocks363            with `base64` field or `data:` URLs).364        - Unknown block types are skipped.365        - Plain text document content (`text-plain`), server tool call arguments,366            and server tool result outputs are truncated to 500 characters.367368    Example:369        Default prefix format:370371        ```python372        from langchain_core.messages import AIMessage, HumanMessage, get_buffer_string373374        messages = [375            HumanMessage(content="Hi, how are you?"),376            AIMessage(content="Good, how are you?"),377        ]378        get_buffer_string(messages)379        # -> "Human: Hi, how are you?\nAI: Good, how are you?"380        ```381382        XML format (useful when content contains role-like prefixes):383384        ```python385        messages = [386            HumanMessage(content="Example: Human: some text"),387            AIMessage(content="I see the example."),388        ]389        get_buffer_string(messages, format="xml")390        # -> '<message type="human">Example: Human: some text</message>\\n'391        # -> '<message type="ai">I see the example.</message>'392        ```393394        XML format with special characters (automatically escaped):395396        ```python397        messages = [398            HumanMessage(content="Is 5 < 10 & 10 > 5?"),399        ]400        get_buffer_string(messages, format="xml")401        # -> '<message type="human">Is 5 &lt; 10 &amp; 10 &gt; 5?</message>'402        ```403404        XML format with tool calls:405406        ```python407        messages = [408            AIMessage(409                content="I'll search for that.",410                tool_calls=[411                    {"id": "call_123", "name": "search", "args": {"query": "weather"}}412                ],413            ),414        ]415        get_buffer_string(messages, format="xml")416        # -> '<message type="ai">\\n'417        # -> '  <content>I\\'ll search for that.</content>\\n'418        # -> '  <tool_call id="call_123" name="search">'419        # -> '{"query": "weather"}</tool_call>\\n'420        # -> '</message>'421        ```422    """423    if format not in {"prefix", "xml"}:424        msg = (425            f"Unrecognized format={format!r}. Supported formats are 'prefix' and 'xml'."426        )427        raise ValueError(msg)428429    string_messages = []430    for m in messages:431        if isinstance(m, HumanMessage):432            role = human_prefix433        elif isinstance(m, AIMessage):434            role = ai_prefix435        elif isinstance(m, SystemMessage):436            role = system_prefix437        elif isinstance(m, FunctionMessage):438            role = function_prefix439        elif isinstance(m, ToolMessage):440            role = tool_prefix441        elif isinstance(m, ChatMessage):442            role = m.role443        else:444            msg = f"Got unsupported message type: {m}"445            raise ValueError(msg)  # noqa: TRY004446447        if format == "xml":448            msg_type = _get_message_type_str(449                m, human_prefix, ai_prefix, system_prefix, function_prefix, tool_prefix450            )451452            # Format content blocks453            if isinstance(m.content, str):454                content_parts = [escape(m.content)] if m.content else []455            else:456                # List of content blocks457                content_parts = []458                for block in m.content:459                    if isinstance(block, str):460                        if block:461                            content_parts.append(escape(block))462                    else:463                        formatted = _format_content_block_xml(block)464                        if formatted:465                            content_parts.append(formatted)466467            # Check if this is an AIMessage with tool calls468            has_tool_calls = isinstance(m, AIMessage) and m.tool_calls469            has_function_call = (470                isinstance(m, AIMessage)471                and not m.tool_calls472                and "function_call" in m.additional_kwargs473            )474475            if has_tool_calls or has_function_call:476                # Use nested structure for AI messages with tool calls477                # Type narrowing: at this point m is AIMessage (verified above)478                ai_msg = cast("AIMessage", m)479                parts = [f"<message type={quoteattr(msg_type)}>"]480                if content_parts:481                    parts.append(f"  <content>{' '.join(content_parts)}</content>")482483                if has_tool_calls:484                    for tc in ai_msg.tool_calls:485                        tc_id = quoteattr(_display_tool_call_id(tc.get("id") or ""))486                        tc_name = quoteattr(str(tc.get("name") or ""))487                        tc_args = escape(488                            json.dumps(tc.get("args", {}), ensure_ascii=False)489                        )490                        parts.append(491                            f"  <tool_call id={tc_id} name={tc_name}>"492                            f"{tc_args}</tool_call>"493                        )494                elif has_function_call:495                    fc = ai_msg.additional_kwargs["function_call"]496                    fc_name = quoteattr(str(fc.get("name") or ""))497                    fc_args = escape(str(fc.get("arguments") or "{}"))498                    parts.append(499                        f"  <function_call name={fc_name}>{fc_args}</function_call>"500                    )501502                parts.append("</message>")503                message = "\n".join(parts)504            else:505                # Simple structure for messages without tool calls506                joined_content = " ".join(content_parts)507                message = (508                    f"<message type={quoteattr(msg_type)}>{joined_content}</message>"509                )510        else:  # format == "prefix"511            content = m.text512            message = f"{role}: {content}"513            tool_info = ""514            if isinstance(m, AIMessage):515                if m.tool_calls:516                    tool_info = str(m.tool_calls)517                elif "function_call" in m.additional_kwargs:518                    # Legacy behavior assumes only one function call per message519                    tool_info = str(m.additional_kwargs["function_call"])520            if tool_info:521                message += tool_info  # Preserve original behavior522523        string_messages.append(message)524525    return message_separator.join(string_messages)526527528def _message_from_dict(message: dict[str, Any]) -> BaseMessage:529    type_ = message["type"]530    if type_ == "human":531        return HumanMessage(**message["data"])532    if type_ == "ai":533        return AIMessage(**message["data"])534    if type_ == "system":535        return SystemMessage(**message["data"])536    if type_ == "chat":537        return ChatMessage(**message["data"])538    if type_ == "function":539        return FunctionMessage(**message["data"])540    if type_ == "tool":541        return ToolMessage(**message["data"])542    if type_ == "remove":543        return RemoveMessage(**message["data"])544    if type_ == "AIMessageChunk":545        return AIMessageChunk(**message["data"])546    if type_ == "HumanMessageChunk":547        return HumanMessageChunk(**message["data"])548    if type_ == "FunctionMessageChunk":549        return FunctionMessageChunk(**message["data"])550    if type_ == "ToolMessageChunk":551        return ToolMessageChunk(**message["data"])552    if type_ == "SystemMessageChunk":553        return SystemMessageChunk(**message["data"])554    if type_ == "ChatMessageChunk":555        return ChatMessageChunk(**message["data"])556    msg = f"Got unexpected message type: {type_}"557    raise ValueError(msg)558559560def messages_from_dict(messages: Sequence[dict[str, Any]]) -> list[BaseMessage]:561    """Convert a sequence of messages from dicts to `Message` objects.562563    Args:564        messages: Sequence of messages (as dicts) to convert.565566    Returns:567        list of messages (BaseMessages).568569    """570    return [_message_from_dict(m) for m in messages]571572573def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:574    """Convert a message chunk to a `Message`.575576    Args:577        chunk: Message chunk to convert.578579    Returns:580        Message.581    """582    if not isinstance(chunk, BaseMessageChunk):583        return chunk584    # chunk classes always have the equivalent non-chunk class as their first parent585    ignore_keys = ["type"]586    if isinstance(chunk, AIMessageChunk):587        ignore_keys.extend(["tool_call_chunks", "chunk_position"])588    return cast(589        "BaseMessage",590        chunk.__class__.__mro__[1](591            **{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}592        ),593    )594595596MessageLikeRepresentation = (597    BaseMessage598    | list[str]599    | tuple[str, str | list[str | dict[str, Any]]]600    | str601    | dict[str, Any]602)603"""A type representing the various ways a message can be represented."""604605606def _create_message_from_message_type(607    message_type: str,608    content: str | list[str | dict[str, Any]],609    name: str | None = None,610    tool_call_id: str | None = None,611    tool_calls: list[dict[str, Any]] | None = None,612    id: str | None = None,613    **additional_kwargs: Any,614) -> BaseMessage:615    """Create a message from a `Message` type and content string.616617    Args:618        message_type: the type of the message (e.g., `'human'`, `'ai'`, etc.).619        content: the content string.620        name: the name of the message.621        tool_call_id: the tool call id.622        tool_calls: the tool calls.623        id: the id of the message.624        additional_kwargs: additional keyword arguments.625626    Returns:627        a message of the appropriate type.628629    Raises:630        ValueError: if the message type is not one of `'human'`, `'user'`, `'ai'`,631            `'assistant'`, `'function'`, `'tool'`, `'system'`, or632            `'developer'`.633    """634    kwargs: dict[str, Any] = {}635    if name is not None:636        kwargs["name"] = name637    if tool_call_id is not None:638        kwargs["tool_call_id"] = tool_call_id639    if additional_kwargs:640        if response_metadata := additional_kwargs.pop("response_metadata", None):641            kwargs["response_metadata"] = response_metadata642        kwargs["additional_kwargs"] = additional_kwargs643        additional_kwargs.update(additional_kwargs.pop("additional_kwargs", {}))644    if id is not None:645        kwargs["id"] = id646    if tool_calls is not None:647        kwargs["tool_calls"] = []648        for tool_call in tool_calls:649            # Convert OpenAI-format tool call to LangChain format.650            if "function" in tool_call:651                args = tool_call["function"]["arguments"]652                if isinstance(args, str):653                    args = json.loads(args, strict=False)654                kwargs["tool_calls"].append(655                    {656                        "name": tool_call["function"]["name"],657                        "args": args,658                        "id": tool_call["id"],659                        "type": "tool_call",660                    }661                )662            else:663                kwargs["tool_calls"].append(tool_call)664    if message_type in {"human", "user"}:665        if example := kwargs.get("additional_kwargs", {}).pop("example", False):666            kwargs["example"] = example667        message: BaseMessage = HumanMessage(content=content, **kwargs)668    elif message_type in {"ai", "assistant"}:669        if example := kwargs.get("additional_kwargs", {}).pop("example", False):670            kwargs["example"] = example671        message = AIMessage(content=content, **kwargs)672    elif message_type in {"system", "developer"}:673        if message_type == "developer":674            kwargs["additional_kwargs"] = kwargs.get("additional_kwargs") or {}675            kwargs["additional_kwargs"]["__openai_role__"] = "developer"676        message = SystemMessage(content=content, **kwargs)677    elif message_type == "function":678        message = FunctionMessage(content=content, **kwargs)679    elif message_type == "tool":680        artifact = kwargs.get("additional_kwargs", {}).pop("artifact", None)681        status = kwargs.get("additional_kwargs", {}).pop("status", None)682        if status is not None:683            kwargs["status"] = status684        message = ToolMessage(content=content, artifact=artifact, **kwargs)685    elif message_type == "remove":686        message = RemoveMessage(**kwargs)687    else:688        msg = (689            f"Unexpected message type: '{message_type}'. Use one of 'human',"690            f" 'user', 'ai', 'assistant', 'function', 'tool', 'system', or 'developer'."691        )692        msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)693        raise ValueError(msg)694    return message695696697# Map of class names emitted in the `Serializable` constructor-envelope698# (`{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"],699# "kwargs": {...}}`) to the message-type strings700# `_create_message_from_message_type` accepts. Read by701# `_convert_to_message`'s dict branch when unpacking that wire shape.702# Kept as a hardcoded allowlist of strings rather than a class registry703# lookup so dispatch never resolves to a class chosen by the caller.704_LC_CONSTRUCTOR_NAME_TO_TYPE: dict[str, str] = {705    "HumanMessage": "human",706    "HumanMessageChunk": "human",707    "AIMessage": "ai",708    "AIMessageChunk": "ai",709    "SystemMessage": "system",710    "SystemMessageChunk": "system",711    "FunctionMessage": "function",712    "FunctionMessageChunk": "function",713    "ToolMessage": "tool",714    "ToolMessageChunk": "tool",715    "RemoveMessage": "remove",716}717718719def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:720    """Instantiate a `Message` from a variety of message formats.721722    The message format can be one of the following:723724    - `BaseMessagePromptTemplate`725    - `BaseMessage`726    - 2-tuple of (role string, template); e.g., (`'human'`, `'{user_input}'`)727    - dict: a message dict with role and content keys728    - dict: the `Serializable` constructor-envelope wire shape729        `{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"],730        "kwargs": {...}}` — unpacked structurally and routed through the731        standard dict-with-type dispatch.732    - string: shorthand for (`'human'`, template); e.g., `'{user_input}'`733734    Args:735        message: a representation of a message in one of the supported formats.736737    Returns:738        An instance of a message or a message template.739740    Raises:741        NotImplementedError: if the message type is not supported.742        ValueError: if the message dict does not contain the required keys.743744    """745    if isinstance(message, BaseMessage):746        message_ = message747    elif isinstance(message, Sequence):748        if isinstance(message, str):749            message_ = _create_message_from_message_type("human", message)750        else:751            try:752                message_type_str, template = message753            except ValueError as e:754                msg = "Message as a sequence must be (role string, template)"755                raise NotImplementedError(msg) from e756            message_ = _create_message_from_message_type(message_type_str, template)757    elif isinstance(message, dict):758        # `Serializable` constructor-envelope wire shape. Detect structurally, map759        # the class name to a known message-type string via a hardcoded760        # allowlist, and recurse with the canonical761        # `{"type": ..., **kwargs}` shape — no `load()`, no dynamic762        # class instantiation.763        if (764            message.get("lc") == 1765            and message.get("type") == "constructor"766            and isinstance(message.get("id"), list)767            and message["id"]768            and isinstance(message.get("kwargs"), dict)769        ):770            mapped = _LC_CONSTRUCTOR_NAME_TO_TYPE.get(message["id"][-1])771            if mapped is not None:772                return _convert_to_message({"type": mapped, **message["kwargs"]})773774        msg_kwargs = message.copy()775        try:776            try:777                msg_type = msg_kwargs.pop("role")778            except KeyError:779                msg_type = msg_kwargs.pop("type")780            # None msg content is not allowed781            msg_content = msg_kwargs.pop("content") or ""782        except KeyError as e:783            msg = f"Message dict must contain 'role' and 'content' keys, got {message}"784            msg = create_message(785                message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE786            )787            raise ValueError(msg) from e788        message_ = _create_message_from_message_type(789            msg_type, msg_content, **msg_kwargs790        )791    else:792        msg = f"Unsupported message type: {type(message)}"  # type: ignore[unreachable]793        msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)794        raise NotImplementedError(msg)795796    return message_797798799def convert_to_messages(800    messages: Iterable[MessageLikeRepresentation] | PromptValue,801) -> list[BaseMessage]:802    """Convert a sequence of messages to a list of messages.803804    Args:805        messages: Sequence of messages to convert.806807    Returns:808        list of messages (BaseMessages).809810    """811    # Import here to avoid circular imports812    from langchain_core.prompt_values import PromptValue  # noqa: PLC0415813814    if isinstance(messages, PromptValue):815        return messages.to_messages()816    return [_convert_to_message(m) for m in messages]817818819_P = ParamSpec("_P")820_R_co = TypeVar("_R_co", covariant=True)821822823class _RunnableSupportCallable(Protocol[_P, _R_co]):824    @overload825    def __call__(826        self,827        messages: None = None,828        *args: _P.args,829        **kwargs: _P.kwargs,830    ) -> Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...831832    @overload833    def __call__(834        self,835        messages: Sequence[MessageLikeRepresentation] | PromptValue,836        *args: _P.args,837        **kwargs: _P.kwargs,838    ) -> _R_co: ...839840    def __call__(841        self,842        messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,843        *args: _P.args,844        **kwargs: _P.kwargs,845    ) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...846847848def _runnable_support(849    func: Callable[850        Concatenate[Sequence[MessageLikeRepresentation] | PromptValue, _P], _R_co851    ],852) -> _RunnableSupportCallable[_P, _R_co]:853    @wraps(func)854    def wrapped(855        messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,856        *args: _P.args,857        **kwargs: _P.kwargs,858    ) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]:859        # Import locally to prevent circular import.860        from langchain_core.runnables.base import RunnableLambda  # noqa: PLC0415861862        if messages is not None:863            return func(messages, *args, **kwargs)864        return RunnableLambda(partial(func, **kwargs), name=func.__name__)865866    return cast("_RunnableSupportCallable[_P, _R_co]", wrapped)867868869@_runnable_support870def filter_messages(871    messages: Iterable[MessageLikeRepresentation] | PromptValue,872    *,873    include_names: Sequence[str] | None = None,874    exclude_names: Sequence[str] | None = None,875    include_types: Sequence[str | type[BaseMessage]] | None = None,876    exclude_types: Sequence[str | type[BaseMessage]] | None = None,877    include_ids: Sequence[str] | None = None,878    exclude_ids: Sequence[str] | None = None,879    exclude_tool_calls: Sequence[str] | bool | None = None,880) -> list[BaseMessage]:881    """Filter messages based on `name`, `type` or `id`.882883    Args:884        messages: Sequence Message-like objects to filter.885        include_names: Message names to include.886        exclude_names: Messages names to exclude.887        include_types: Message types to include. Can be specified as string names888            (e.g. `'system'`, `'human'`, `'ai'`, ...) or as `BaseMessage`889            classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`, ...).890891        exclude_types: Message types to exclude. Can be specified as string names892            (e.g. `'system'`, `'human'`, `'ai'`, ...) or as `BaseMessage`893            classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`, ...).894895        include_ids: Message IDs to include.896        exclude_ids: Message IDs to exclude.897        exclude_tool_calls: Tool call IDs to exclude.898            Can be one of the following:899            - `True`: All `AIMessage` objects with tool calls and all `ToolMessage`900                objects will be excluded.901            - a sequence of tool call IDs to exclude:902                - `ToolMessage` objects with the corresponding tool call ID will be903                    excluded.904                - The `tool_calls` in the AIMessage will be updated to exclude905                    matching tool calls. If all `tool_calls` are filtered from an906                    AIMessage, the whole message is excluded.907908    Returns:909        A list of Messages that meets at least one of the `incl_*` conditions and none910        of the `excl_*` conditions. If not `incl_*` conditions are specified then911        anything that is not explicitly excluded will be included.912913    Raises:914        ValueError: If two incompatible arguments are provided.915916    Example:917        ```python918        from langchain_core.messages import (919            filter_messages,920            AIMessage,921            HumanMessage,922            SystemMessage,923        )924925        messages = [926            SystemMessage("you're a good assistant."),927            HumanMessage("what's your name", id="foo", name="example_user"),928            AIMessage("steve-o", id="bar", name="example_assistant"),929            HumanMessage(930                "what's your favorite color",931                id="baz",932            ),933            AIMessage(934                "silicon blue",935                id="blah",936            ),937        ]938939        filter_messages(940            messages,941            include_names=("example_user", "example_assistant"),942            include_types=("system",),943            exclude_ids=("bar",),944        )945        ```946947        ```python948        [949            SystemMessage("you're a good assistant."),950            HumanMessage("what's your name", id="foo", name="example_user"),951        ]952        ```953    """954    messages = convert_to_messages(messages)955    filtered: list[BaseMessage] = []956    for msg in messages:957        if (958            (exclude_names and msg.name in exclude_names)959            or (exclude_types and _is_message_type(msg, exclude_types))960            or (exclude_ids and msg.id in exclude_ids)961        ):962            continue963964        if exclude_tool_calls is True and (965            (isinstance(msg, AIMessage) and msg.tool_calls)966            or isinstance(msg, ToolMessage)967        ):968            continue969970        new_msg = msg971        if isinstance(exclude_tool_calls, (list, tuple, set)):972            if isinstance(msg, AIMessage) and msg.tool_calls:973                tool_calls = [974                    tool_call975                    for tool_call in msg.tool_calls976                    if tool_call["id"] not in exclude_tool_calls977                ]978                if not tool_calls:979                    continue980981                content = msg.content982                # handle Anthropic content blocks983                if isinstance(msg.content, list):984                    content = [985                        content_block986                        for content_block in msg.content987                        if (988                            not isinstance(content_block, dict)989                            or content_block.get("type") != "tool_use"990                            or content_block.get("id") not in exclude_tool_calls991                        )992                    ]993994                new_msg = msg.model_copy(995                    update={"tool_calls": tool_calls, "content": content}996                )997            elif (998                isinstance(msg, ToolMessage) and msg.tool_call_id in exclude_tool_calls999            ):1000                continue10011002        # default to inclusion when no inclusion criteria given.1003        if (1004            not (include_types or include_ids or include_names)1005            or (include_names and new_msg.name in include_names)1006            or (include_types and _is_message_type(new_msg, include_types))1007            or (include_ids and new_msg.id in include_ids)1008        ):1009            filtered.append(new_msg)10101011    return filtered101210131014@_runnable_support1015def merge_message_runs(1016    messages: Iterable[MessageLikeRepresentation] | PromptValue,1017    *,1018    chunk_separator: str = "\n",1019) -> list[BaseMessage]:1020    r"""Merge consecutive Messages of the same type.10211022    !!! note1023        `ToolMessage` objects are not merged, as each has a distinct tool call id that1024        can't be merged.10251026    Args:1027        messages: Sequence Message-like objects to merge.1028        chunk_separator: Specify the string to be inserted between message chunks.10291030    Returns:1031        list of BaseMessages with consecutive runs of message types merged into single1032        messages. By default, if two messages being merged both have string contents,1033        the merged content is a concatenation of the two strings with a new-line1034        separator.1035        The separator inserted between message chunks can be controlled by specifying1036        any string with `chunk_separator`. If at least one of the messages has a list1037        of content blocks, the merged content is a list of content blocks.10381039    Example:1040        ```python1041        from langchain_core.messages import (1042            merge_message_runs,1043            AIMessage,1044            HumanMessage,1045            SystemMessage,1046            ToolCall,1047        )10481049        messages = [1050            SystemMessage("you're a good assistant."),1051            HumanMessage(1052                "what's your favorite color",1053                id="foo",1054            ),1055            HumanMessage(1056                "wait your favorite food",1057                id="bar",1058            ),1059            AIMessage(1060                "my favorite colo",1061                tool_calls=[1062                    ToolCall(1063                        name="blah_tool", args={"x": 2}, id="123", type="tool_call"1064                    )1065                ],1066                id="baz",1067            ),1068            AIMessage(1069                [{"type": "text", "text": "my favorite dish is lasagna"}],1070                tool_calls=[1071                    ToolCall(1072                        name="blah_tool",1073                        args={"x": -10},1074                        id="456",1075                        type="tool_call",1076                    )1077                ],1078                id="blur",1079            ),1080        ]10811082        merge_message_runs(messages)1083        ```10841085        ```python1086        [1087            SystemMessage("you're a good assistant."),1088            HumanMessage(1089                "what's your favorite color\\n"1090                "wait your favorite food", id="foo",1091            ),1092            AIMessage(1093                [1094                    "my favorite colo",1095                    {"type": "text", "text": "my favorite dish is lasagna"}1096                ],1097                tool_calls=[1098                    ToolCall({1099                        "name": "blah_tool",1100                        "args": {"x": 2},1101                        "id": "123",1102                        "type": "tool_call"1103                    }),1104                    ToolCall({1105                        "name": "blah_tool",1106                        "args": {"x": -10},1107                        "id": "456",1108                        "type": "tool_call"1109                    })1110                ]1111                id="baz"1112            ),1113        ]11141115        ```1116    """1117    if not messages:1118        return []1119    messages = convert_to_messages(messages)1120    merged: list[BaseMessage] = []1121    for msg in messages:1122        last = merged.pop() if merged else None1123        if not last:1124            merged.append(msg)1125        elif isinstance(msg, ToolMessage) or not isinstance(msg, last.__class__):1126            merged.extend([last, msg])1127        else:1128            last_chunk = _msg_to_chunk(last)1129            curr_chunk = _msg_to_chunk(msg)1130            if curr_chunk.response_metadata:1131                curr_chunk.response_metadata.clear()1132            if (1133                isinstance(last_chunk.content, str)1134                and isinstance(curr_chunk.content, str)1135                and last_chunk.content1136                and curr_chunk.content1137            ):1138                last_chunk.content += chunk_separator1139            merged.append(_chunk_to_msg(last_chunk + curr_chunk))1140    return merged114111421143# TODO: Update so validation errors (for token_counter, for example) are raised on1144# init not at runtime.1145@_runnable_support1146def trim_messages(1147    messages: Iterable[MessageLikeRepresentation] | PromptValue,1148    *,1149    max_tokens: int,1150    token_counter: Callable[[list[BaseMessage]], int]1151    | Callable[[BaseMessage], int]1152    | BaseLanguageModel[Any]1153    | Literal["approximate"],1154    strategy: Literal["first", "last"] = "last",1155    allow_partial: bool = False,1156    end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1157    start_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1158    include_system: bool = False,1159    text_splitter: Callable[[str], list[str]] | TextSplitter | None = None,1160) -> list[BaseMessage]:1161    r"""Trim messages to be below a token count.11621163    `trim_messages` can be used to reduce the size of a chat history to a specified1164    token or message count.11651166    In either case, if passing the trimmed chat history back into a chat model1167    directly, the resulting chat history should usually satisfy the following1168    properties:11691170    1. The resulting chat history should be valid. Most chat models expect that chat1171        history starts with either (1) a `HumanMessage` or (2) a `SystemMessage`1172        followed by a `HumanMessage`. To achieve this, set `start_on='human'`.1173        In addition, generally a `ToolMessage` can only appear after an `AIMessage`1174        that involved a tool call.1175    2. It includes recent messages and drops old messages in the chat history.1176        To achieve this set the `strategy='last'`.1177    3. Usually, the new chat history should include the `SystemMessage` if it1178        was present in the original chat history since the `SystemMessage` includes1179        special instructions to the chat model. The `SystemMessage` is almost always1180        the first message in the history if present. To achieve this set the1181        `include_system=True`.11821183    !!! note1184        The examples below show how to configure `trim_messages` to achieve a behavior1185        consistent with the above properties.11861187    Args:1188        messages: Sequence of Message-like objects to trim.1189        max_tokens: Max token count of trimmed messages.1190        token_counter: Function or llm for counting tokens in a `BaseMessage` or a1191            list of `BaseMessage`.11921193            If a `BaseLanguageModel` is passed in then1194            `BaseLanguageModel.get_num_tokens_from_messages()` will be used. Set to1195            `len` to count the number of **messages** in the chat history.11961197            You can also use string shortcuts for convenience:11981199            - `'approximate'`: Uses `count_tokens_approximately` for fast, approximate1200                token counts.12011202            !!! note12031204                `count_tokens_approximately` (or the shortcut `'approximate'`) is1205                recommended for using `trim_messages` on the hot path, where exact token1206                counting is not necessary.12071208        strategy: Strategy for trimming.12091210            - `'first'`: Keep the first `<= n_count` tokens of the messages.1211            - `'last'`: Keep the last `<= n_count` tokens of the messages.1212        allow_partial: Whether to split a message if only part of the message can be1213            included.12141215            If `strategy='last'` then the last partial contents of a message are1216            included. If `strategy='first'` then the first partial contents of a1217            message are included.1218        end_on: The message type to end on.12191220            If specified then every message after the last occurrence of this type is1221            ignored. If `strategy='last'` then this is done before we attempt to get the1222            last `max_tokens`. If `strategy='first'` then this is done after we get the1223            first `max_tokens`. Can be specified as string names (e.g. `'system'`,1224            `'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g. `SystemMessage`,1225            `HumanMessage`, `AIMessage`, ...). Can be a single type or a list of types.12261227        start_on: The message type to start on.12281229            Should only be specified if `strategy='last'`. If specified then every1230            message before the first occurrence of this type is ignored. This is done1231            after we trim the initial messages to the last `max_tokens`. Does not apply1232            to a `SystemMessage` at index 0 if `include_system=True`. Can be specified1233            as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or as1234            `BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`,1235            ...). Can be a single type or a list of types.12361237        include_system: Whether to keep the `SystemMessage` if there is one at index1238            `0`.12391240            Should only be specified if `strategy="last"`.1241        text_splitter: Function or `langchain_text_splitters.TextSplitter` for1242            splitting the string contents of a message.12431244            Only used if `allow_partial=True`. If `strategy='last'` then the last split1245            tokens from a partial message will be included. if `strategy='first'` then1246            the first split tokens from a partial message will be included. Token1247            splitter assumes that separators are kept, so that split contents can be1248            directly concatenated to recreate the original text. Defaults to splitting1249            on newlines.12501251    Returns:1252        List of trimmed `BaseMessage`.12531254    Raises:1255        ValueError: if two incompatible arguments are specified or an unrecognized1256            `strategy` is specified.12571258    Example:1259        Trim chat history based on token count, keeping the `SystemMessage` if1260        present, and ensuring that the chat history starts with a `HumanMessage` (or a1261        `SystemMessage` followed by a `HumanMessage`).12621263        ```python1264        from langchain_core.messages import (1265            AIMessage,1266            HumanMessage,1267            BaseMessage,1268            SystemMessage,1269            trim_messages,1270        )12711272        messages = [1273            SystemMessage("you're a good assistant, you always respond with a joke."),1274            HumanMessage("i wonder why it's called langchain"),1275            AIMessage(1276                'Well, I guess they thought "WordRope" and "SentenceString" just '1277                "didn't have the same ring to it!"1278            ),1279            HumanMessage("and who is harrison chasing anyways"),1280            AIMessage(1281                "Hmmm let me think.\n\nWhy, he's probably chasing after the last "1282                "cup of coffee in the office!"1283            ),1284            HumanMessage("what do you call a speechless parrot"),1285        ]128612871288        trim_messages(1289            messages,1290            max_tokens=45,1291            strategy="last",1292            token_counter=ChatOpenAI(model="gpt-5.5"),1293            # Most chat models expect that chat history starts with either:1294            # (1) a HumanMessage or1295            # (2) a SystemMessage followed by a HumanMessage1296            start_on="human",1297            # Usually, we want to keep the SystemMessage1298            # if it's present in the original history.1299            # The SystemMessage has special instructions for the model.1300            include_system=True,1301            allow_partial=False,1302        )1303        ```13041305        ```python1306        [1307            SystemMessage(1308                content="you're a good assistant, you always respond with a joke."1309            ),1310            HumanMessage(content="what do you call a speechless parrot"),1311        ]1312        ```13131314        Trim chat history using approximate token counting with `'approximate'`:13151316        ```python1317        trim_messages(1318            messages,1319            max_tokens=45,1320            strategy="last",1321            # Using the "approximate" shortcut for fast token counting1322            token_counter="approximate",1323            start_on="human",1324            include_system=True,1325        )13261327        # This is equivalent to using `count_tokens_approximately` directly1328        from langchain_core.messages.utils import count_tokens_approximately13291330        trim_messages(1331            messages,1332            max_tokens=45,1333            strategy="last",1334            token_counter=count_tokens_approximately,1335            start_on="human",1336            include_system=True,1337        )1338        ```13391340        Trim chat history based on the message count, keeping the `SystemMessage` if1341        present, and ensuring that the chat history starts with a HumanMessage (1342        or a `SystemMessage` followed by a `HumanMessage`).13431344            trim_messages(1345                messages,1346                # When `len` is passed in as the token counter function,1347                # max_tokens will count the number of messages in the chat history.1348                max_tokens=4,1349                strategy="last",1350                # Passing in `len` as a token counter function will1351                # count the number of messages in the chat history.1352                token_counter=len,1353                # Most chat models expect that chat history starts with either:1354                # (1) a HumanMessage or1355                # (2) a SystemMessage followed by a HumanMessage1356                start_on="human",1357                # Usually, we want to keep the SystemMessage1358                # if it's present in the original history.1359                # The SystemMessage has special instructions for the model.1360                include_system=True,1361                allow_partial=False,1362            )13631364        ```python1365        [1366            SystemMessage(1367                content="you're a good assistant, you always respond with a joke."1368            ),1369            HumanMessage(content="and who is harrison chasing anyways"),1370            AIMessage(1371                content="Hmmm let me think.\n\nWhy, he's probably chasing after "1372                "the last cup of coffee in the office!"1373            ),1374            HumanMessage(content="what do you call a speechless parrot"),1375        ]1376        ```1377        Trim chat history using a custom token counter function that counts the1378        number of tokens in each message.13791380        ```python1381        messages = [1382            SystemMessage("This is a 4 token text. The full message is 10 tokens."),1383            HumanMessage(1384                "This is a 4 token text. The full message is 10 tokens.", id="first"1385            ),1386            AIMessage(1387                [1388                    {"type": "text", "text": "This is the FIRST 4 token block."},1389                    {"type": "text", "text": "This is the SECOND 4 token block."},1390                ],1391                id="second",1392            ),1393            HumanMessage(1394                "This is a 4 token text. The full message is 10 tokens.", id="third"1395            ),1396            AIMessage(1397                "This is a 4 token text. The full message is 10 tokens.",1398                id="fourth",1399            ),1400        ]140114021403        def dummy_token_counter(messages: list[BaseMessage]) -> int:1404            # treat each message like it adds 3 default tokens at the beginning1405            # of the message and at the end of the message. 3 + 4 + 3 = 10 tokens1406            # per message.14071408            default_content_len = 41409            default_msg_prefix_len = 31410            default_msg_suffix_len = 314111412            count = 01413            for msg in messages:1414                if isinstance(msg.content, str):1415                    count += (1416                        default_msg_prefix_len1417                        + default_content_len1418                        + default_msg_suffix_len1419                    )1420                if isinstance(msg.content, list):1421                    count += (1422                        default_msg_prefix_len1423                        + len(msg.content) * default_content_len1424                        + default_msg_suffix_len1425                    )1426            return count1427        ```14281429        First 30 tokens, allowing partial messages:1430        ```python1431        trim_messages(1432            messages,1433            max_tokens=30,1434            token_counter=dummy_token_counter,1435            strategy="first",1436            allow_partial=True,1437        )1438        ```14391440        ```python1441        [1442            SystemMessage("This is a 4 token text. The full message is 10 tokens."),1443            HumanMessage(1444                "This is a 4 token text. The full message is 10 tokens.",1445                id="first",1446            ),1447            AIMessage(1448                [{"type": "text", "text": "This is the FIRST 4 token block."}],1449                id="second",1450            ),1451        ]1452        ```1453    """1454    # Validate arguments1455    if start_on and strategy == "first":1456        msg = "start_on parameter is only valid with strategy='last'"1457        raise ValueError(msg)1458    if include_system and strategy == "first":1459        msg = "include_system parameter is only valid with strategy='last'"1460        raise ValueError(msg)14611462    messages = convert_to_messages(messages)14631464    # Handle string shortcuts for token counter1465    if isinstance(token_counter, str):1466        if token_counter in _TOKEN_COUNTER_SHORTCUTS:1467            actual_token_counter = _TOKEN_COUNTER_SHORTCUTS[token_counter]1468        else:1469            available_shortcuts = ", ".join(1470                f"'{key}'" for key in _TOKEN_COUNTER_SHORTCUTS1471            )1472            msg = (1473                f"Invalid token_counter shortcut '{token_counter}'. "1474                f"Available shortcuts: {available_shortcuts}."1475            )1476            raise ValueError(msg)1477    else:1478        # Type narrowing: at this point token_counter is not a str1479        actual_token_counter = token_counter  # type: ignore[assignment]14801481    if hasattr(actual_token_counter, "get_num_tokens_from_messages"):1482        list_token_counter = actual_token_counter.get_num_tokens_from_messages1483    elif callable(actual_token_counter):1484        if (1485            next(1486                iter(inspect.signature(actual_token_counter).parameters.values())1487            ).annotation1488            is BaseMessage1489        ):14901491            def list_token_counter(messages: Sequence[BaseMessage]) -> int:1492                return sum(actual_token_counter(msg) for msg in messages)  # type: ignore[arg-type, misc]14931494        else:1495            list_token_counter = actual_token_counter1496    else:1497        msg = (  # type: ignore[unreachable]1498            f"'token_counter' expected to be a model that implements "1499            f"'get_num_tokens_from_messages()' or a function. Received object of type "1500            f"{type(actual_token_counter)}."1501        )1502        raise ValueError(msg)15031504    text_splitter_fn: Callable[[str], list[str]]1505    if _HAS_LANGCHAIN_TEXT_SPLITTERS and isinstance(text_splitter, TextSplitter):1506        text_splitter_fn = text_splitter.split_text1507    elif text_splitter:1508        text_splitter_fn = cast("Callable[[str], list[str]]", text_splitter)1509    else:1510        text_splitter_fn = _default_text_splitter15111512    if strategy == "first":1513        return _first_max_tokens(1514            messages,1515            max_tokens=max_tokens,1516            token_counter=list_token_counter,1517            text_splitter=text_splitter_fn,1518            partial_strategy="first" if allow_partial else None,1519            end_on=end_on,1520        )1521    if strategy == "last":1522        return _last_max_tokens(1523            messages,1524            max_tokens=max_tokens,1525            token_counter=list_token_counter,1526            allow_partial=allow_partial,1527            include_system=include_system,1528            start_on=start_on,1529            end_on=end_on,1530            text_splitter=text_splitter_fn,1531        )1532    msg = f"Unrecognized {strategy=}. Supported strategies are 'last' and 'first'."  # type: ignore[unreachable]1533    raise ValueError(msg)153415351536_SingleMessage = BaseMessage | str | dict[str, Any]1537_T = TypeVar("_T", bound=_SingleMessage)1538# A sequence of _SingleMessage that is NOT a bare str1539_MultipleMessages = Sequence[_T]154015411542@overload1543def convert_to_openai_messages(1544    messages: _SingleMessage,1545    *,1546    text_format: Literal["string", "block"] = "string",1547    include_id: bool = False,1548    pass_through_unknown_blocks: bool = True,1549) -> dict[str, Any]: ...155015511552@overload1553def convert_to_openai_messages(1554    messages: _MultipleMessages[Any],1555    *,1556    text_format: Literal["string", "block"] = "string",1557    include_id: bool = False,1558    pass_through_unknown_blocks: bool = True,1559) -> list[dict[str, Any]]: ...156015611562def convert_to_openai_messages(1563    messages: MessageLikeRepresentation | Sequence[MessageLikeRepresentation],1564    *,1565    text_format: Literal["string", "block"] = "string",1566    include_id: bool = False,1567    pass_through_unknown_blocks: bool = True,1568) -> dict[str, Any] | list[dict[str, Any]]:1569    """Convert LangChain messages into OpenAI message dicts.15701571    Args:1572        messages: Message-like object or iterable of objects whose contents are1573            in OpenAI, Anthropic, Bedrock Converse, or VertexAI formats.1574        text_format: How to format string or text block contents:1575            - `'string'`:1576                If a message has a string content, this is left as a string. If1577                a message has content blocks that are all of type `'text'`, these1578                are joined with a newline to make a single string. If a message has1579                content blocks and at least one isn't of type `'text'`, then1580                all blocks are left as dicts.1581            - `'block'`:1582                If a message has a string content, this is turned into a list1583                with a single content block of type `'text'`. If a message has1584                content blocks these are left as is.1585        include_id: Whether to include message IDs in the openai messages, if they1586            are present in the source messages.1587        pass_through_unknown_blocks: Whether to include content blocks with unknown1588            formats in the output. If `False`, an error is raised if an unknown1589            content block is encountered.15901591    Raises:1592        ValueError: if an unrecognized `text_format` is specified, or if a message1593            content block is missing expected keys.15941595    Returns:1596        The return type depends on the input type:15971598        - dict:1599            If a single message-like object is passed in, a single OpenAI message1600            dict is returned.1601        - list[dict]:1602            If a sequence of message-like objects are passed in, a list of OpenAI1603            message dicts is returned.16041605    Example:1606        ```python1607        from langchain_core.messages import (1608            convert_to_openai_messages,1609            AIMessage,1610            SystemMessage,1611            ToolMessage,1612        )16131614        messages = [1615            SystemMessage([{"type": "text", "text": "foo"}]),1616            {1617                "role": "user",1618                "content": [1619                    {"type": "text", "text": "what's in this"},1620                    {1621                        "type": "image_url",1622                        "image_url": {"url": "data:image/png;base64,'/9j/4AAQSk'"},1623                    },1624                ],1625            },1626            AIMessage(1627                "",1628                tool_calls=[1629                    {1630                        "name": "analyze",1631                        "args": {"baz": "buz"},1632                        "id": "1",1633                        "type": "tool_call",1634                    }1635                ],1636            ),1637            ToolMessage("foobar", tool_call_id="1", name="bar"),1638            {"role": "assistant", "content": "that's nice"},1639        ]1640        oai_messages = convert_to_openai_messages(messages)1641        # -> [1642        #   {'role': 'system', 'content': 'foo'},1643        #   {'role': 'user', 'content': [{'type': 'text', 'text': 'what's in this'}, {'type': 'image_url', 'image_url': {'url': "data:image/png;base64,'/9j/4AAQSk'"}}]},1644        #   {'role': 'assistant', 'tool_calls': [{'type': 'function', 'id': '1','function': {'name': 'analyze', 'arguments': '{"baz": "buz"}'}}], 'content': ''},1645        #   {'role': 'tool', 'name': 'bar', 'content': 'foobar'},1646        #   {'role': 'assistant', 'content': 'that's nice'}1647        # ]1648        ```16491650    !!! version-added "Added in `langchain-core` 0.3.11"16511652    """  # noqa: E5011653    if text_format not in {"string", "block"}:1654        err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'."1655        raise ValueError(err)16561657    oai_messages: list[dict[str, Any]] = []16581659    messages_: Sequence[MessageLikeRepresentation]1660    if is_single := isinstance(messages, (BaseMessage, dict, str)):1661        messages_ = [messages]1662    else:1663        messages_ = cast("Sequence[MessageLikeRepresentation]", messages)16641665    for i, message in enumerate(convert_to_messages(messages_)):1666        oai_msg: dict[str, Any] = {"role": _get_message_openai_role(message)}1667        tool_messages: list[dict[str, Any]] = []1668        content: str | list[dict[str, Any]]16691670        if message.name:1671            oai_msg["name"] = message.name1672        if isinstance(message, AIMessage) and message.tool_calls:1673            oai_msg["tool_calls"] = _convert_to_openai_tool_calls(message.tool_calls)1674        if message.additional_kwargs.get("refusal"):1675            oai_msg["refusal"] = message.additional_kwargs["refusal"]1676        if isinstance(message, ToolMessage):1677            oai_msg["tool_call_id"] = message.tool_call_id1678        if include_id and message.id:1679            oai_msg["id"] = message.id16801681        if not message.content:1682            content = "" if text_format == "string" else []1683        elif isinstance(message.content, str):1684            if text_format == "string":1685                content = message.content1686            else:1687                content = [{"type": "text", "text": message.content}]1688        elif text_format == "string" and all(1689            isinstance(block, str) or block.get("type") == "text"1690            for block in message.content1691        ):1692            content = "\n".join(1693                block if isinstance(block, str) else block["text"]1694                for block in message.content1695            )1696        else:1697            content = []1698            for j, block in enumerate(message.content):1699                # OpenAI format1700                if isinstance(block, str):1701                    content.append({"type": "text", "text": block})1702                elif block.get("type") == "text":1703                    if missing := [k for k in ("text",) if k not in block]:1704                        err = (1705                            f"Unrecognized content block at "1706                            f"messages[{i}].content[{j}] has 'type': 'text' "1707                            f"but is missing expected key(s) "1708                            f"{missing}. Full content block:\n\n{block}"1709                        )1710                        raise ValueError(err)1711                    content.append({"type": block["type"], "text": block["text"]})1712                elif block.get("type") == "image_url":1713                    if missing := [k for k in ("image_url",) if k not in block]:1714                        err = (1715                            f"Unrecognized content block at "1716                            f"messages[{i}].content[{j}] has 'type': 'image_url' "1717                            f"but is missing expected key(s) "1718                            f"{missing}. Full content block:\n\n{block}"1719                        )1720                        raise ValueError(err)1721                    content.append(1722                        {1723                            "type": "image_url",1724                            "image_url": block["image_url"],1725                        }1726                    )1727                # Standard multi-modal content block1728                elif is_data_content_block(block):1729                    formatted_block = convert_to_openai_data_block(block)1730                    if (1731                        formatted_block.get("type") == "file"1732                        and "file" in formatted_block1733                        and "filename" not in formatted_block["file"]1734                    ):1735                        logger.info("Generating a fallback filename.")1736                        formatted_block["file"]["filename"] = "LC_AUTOGENERATED"1737                    content.append(formatted_block)1738                # Anthropic and Bedrock converse format1739                elif (block.get("type") == "image") or "image" in block:1740                    # Anthropic1741                    if source := block.get("source"):1742                        if missing := [1743                            k for k in ("media_type", "type", "data") if k not in source1744                        ]:1745                            err = (1746                                f"Unrecognized content block at "1747                                f"messages[{i}].content[{j}] has 'type': 'image' "1748                                f"but 'source' is missing expected key(s) "1749                                f"{missing}. Full content block:\n\n{block}"1750                            )1751                            raise ValueError(err)1752                        content.append(1753                            {1754                                "type": "image_url",1755                                "image_url": {1756                                    "url": (1757                                        f"data:{source['media_type']};"1758                                        f"{source['type']},{source['data']}"1759                                    )1760                                },1761                            }1762                        )1763                    # Bedrock converse1764                    elif image := block.get("image"):1765                        if missing := [1766                            k for k in ("source", "format") if k not in image1767                        ]:1768                            err = (1769                                f"Unrecognized content block at "1770                                f"messages[{i}].content[{j}] has key 'image', "1771                                f"but 'image' is missing expected key(s) "1772                                f"{missing}. Full content block:\n\n{block}"1773                            )1774                            raise ValueError(err)1775                        b64_image = _bytes_to_b64_str(image["source"]["bytes"])1776                        content.append(1777                            {1778                                "type": "image_url",1779                                "image_url": {1780                                    "url": (1781                                        f"data:image/{image['format']};base64,{b64_image}"1782                                    )1783                                },1784                            }1785                        )1786                    else:1787                        err = (1788                            f"Unrecognized content block at "1789                            f"messages[{i}].content[{j}] has 'type': 'image' "1790                            f"but does not have a 'source' or 'image' key. Full "1791                            f"content block:\n\n{block}"1792                        )1793                        raise ValueError(err)1794                # OpenAI file format1795                elif (1796                    block.get("type") == "file"1797                    and isinstance(block.get("file"), dict)1798                    and isinstance(block.get("file", {}).get("file_data"), str)1799                ):1800                    if block.get("file", {}).get("filename") is None:1801                        logger.info("Generating a fallback filename.")1802                        formatted_block = {1803                            **block,1804                            "file": {**block["file"], "filename": "LC_AUTOGENERATED"},1805                        }1806                        content.append(formatted_block)1807                    else:1808                        content.append(block)1809                # OpenAI audio format1810                elif (1811                    block.get("type") == "input_audio"1812                    and isinstance(block.get("input_audio"), dict)1813                    and isinstance(block.get("input_audio", {}).get("data"), str)1814                    and isinstance(block.get("input_audio", {}).get("format"), str)1815                ):1816                    content.append(block)1817                elif block.get("type") == "tool_use":1818                    if missing := [1819                        k for k in ("id", "name", "input") if k not in block1820                    ]:1821                        err = (1822                            f"Unrecognized content block at "1823                            f"messages[{i}].content[{j}] has 'type': "1824                            f"'tool_use', but is missing expected key(s) "1825                            f"{missing}. Full content block:\n\n{block}"1826                        )1827                        raise ValueError(err)1828                    if not any(1829                        tool_call["id"] == block["id"]1830                        for tool_call in cast("AIMessage", message).tool_calls1831                    ):1832                        oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])1833                        oai_msg["tool_calls"].append(1834                            {1835                                "type": "function",1836                                "id": block["id"],1837                                "function": {1838                                    "name": block["name"],1839                                    "arguments": json.dumps(1840                                        block["input"], ensure_ascii=False1841                                    ),1842                                },1843                            }1844                        )1845                elif block.get("type") == "function_call":  # OpenAI Responses1846                    if not any(1847                        tool_call["id"] == block.get("call_id")1848                        for tool_call in cast("AIMessage", message).tool_calls1849                    ):1850                        if missing := [1851                            k1852                            for k in ("call_id", "name", "arguments")1853                            if k not in block1854                        ]:1855                            err = (1856                                f"Unrecognized content block at "1857                                f"messages[{i}].content[{j}] has 'type': "1858                                f"'tool_use', but is missing expected key(s) "1859                                f"{missing}. Full content block:\n\n{block}"1860                            )1861                            raise ValueError(err)1862                        oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])1863                        oai_msg["tool_calls"].append(1864                            {1865                                "type": "function",1866                                "id": block.get("call_id"),1867                                "function": {1868                                    "name": block.get("name"),1869                                    "arguments": block.get("arguments"),1870                                },1871                            }1872                        )1873                    if pass_through_unknown_blocks:1874                        content.append(block)1875                elif block.get("type") == "tool_result":1876                    if missing := [1877                        k for k in ("content", "tool_use_id") if k not in block1878                    ]:1879                        msg = (1880                            f"Unrecognized content block at "1881                            f"messages[{i}].content[{j}] has 'type': "1882                            f"'tool_result', but is missing expected key(s) "1883                            f"{missing}. Full content block:\n\n{block}"1884                        )1885                        raise ValueError(msg)1886                    tool_message = ToolMessage(1887                        block["content"],1888                        tool_call_id=block["tool_use_id"],1889                        status="error" if block.get("is_error") else "success",1890                    )1891                    # Recurse to make sure tool message contents are OpenAI format.1892                    tool_messages.extend(1893                        convert_to_openai_messages(1894                            [tool_message], text_format=text_format1895                        )1896                    )1897                elif (block.get("type") == "json") or "json" in block:1898                    if "json" not in block:1899                        msg = (1900                            f"Unrecognized content block at "1901                            f"messages[{i}].content[{j}] has 'type': 'json' "1902                            f"but does not have a 'json' key. Full "1903                            f"content block:\n\n{block}"1904                        )1905                        raise ValueError(msg)1906                    content.append(1907                        {1908                            "type": "text",1909                            "text": json.dumps(block["json"]),1910                        }1911                    )1912                elif (block.get("type") == "guard_content") or "guard_content" in block:1913                    if (1914                        "guard_content" not in block1915                        or "text" not in block["guard_content"]1916                    ):1917                        msg = (1918                            f"Unrecognized content block at "1919                            f"messages[{i}].content[{j}] has 'type': "1920                            f"'guard_content' but does not have a "1921                            f"messages[{i}].content[{j}]['guard_content']['text'] "1922                            f"key. Full content block:\n\n{block}"1923                        )1924                        raise ValueError(msg)1925                    text = block["guard_content"]["text"]1926                    if isinstance(text, dict):1927                        text = text["text"]1928                    content.append({"type": "text", "text": text})1929                # VertexAI format1930                elif block.get("type") == "media":1931                    if missing := [k for k in ("mime_type", "data") if k not in block]:1932                        err = (1933                            f"Unrecognized content block at "1934                            f"messages[{i}].content[{j}] has 'type': "1935                            f"'media' but does not have key(s) {missing}. Full "1936                            f"content block:\n\n{block}"1937                        )1938                        raise ValueError(err)1939                    if "image" not in block["mime_type"]:1940                        err = (1941                            f"OpenAI messages can only support text and image data."1942                            f" Received content block with media of type:"1943                            f" {block['mime_type']}"1944                        )1945                        raise ValueError(err)1946                    b64_image = _bytes_to_b64_str(block["data"])1947                    content.append(1948                        {1949                            "type": "image_url",1950                            "image_url": {1951                                "url": (f"data:{block['mime_type']};base64,{b64_image}")1952                            },1953                        }1954                    )1955                elif (1956                    block.get("type") in {"thinking", "reasoning"}1957                    or pass_through_unknown_blocks1958                ):1959                    content.append(block)1960                else:1961                    err = (1962                        f"Unrecognized content block at "1963                        f"messages[{i}].content[{j}] does not match OpenAI, "1964                        f"Anthropic, Bedrock Converse, or VertexAI format. Full "1965                        f"content block:\n\n{block}"1966                    )1967                    raise ValueError(err)1968            if text_format == "string" and not any(1969                block["type"] != "text" for block in content1970            ):1971                content = "\n".join(block["text"] for block in content)1972        oai_msg["content"] = content1973        if message.content and not oai_msg["content"] and tool_messages:1974            oai_messages.extend(tool_messages)1975        else:1976            oai_messages.extend([oai_msg, *tool_messages])19771978    if is_single:1979        return oai_messages[0]1980    return oai_messages198119821983def _first_max_tokens(1984    messages: Sequence[BaseMessage],1985    *,1986    max_tokens: int,1987    token_counter: Callable[[list[BaseMessage]], int],1988    text_splitter: Callable[[str], list[str]],1989    partial_strategy: Literal["first", "last"] | None = None,1990    end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1991) -> list[BaseMessage]:1992    messages = list(messages)1993    if not messages:1994        return messages19951996    # Check if all messages already fit within token limit1997    if token_counter(messages) <= max_tokens:1998        # When all messages fit, only apply end_on filtering if needed1999        if end_on:2000            for _ in range(len(messages)):

Code quality findings 2

Ensure functions have docstrings for documentation
info maintainability missing-docstring
def wrapped(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def list_token_counter(messages: Sequence[BaseMessage]) -> int:

Get this view in your editor

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

Generated by searchcode.com · powered by code_get_file + code_get_findings