Ensure functions have docstrings for documentation
def wrapped(
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 < 10 & 10 > 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)):
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