Overuse may indicate design issues; consider polymorphism
if isinstance(v, dict) and "type" in v:
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)285286287def get_buffer_string(288 messages: Sequence[BaseMessage],289 human_prefix: str = "Human",290 ai_prefix: str = "AI",291 *,292 system_prefix: str = "System",293 function_prefix: str = "Function",294 tool_prefix: str = "Tool",295 message_separator: str = "\n",296 format: Literal["prefix", "xml"] = "prefix", # noqa: A002297) -> str:298 r"""Convert a sequence of messages to strings and concatenate them into one string.299300 Args:301 messages: Messages to be converted to strings.302 human_prefix: The prefix to prepend to contents of `HumanMessage`s.303 ai_prefix: The prefix to prepend to contents of `AIMessage`.304 system_prefix: The prefix to prepend to contents of `SystemMessage`s.305 function_prefix: The prefix to prepend to contents of `FunctionMessage`s.306 tool_prefix: The prefix to prepend to contents of `ToolMessage`s.307 message_separator: The separator to use between messages.308 format: The output format. `'prefix'` uses `Role: content` format (default).309 For multimodal messages, only string content and `text` blocks are310 included; non-text blocks such as images, audio, and video are omitted.311312 `'xml'` uses XML-style `<message type='role'>` format with proper313 character escaping, which is useful when message content may314 contain role-like prefixes that could cause ambiguity.315316 Use `'xml'` when you need a structured representation of supported317 multimodal content blocks.318319 Returns:320 A single string concatenation of all input messages.321322 Raises:323 ValueError: If an unsupported message type is encountered.324325 !!! warning326327 If a message is an `AIMessage` and contains both tool calls under `tool_calls`328 and a function call under `additional_kwargs["function_call"]`, only the tool329 calls will be appended to the string representation.330331 !!! note "XML format"332333 When using `format='xml'`:334335 - All messages use uniform `<message type="role">content</message>` format.336 - The `type` attribute uses `human_prefix` (lowercased) for `HumanMessage`,337 `ai_prefix` (lowercased) for `AIMessage`, `system_prefix` (lowercased)338 for `SystemMessage`, `function_prefix` (lowercased) for `FunctionMessage`,339 `tool_prefix` (lowercased) for `ToolMessage`, and the original role340 (unchanged) for `ChatMessage`.341 - Message content is escaped using `xml.sax.saxutils.escape()`.342 - Attribute values are escaped using `xml.sax.saxutils.quoteattr()`.343 - AI messages with tool calls use nested structure with `<content>` and344 `<tool_call>` elements.345 - For multi-modal content (list of content blocks), supported block types346 are: `text`, `reasoning`, `image` (URL/file_id only), `image_url`347 (OpenAI-style, URL only), `audio` (URL/file_id only), `video` (URL/file_id348 only), `text-plain`, `server_tool_call`, and `server_tool_result`.349 - Content blocks with base64-encoded data are skipped (including blocks350 with `base64` field or `data:` URLs).351 - Unknown block types are skipped.352 - Plain text document content (`text-plain`), server tool call arguments,353 and server tool result outputs are truncated to 500 characters.354355 Example:356 Default prefix format:357358 ```python359 from langchain_core.messages import AIMessage, HumanMessage, get_buffer_string360361 messages = [362 HumanMessage(content="Hi, how are you?"),363 AIMessage(content="Good, how are you?"),364 ]365 get_buffer_string(messages)366 # -> "Human: Hi, how are you?\nAI: Good, how are you?"367 ```368369 XML format (useful when content contains role-like prefixes):370371 ```python372 messages = [373 HumanMessage(content="Example: Human: some text"),374 AIMessage(content="I see the example."),375 ]376 get_buffer_string(messages, format="xml")377 # -> '<message type="human">Example: Human: some text</message>\\n'378 # -> '<message type="ai">I see the example.</message>'379 ```380381 XML format with special characters (automatically escaped):382383 ```python384 messages = [385 HumanMessage(content="Is 5 < 10 & 10 > 5?"),386 ]387 get_buffer_string(messages, format="xml")388 # -> '<message type="human">Is 5 < 10 & 10 > 5?</message>'389 ```390391 XML format with tool calls:392393 ```python394 messages = [395 AIMessage(396 content="I'll search for that.",397 tool_calls=[398 {"id": "call_123", "name": "search", "args": {"query": "weather"}}399 ],400 ),401 ]402 get_buffer_string(messages, format="xml")403 # -> '<message type="ai">\\n'404 # -> ' <content>I\\'ll search for that.</content>\\n'405 # -> ' <tool_call id="call_123" name="search">'406 # -> '{"query": "weather"}</tool_call>\\n'407 # -> '</message>'408 ```409 """410 if format not in {"prefix", "xml"}:411 msg = (412 f"Unrecognized format={format!r}. Supported formats are 'prefix' and 'xml'."413 )414 raise ValueError(msg)415416 string_messages = []417 for m in messages:418 if isinstance(m, HumanMessage):419 role = human_prefix420 elif isinstance(m, AIMessage):421 role = ai_prefix422 elif isinstance(m, SystemMessage):423 role = system_prefix424 elif isinstance(m, FunctionMessage):425 role = function_prefix426 elif isinstance(m, ToolMessage):427 role = tool_prefix428 elif isinstance(m, ChatMessage):429 role = m.role430 else:431 msg = f"Got unsupported message type: {m}"432 raise ValueError(msg) # noqa: TRY004433434 if format == "xml":435 msg_type = _get_message_type_str(436 m, human_prefix, ai_prefix, system_prefix, function_prefix, tool_prefix437 )438439 # Format content blocks440 if isinstance(m.content, str):441 content_parts = [escape(m.content)] if m.content else []442 else:443 # List of content blocks444 content_parts = []445 for block in m.content:446 if isinstance(block, str):447 if block:448 content_parts.append(escape(block))449 else:450 formatted = _format_content_block_xml(block)451 if formatted:452 content_parts.append(formatted)453454 # Check if this is an AIMessage with tool calls455 has_tool_calls = isinstance(m, AIMessage) and m.tool_calls456 has_function_call = (457 isinstance(m, AIMessage)458 and not m.tool_calls459 and "function_call" in m.additional_kwargs460 )461462 if has_tool_calls or has_function_call:463 # Use nested structure for AI messages with tool calls464 # Type narrowing: at this point m is AIMessage (verified above)465 ai_msg = cast("AIMessage", m)466 parts = [f"<message type={quoteattr(msg_type)}>"]467 if content_parts:468 parts.append(f" <content>{' '.join(content_parts)}</content>")469470 if has_tool_calls:471 for tc in ai_msg.tool_calls:472 tc_id = quoteattr(str(tc.get("id") or ""))473 tc_name = quoteattr(str(tc.get("name") or ""))474 tc_args = escape(475 json.dumps(tc.get("args", {}), ensure_ascii=False)476 )477 parts.append(478 f" <tool_call id={tc_id} name={tc_name}>"479 f"{tc_args}</tool_call>"480 )481 elif has_function_call:482 fc = ai_msg.additional_kwargs["function_call"]483 fc_name = quoteattr(str(fc.get("name") or ""))484 fc_args = escape(str(fc.get("arguments") or "{}"))485 parts.append(486 f" <function_call name={fc_name}>{fc_args}</function_call>"487 )488489 parts.append("</message>")490 message = "\n".join(parts)491 else:492 # Simple structure for messages without tool calls493 joined_content = " ".join(content_parts)494 message = (495 f"<message type={quoteattr(msg_type)}>{joined_content}</message>"496 )497 else: # format == "prefix"498 content = m.text499 message = f"{role}: {content}"500 tool_info = ""501 if isinstance(m, AIMessage):502 if m.tool_calls:503 tool_info = str(m.tool_calls)504 elif "function_call" in m.additional_kwargs:505 # Legacy behavior assumes only one function call per message506 tool_info = str(m.additional_kwargs["function_call"])507 if tool_info:508 message += tool_info # Preserve original behavior509510 string_messages.append(message)511512 return message_separator.join(string_messages)513514515def _message_from_dict(message: dict[str, Any]) -> BaseMessage:516 type_ = message["type"]517 if type_ == "human":518 return HumanMessage(**message["data"])519 if type_ == "ai":520 return AIMessage(**message["data"])521 if type_ == "system":522 return SystemMessage(**message["data"])523 if type_ == "chat":524 return ChatMessage(**message["data"])525 if type_ == "function":526 return FunctionMessage(**message["data"])527 if type_ == "tool":528 return ToolMessage(**message["data"])529 if type_ == "remove":530 return RemoveMessage(**message["data"])531 if type_ == "AIMessageChunk":532 return AIMessageChunk(**message["data"])533 if type_ == "HumanMessageChunk":534 return HumanMessageChunk(**message["data"])535 if type_ == "FunctionMessageChunk":536 return FunctionMessageChunk(**message["data"])537 if type_ == "ToolMessageChunk":538 return ToolMessageChunk(**message["data"])539 if type_ == "SystemMessageChunk":540 return SystemMessageChunk(**message["data"])541 if type_ == "ChatMessageChunk":542 return ChatMessageChunk(**message["data"])543 msg = f"Got unexpected message type: {type_}"544 raise ValueError(msg)545546547def messages_from_dict(messages: Sequence[dict[str, Any]]) -> list[BaseMessage]:548 """Convert a sequence of messages from dicts to `Message` objects.549550 Args:551 messages: Sequence of messages (as dicts) to convert.552553 Returns:554 list of messages (BaseMessages).555556 """557 return [_message_from_dict(m) for m in messages]558559560def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:561 """Convert a message chunk to a `Message`.562563 Args:564 chunk: Message chunk to convert.565566 Returns:567 Message.568 """569 if not isinstance(chunk, BaseMessageChunk):570 return chunk571 # chunk classes always have the equivalent non-chunk class as their first parent572 ignore_keys = ["type"]573 if isinstance(chunk, AIMessageChunk):574 ignore_keys.extend(["tool_call_chunks", "chunk_position"])575 return cast(576 "BaseMessage",577 chunk.__class__.__mro__[1](578 **{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}579 ),580 )581582583MessageLikeRepresentation = (584 BaseMessage585 | list[str]586 | tuple[str, str | list[str | dict[str, Any]]]587 | str588 | dict[str, Any]589)590"""A type representing the various ways a message can be represented."""591592593def _create_message_from_message_type(594 message_type: str,595 content: str | list[str | dict[str, Any]],596 name: str | None = None,597 tool_call_id: str | None = None,598 tool_calls: list[dict[str, Any]] | None = None,599 id: str | None = None,600 **additional_kwargs: Any,601) -> BaseMessage:602 """Create a message from a `Message` type and content string.603604 Args:605 message_type: the type of the message (e.g., `'human'`, `'ai'`, etc.).606 content: the content string.607 name: the name of the message.608 tool_call_id: the tool call id.609 tool_calls: the tool calls.610 id: the id of the message.611 additional_kwargs: additional keyword arguments.612613 Returns:614 a message of the appropriate type.615616 Raises:617 ValueError: if the message type is not one of `'human'`, `'user'`, `'ai'`,618 `'assistant'`, `'function'`, `'tool'`, `'system'`, or619 `'developer'`.620 """621 kwargs: dict[str, Any] = {}622 if name is not None:623 kwargs["name"] = name624 if tool_call_id is not None:625 kwargs["tool_call_id"] = tool_call_id626 if additional_kwargs:627 if response_metadata := additional_kwargs.pop("response_metadata", None):628 kwargs["response_metadata"] = response_metadata629 kwargs["additional_kwargs"] = additional_kwargs630 additional_kwargs.update(additional_kwargs.pop("additional_kwargs", {}))631 if id is not None:632 kwargs["id"] = id633 if tool_calls is not None:634 kwargs["tool_calls"] = []635 for tool_call in tool_calls:636 # Convert OpenAI-format tool call to LangChain format.637 if "function" in tool_call:638 args = tool_call["function"]["arguments"]639 if isinstance(args, str):640 args = json.loads(args, strict=False)641 kwargs["tool_calls"].append(642 {643 "name": tool_call["function"]["name"],644 "args": args,645 "id": tool_call["id"],646 "type": "tool_call",647 }648 )649 else:650 kwargs["tool_calls"].append(tool_call)651 if message_type in {"human", "user"}:652 if example := kwargs.get("additional_kwargs", {}).pop("example", False):653 kwargs["example"] = example654 message: BaseMessage = HumanMessage(content=content, **kwargs)655 elif message_type in {"ai", "assistant"}:656 if example := kwargs.get("additional_kwargs", {}).pop("example", False):657 kwargs["example"] = example658 message = AIMessage(content=content, **kwargs)659 elif message_type in {"system", "developer"}:660 if message_type == "developer":661 kwargs["additional_kwargs"] = kwargs.get("additional_kwargs") or {}662 kwargs["additional_kwargs"]["__openai_role__"] = "developer"663 message = SystemMessage(content=content, **kwargs)664 elif message_type == "function":665 message = FunctionMessage(content=content, **kwargs)666 elif message_type == "tool":667 artifact = kwargs.get("additional_kwargs", {}).pop("artifact", None)668 status = kwargs.get("additional_kwargs", {}).pop("status", None)669 if status is not None:670 kwargs["status"] = status671 message = ToolMessage(content=content, artifact=artifact, **kwargs)672 elif message_type == "remove":673 message = RemoveMessage(**kwargs)674 else:675 msg = (676 f"Unexpected message type: '{message_type}'. Use one of 'human',"677 f" 'user', 'ai', 'assistant', 'function', 'tool', 'system', or 'developer'."678 )679 msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)680 raise ValueError(msg)681 return message682683684# Map of class names emitted in the `Serializable` constructor-envelope685# (`{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"],686# "kwargs": {...}}`) to the message-type strings687# `_create_message_from_message_type` accepts. Read by688# `_convert_to_message`'s dict branch when unpacking that wire shape.689# Kept as a hardcoded allowlist of strings rather than a class registry690# lookup so dispatch never resolves to a class chosen by the caller.691_LC_CONSTRUCTOR_NAME_TO_TYPE: dict[str, str] = {692 "HumanMessage": "human",693 "HumanMessageChunk": "human",694 "AIMessage": "ai",695 "AIMessageChunk": "ai",696 "SystemMessage": "system",697 "SystemMessageChunk": "system",698 "FunctionMessage": "function",699 "FunctionMessageChunk": "function",700 "ToolMessage": "tool",701 "ToolMessageChunk": "tool",702 "RemoveMessage": "remove",703}704705706def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:707 """Instantiate a `Message` from a variety of message formats.708709 The message format can be one of the following:710711 - `BaseMessagePromptTemplate`712 - `BaseMessage`713 - 2-tuple of (role string, template); e.g., (`'human'`, `'{user_input}'`)714 - dict: a message dict with role and content keys715 - dict: the `Serializable` constructor-envelope wire shape716 `{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"],717 "kwargs": {...}}` — unpacked structurally and routed through the718 standard dict-with-type dispatch.719 - string: shorthand for (`'human'`, template); e.g., `'{user_input}'`720721 Args:722 message: a representation of a message in one of the supported formats.723724 Returns:725 An instance of a message or a message template.726727 Raises:728 NotImplementedError: if the message type is not supported.729 ValueError: if the message dict does not contain the required keys.730731 """732 if isinstance(message, BaseMessage):733 message_ = message734 elif isinstance(message, Sequence):735 if isinstance(message, str):736 message_ = _create_message_from_message_type("human", message)737 else:738 try:739 message_type_str, template = message740 except ValueError as e:741 msg = "Message as a sequence must be (role string, template)"742 raise NotImplementedError(msg) from e743 message_ = _create_message_from_message_type(message_type_str, template)744 elif isinstance(message, dict):745 # `Serializable` constructor-envelope wire shape. Detect structurally, map746 # the class name to a known message-type string via a hardcoded747 # allowlist, and recurse with the canonical748 # `{"type": ..., **kwargs}` shape — no `load()`, no dynamic749 # class instantiation.750 if (751 message.get("lc") == 1752 and message.get("type") == "constructor"753 and isinstance(message.get("id"), list)754 and message["id"]755 and isinstance(message.get("kwargs"), dict)756 ):757 mapped = _LC_CONSTRUCTOR_NAME_TO_TYPE.get(message["id"][-1])758 if mapped is not None:759 return _convert_to_message({"type": mapped, **message["kwargs"]})760761 msg_kwargs = message.copy()762 try:763 try:764 msg_type = msg_kwargs.pop("role")765 except KeyError:766 msg_type = msg_kwargs.pop("type")767 # None msg content is not allowed768 msg_content = msg_kwargs.pop("content") or ""769 except KeyError as e:770 msg = f"Message dict must contain 'role' and 'content' keys, got {message}"771 msg = create_message(772 message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE773 )774 raise ValueError(msg) from e775 message_ = _create_message_from_message_type(776 msg_type, msg_content, **msg_kwargs777 )778 else:779 msg = f"Unsupported message type: {type(message)}" # type: ignore[unreachable]780 msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)781 raise NotImplementedError(msg)782783 return message_784785786def convert_to_messages(787 messages: Iterable[MessageLikeRepresentation] | PromptValue,788) -> list[BaseMessage]:789 """Convert a sequence of messages to a list of messages.790791 Args:792 messages: Sequence of messages to convert.793794 Returns:795 list of messages (BaseMessages).796797 """798 # Import here to avoid circular imports799 from langchain_core.prompt_values import PromptValue # noqa: PLC0415800801 if isinstance(messages, PromptValue):802 return messages.to_messages()803 return [_convert_to_message(m) for m in messages]804805806_P = ParamSpec("_P")807_R_co = TypeVar("_R_co", covariant=True)808809810class _RunnableSupportCallable(Protocol[_P, _R_co]):811 @overload812 def __call__(813 self,814 messages: None = None,815 *args: _P.args,816 **kwargs: _P.kwargs,817 ) -> Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...818819 @overload820 def __call__(821 self,822 messages: Sequence[MessageLikeRepresentation] | PromptValue,823 *args: _P.args,824 **kwargs: _P.kwargs,825 ) -> _R_co: ...826827 def __call__(828 self,829 messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,830 *args: _P.args,831 **kwargs: _P.kwargs,832 ) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...833834835def _runnable_support(836 func: Callable[837 Concatenate[Sequence[MessageLikeRepresentation] | PromptValue, _P], _R_co838 ],839) -> _RunnableSupportCallable[_P, _R_co]:840 @wraps(func)841 def wrapped(842 messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,843 *args: _P.args,844 **kwargs: _P.kwargs,845 ) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]:846 # Import locally to prevent circular import.847 from langchain_core.runnables.base import RunnableLambda # noqa: PLC0415848849 if messages is not None:850 return func(messages, *args, **kwargs)851 return RunnableLambda(partial(func, **kwargs), name=func.__name__)852853 return cast("_RunnableSupportCallable[_P, _R_co]", wrapped)854855856@_runnable_support857def filter_messages(858 messages: Iterable[MessageLikeRepresentation] | PromptValue,859 *,860 include_names: Sequence[str] | None = None,861 exclude_names: Sequence[str] | None = None,862 include_types: Sequence[str | type[BaseMessage]] | None = None,863 exclude_types: Sequence[str | type[BaseMessage]] | None = None,864 include_ids: Sequence[str] | None = None,865 exclude_ids: Sequence[str] | None = None,866 exclude_tool_calls: Sequence[str] | bool | None = None,867) -> list[BaseMessage]:868 """Filter messages based on `name`, `type` or `id`.869870 Args:871 messages: Sequence Message-like objects to filter.872 include_names: Message names to include.873 exclude_names: Messages names to exclude.874 include_types: Message types to include. Can be specified as string names875 (e.g. `'system'`, `'human'`, `'ai'`, ...) or as `BaseMessage`876 classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`, ...).877878 exclude_types: Message types to exclude. Can be specified as string names879 (e.g. `'system'`, `'human'`, `'ai'`, ...) or as `BaseMessage`880 classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`, ...).881882 include_ids: Message IDs to include.883 exclude_ids: Message IDs to exclude.884 exclude_tool_calls: Tool call IDs to exclude.885 Can be one of the following:886 - `True`: All `AIMessage` objects with tool calls and all `ToolMessage`887 objects will be excluded.888 - a sequence of tool call IDs to exclude:889 - `ToolMessage` objects with the corresponding tool call ID will be890 excluded.891 - The `tool_calls` in the AIMessage will be updated to exclude892 matching tool calls. If all `tool_calls` are filtered from an893 AIMessage, the whole message is excluded.894895 Returns:896 A list of Messages that meets at least one of the `incl_*` conditions and none897 of the `excl_*` conditions. If not `incl_*` conditions are specified then898 anything that is not explicitly excluded will be included.899900 Raises:901 ValueError: If two incompatible arguments are provided.902903 Example:904 ```python905 from langchain_core.messages import (906 filter_messages,907 AIMessage,908 HumanMessage,909 SystemMessage,910 )911912 messages = [913 SystemMessage("you're a good assistant."),914 HumanMessage("what's your name", id="foo", name="example_user"),915 AIMessage("steve-o", id="bar", name="example_assistant"),916 HumanMessage(917 "what's your favorite color",918 id="baz",919 ),920 AIMessage(921 "silicon blue",922 id="blah",923 ),924 ]925926 filter_messages(927 messages,928 include_names=("example_user", "example_assistant"),929 include_types=("system",),930 exclude_ids=("bar",),931 )932 ```933934 ```python935 [936 SystemMessage("you're a good assistant."),937 HumanMessage("what's your name", id="foo", name="example_user"),938 ]939 ```940 """941 messages = convert_to_messages(messages)942 filtered: list[BaseMessage] = []943 for msg in messages:944 if (945 (exclude_names and msg.name in exclude_names)946 or (exclude_types and _is_message_type(msg, exclude_types))947 or (exclude_ids and msg.id in exclude_ids)948 ):949 continue950951 if exclude_tool_calls is True and (952 (isinstance(msg, AIMessage) and msg.tool_calls)953 or isinstance(msg, ToolMessage)954 ):955 continue956957 new_msg = msg958 if isinstance(exclude_tool_calls, (list, tuple, set)):959 if isinstance(msg, AIMessage) and msg.tool_calls:960 tool_calls = [961 tool_call962 for tool_call in msg.tool_calls963 if tool_call["id"] not in exclude_tool_calls964 ]965 if not tool_calls:966 continue967968 content = msg.content969 # handle Anthropic content blocks970 if isinstance(msg.content, list):971 content = [972 content_block973 for content_block in msg.content974 if (975 not isinstance(content_block, dict)976 or content_block.get("type") != "tool_use"977 or content_block.get("id") not in exclude_tool_calls978 )979 ]980981 new_msg = msg.model_copy(982 update={"tool_calls": tool_calls, "content": content}983 )984 elif (985 isinstance(msg, ToolMessage) and msg.tool_call_id in exclude_tool_calls986 ):987 continue988989 # default to inclusion when no inclusion criteria given.990 if (991 not (include_types or include_ids or include_names)992 or (include_names and new_msg.name in include_names)993 or (include_types and _is_message_type(new_msg, include_types))994 or (include_ids and new_msg.id in include_ids)995 ):996 filtered.append(new_msg)997998 return filtered99910001001@_runnable_support1002def merge_message_runs(1003 messages: Iterable[MessageLikeRepresentation] | PromptValue,1004 *,1005 chunk_separator: str = "\n",1006) -> list[BaseMessage]:1007 r"""Merge consecutive Messages of the same type.10081009 !!! note1010 `ToolMessage` objects are not merged, as each has a distinct tool call id that1011 can't be merged.10121013 Args:1014 messages: Sequence Message-like objects to merge.1015 chunk_separator: Specify the string to be inserted between message chunks.10161017 Returns:1018 list of BaseMessages with consecutive runs of message types merged into single1019 messages. By default, if two messages being merged both have string contents,1020 the merged content is a concatenation of the two strings with a new-line1021 separator.1022 The separator inserted between message chunks can be controlled by specifying1023 any string with `chunk_separator`. If at least one of the messages has a list1024 of content blocks, the merged content is a list of content blocks.10251026 Example:1027 ```python1028 from langchain_core.messages import (1029 merge_message_runs,1030 AIMessage,1031 HumanMessage,1032 SystemMessage,1033 ToolCall,1034 )10351036 messages = [1037 SystemMessage("you're a good assistant."),1038 HumanMessage(1039 "what's your favorite color",1040 id="foo",1041 ),1042 HumanMessage(1043 "wait your favorite food",1044 id="bar",1045 ),1046 AIMessage(1047 "my favorite colo",1048 tool_calls=[1049 ToolCall(1050 name="blah_tool", args={"x": 2}, id="123", type="tool_call"1051 )1052 ],1053 id="baz",1054 ),1055 AIMessage(1056 [{"type": "text", "text": "my favorite dish is lasagna"}],1057 tool_calls=[1058 ToolCall(1059 name="blah_tool",1060 args={"x": -10},1061 id="456",1062 type="tool_call",1063 )1064 ],1065 id="blur",1066 ),1067 ]10681069 merge_message_runs(messages)1070 ```10711072 ```python1073 [1074 SystemMessage("you're a good assistant."),1075 HumanMessage(1076 "what's your favorite color\\n"1077 "wait your favorite food", id="foo",1078 ),1079 AIMessage(1080 [1081 "my favorite colo",1082 {"type": "text", "text": "my favorite dish is lasagna"}1083 ],1084 tool_calls=[1085 ToolCall({1086 "name": "blah_tool",1087 "args": {"x": 2},1088 "id": "123",1089 "type": "tool_call"1090 }),1091 ToolCall({1092 "name": "blah_tool",1093 "args": {"x": -10},1094 "id": "456",1095 "type": "tool_call"1096 })1097 ]1098 id="baz"1099 ),1100 ]11011102 ```1103 """1104 if not messages:1105 return []1106 messages = convert_to_messages(messages)1107 merged: list[BaseMessage] = []1108 for msg in messages:1109 last = merged.pop() if merged else None1110 if not last:1111 merged.append(msg)1112 elif isinstance(msg, ToolMessage) or not isinstance(msg, last.__class__):1113 merged.extend([last, msg])1114 else:1115 last_chunk = _msg_to_chunk(last)1116 curr_chunk = _msg_to_chunk(msg)1117 if curr_chunk.response_metadata:1118 curr_chunk.response_metadata.clear()1119 if (1120 isinstance(last_chunk.content, str)1121 and isinstance(curr_chunk.content, str)1122 and last_chunk.content1123 and curr_chunk.content1124 ):1125 last_chunk.content += chunk_separator1126 merged.append(_chunk_to_msg(last_chunk + curr_chunk))1127 return merged112811291130# TODO: Update so validation errors (for token_counter, for example) are raised on1131# init not at runtime.1132@_runnable_support1133def trim_messages(1134 messages: Iterable[MessageLikeRepresentation] | PromptValue,1135 *,1136 max_tokens: int,1137 token_counter: Callable[[list[BaseMessage]], int]1138 | Callable[[BaseMessage], int]1139 | BaseLanguageModel[Any]1140 | Literal["approximate"],1141 strategy: Literal["first", "last"] = "last",1142 allow_partial: bool = False,1143 end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1144 start_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1145 include_system: bool = False,1146 text_splitter: Callable[[str], list[str]] | TextSplitter | None = None,1147) -> list[BaseMessage]:1148 r"""Trim messages to be below a token count.11491150 `trim_messages` can be used to reduce the size of a chat history to a specified1151 token or message count.11521153 In either case, if passing the trimmed chat history back into a chat model1154 directly, the resulting chat history should usually satisfy the following1155 properties:11561157 1. The resulting chat history should be valid. Most chat models expect that chat1158 history starts with either (1) a `HumanMessage` or (2) a `SystemMessage`1159 followed by a `HumanMessage`. To achieve this, set `start_on='human'`.1160 In addition, generally a `ToolMessage` can only appear after an `AIMessage`1161 that involved a tool call.1162 2. It includes recent messages and drops old messages in the chat history.1163 To achieve this set the `strategy='last'`.1164 3. Usually, the new chat history should include the `SystemMessage` if it1165 was present in the original chat history since the `SystemMessage` includes1166 special instructions to the chat model. The `SystemMessage` is almost always1167 the first message in the history if present. To achieve this set the1168 `include_system=True`.11691170 !!! note1171 The examples below show how to configure `trim_messages` to achieve a behavior1172 consistent with the above properties.11731174 Args:1175 messages: Sequence of Message-like objects to trim.1176 max_tokens: Max token count of trimmed messages.1177 token_counter: Function or llm for counting tokens in a `BaseMessage` or a1178 list of `BaseMessage`.11791180 If a `BaseLanguageModel` is passed in then1181 `BaseLanguageModel.get_num_tokens_from_messages()` will be used. Set to1182 `len` to count the number of **messages** in the chat history.11831184 You can also use string shortcuts for convenience:11851186 - `'approximate'`: Uses `count_tokens_approximately` for fast, approximate1187 token counts.11881189 !!! note11901191 `count_tokens_approximately` (or the shortcut `'approximate'`) is1192 recommended for using `trim_messages` on the hot path, where exact token1193 counting is not necessary.11941195 strategy: Strategy for trimming.11961197 - `'first'`: Keep the first `<= n_count` tokens of the messages.1198 - `'last'`: Keep the last `<= n_count` tokens of the messages.1199 allow_partial: Whether to split a message if only part of the message can be1200 included.12011202 If `strategy='last'` then the last partial contents of a message are1203 included. If `strategy='first'` then the first partial contents of a1204 message are included.1205 end_on: The message type to end on.12061207 If specified then every message after the last occurrence of this type is1208 ignored. If `strategy='last'` then this is done before we attempt to get the1209 last `max_tokens`. If `strategy='first'` then this is done after we get the1210 first `max_tokens`. Can be specified as string names (e.g. `'system'`,1211 `'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g. `SystemMessage`,1212 `HumanMessage`, `AIMessage`, ...). Can be a single type or a list of types.12131214 start_on: The message type to start on.12151216 Should only be specified if `strategy='last'`. If specified then every1217 message before the first occurrence of this type is ignored. This is done1218 after we trim the initial messages to the last `max_tokens`. Does not apply1219 to a `SystemMessage` at index 0 if `include_system=True`. Can be specified1220 as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or as1221 `BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`,1222 ...). Can be a single type or a list of types.12231224 include_system: Whether to keep the `SystemMessage` if there is one at index1225 `0`.12261227 Should only be specified if `strategy="last"`.1228 text_splitter: Function or `langchain_text_splitters.TextSplitter` for1229 splitting the string contents of a message.12301231 Only used if `allow_partial=True`. If `strategy='last'` then the last split1232 tokens from a partial message will be included. if `strategy='first'` then1233 the first split tokens from a partial message will be included. Token1234 splitter assumes that separators are kept, so that split contents can be1235 directly concatenated to recreate the original text. Defaults to splitting1236 on newlines.12371238 Returns:1239 List of trimmed `BaseMessage`.12401241 Raises:1242 ValueError: if two incompatible arguments are specified or an unrecognized1243 `strategy` is specified.12441245 Example:1246 Trim chat history based on token count, keeping the `SystemMessage` if1247 present, and ensuring that the chat history starts with a `HumanMessage` (or a1248 `SystemMessage` followed by a `HumanMessage`).12491250 ```python1251 from langchain_core.messages import (1252 AIMessage,1253 HumanMessage,1254 BaseMessage,1255 SystemMessage,1256 trim_messages,1257 )12581259 messages = [1260 SystemMessage("you're a good assistant, you always respond with a joke."),1261 HumanMessage("i wonder why it's called langchain"),1262 AIMessage(1263 'Well, I guess they thought "WordRope" and "SentenceString" just '1264 "didn't have the same ring to it!"1265 ),1266 HumanMessage("and who is harrison chasing anyways"),1267 AIMessage(1268 "Hmmm let me think.\n\nWhy, he's probably chasing after the last "1269 "cup of coffee in the office!"1270 ),1271 HumanMessage("what do you call a speechless parrot"),1272 ]127312741275 trim_messages(1276 messages,1277 max_tokens=45,1278 strategy="last",1279 token_counter=ChatOpenAI(model="openai:gpt-5.5"),1280 # Most chat models expect that chat history starts with either:1281 # (1) a HumanMessage or1282 # (2) a SystemMessage followed by a HumanMessage1283 start_on="human",1284 # Usually, we want to keep the SystemMessage1285 # if it's present in the original history.1286 # The SystemMessage has special instructions for the model.1287 include_system=True,1288 allow_partial=False,1289 )1290 ```12911292 ```python1293 [1294 SystemMessage(1295 content="you're a good assistant, you always respond with a joke."1296 ),1297 HumanMessage(content="what do you call a speechless parrot"),1298 ]1299 ```13001301 Trim chat history using approximate token counting with `'approximate'`:13021303 ```python1304 trim_messages(1305 messages,1306 max_tokens=45,1307 strategy="last",1308 # Using the "approximate" shortcut for fast token counting1309 token_counter="approximate",1310 start_on="human",1311 include_system=True,1312 )13131314 # This is equivalent to using `count_tokens_approximately` directly1315 from langchain_core.messages.utils import count_tokens_approximately13161317 trim_messages(1318 messages,1319 max_tokens=45,1320 strategy="last",1321 token_counter=count_tokens_approximately,1322 start_on="human",1323 include_system=True,1324 )1325 ```13261327 Trim chat history based on the message count, keeping the `SystemMessage` if1328 present, and ensuring that the chat history starts with a HumanMessage (1329 or a `SystemMessage` followed by a `HumanMessage`).13301331 trim_messages(1332 messages,1333 # When `len` is passed in as the token counter function,1334 # max_tokens will count the number of messages in the chat history.1335 max_tokens=4,1336 strategy="last",1337 # Passing in `len` as a token counter function will1338 # count the number of messages in the chat history.1339 token_counter=len,1340 # Most chat models expect that chat history starts with either:1341 # (1) a HumanMessage or1342 # (2) a SystemMessage followed by a HumanMessage1343 start_on="human",1344 # Usually, we want to keep the SystemMessage1345 # if it's present in the original history.1346 # The SystemMessage has special instructions for the model.1347 include_system=True,1348 allow_partial=False,1349 )13501351 ```python1352 [1353 SystemMessage(1354 content="you're a good assistant, you always respond with a joke."1355 ),1356 HumanMessage(content="and who is harrison chasing anyways"),1357 AIMessage(1358 content="Hmmm let me think.\n\nWhy, he's probably chasing after "1359 "the last cup of coffee in the office!"1360 ),1361 HumanMessage(content="what do you call a speechless parrot"),1362 ]1363 ```1364 Trim chat history using a custom token counter function that counts the1365 number of tokens in each message.13661367 ```python1368 messages = [1369 SystemMessage("This is a 4 token text. The full message is 10 tokens."),1370 HumanMessage(1371 "This is a 4 token text. The full message is 10 tokens.", id="first"1372 ),1373 AIMessage(1374 [1375 {"type": "text", "text": "This is the FIRST 4 token block."},1376 {"type": "text", "text": "This is the SECOND 4 token block."},1377 ],1378 id="second",1379 ),1380 HumanMessage(1381 "This is a 4 token text. The full message is 10 tokens.", id="third"1382 ),1383 AIMessage(1384 "This is a 4 token text. The full message is 10 tokens.",1385 id="fourth",1386 ),1387 ]138813891390 def dummy_token_counter(messages: list[BaseMessage]) -> int:1391 # treat each message like it adds 3 default tokens at the beginning1392 # of the message and at the end of the message. 3 + 4 + 3 = 10 tokens1393 # per message.13941395 default_content_len = 41396 default_msg_prefix_len = 31397 default_msg_suffix_len = 313981399 count = 01400 for msg in messages:1401 if isinstance(msg.content, str):1402 count += (1403 default_msg_prefix_len1404 + default_content_len1405 + default_msg_suffix_len1406 )1407 if isinstance(msg.content, list):1408 count += (1409 default_msg_prefix_len1410 + len(msg.content) * default_content_len1411 + default_msg_suffix_len1412 )1413 return count1414 ```14151416 First 30 tokens, allowing partial messages:1417 ```python1418 trim_messages(1419 messages,1420 max_tokens=30,1421 token_counter=dummy_token_counter,1422 strategy="first",1423 allow_partial=True,1424 )1425 ```14261427 ```python1428 [1429 SystemMessage("This is a 4 token text. The full message is 10 tokens."),1430 HumanMessage(1431 "This is a 4 token text. The full message is 10 tokens.",1432 id="first",1433 ),1434 AIMessage(1435 [{"type": "text", "text": "This is the FIRST 4 token block."}],1436 id="second",1437 ),1438 ]1439 ```1440 """1441 # Validate arguments1442 if start_on and strategy == "first":1443 msg = "start_on parameter is only valid with strategy='last'"1444 raise ValueError(msg)1445 if include_system and strategy == "first":1446 msg = "include_system parameter is only valid with strategy='last'"1447 raise ValueError(msg)14481449 messages = convert_to_messages(messages)14501451 # Handle string shortcuts for token counter1452 if isinstance(token_counter, str):1453 if token_counter in _TOKEN_COUNTER_SHORTCUTS:1454 actual_token_counter = _TOKEN_COUNTER_SHORTCUTS[token_counter]1455 else:1456 available_shortcuts = ", ".join(1457 f"'{key}'" for key in _TOKEN_COUNTER_SHORTCUTS1458 )1459 msg = (1460 f"Invalid token_counter shortcut '{token_counter}'. "1461 f"Available shortcuts: {available_shortcuts}."1462 )1463 raise ValueError(msg)1464 else:1465 # Type narrowing: at this point token_counter is not a str1466 actual_token_counter = token_counter # type: ignore[assignment]14671468 if hasattr(actual_token_counter, "get_num_tokens_from_messages"):1469 list_token_counter = actual_token_counter.get_num_tokens_from_messages1470 elif callable(actual_token_counter):1471 if (1472 next(1473 iter(inspect.signature(actual_token_counter).parameters.values())1474 ).annotation1475 is BaseMessage1476 ):14771478 def list_token_counter(messages: Sequence[BaseMessage]) -> int:1479 return sum(actual_token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]14801481 else:1482 list_token_counter = actual_token_counter1483 else:1484 msg = ( # type: ignore[unreachable]1485 f"'token_counter' expected to be a model that implements "1486 f"'get_num_tokens_from_messages()' or a function. Received object of type "1487 f"{type(actual_token_counter)}."1488 )1489 raise ValueError(msg)14901491 text_splitter_fn: Callable[[str], list[str]]1492 if _HAS_LANGCHAIN_TEXT_SPLITTERS and isinstance(text_splitter, TextSplitter):1493 text_splitter_fn = text_splitter.split_text1494 elif text_splitter:1495 text_splitter_fn = cast("Callable[[str], list[str]]", text_splitter)1496 else:1497 text_splitter_fn = _default_text_splitter14981499 if strategy == "first":1500 return _first_max_tokens(1501 messages,1502 max_tokens=max_tokens,1503 token_counter=list_token_counter,1504 text_splitter=text_splitter_fn,1505 partial_strategy="first" if allow_partial else None,1506 end_on=end_on,1507 )1508 if strategy == "last":1509 return _last_max_tokens(1510 messages,1511 max_tokens=max_tokens,1512 token_counter=list_token_counter,1513 allow_partial=allow_partial,1514 include_system=include_system,1515 start_on=start_on,1516 end_on=end_on,1517 text_splitter=text_splitter_fn,1518 )1519 msg = f"Unrecognized {strategy=}. Supported strategies are 'last' and 'first'." # type: ignore[unreachable]1520 raise ValueError(msg)152115221523_SingleMessage = BaseMessage | str | dict[str, Any]1524_T = TypeVar("_T", bound=_SingleMessage)1525# A sequence of _SingleMessage that is NOT a bare str1526_MultipleMessages = Sequence[_T]152715281529@overload1530def convert_to_openai_messages(1531 messages: _SingleMessage,1532 *,1533 text_format: Literal["string", "block"] = "string",1534 include_id: bool = False,1535 pass_through_unknown_blocks: bool = True,1536) -> dict[str, Any]: ...153715381539@overload1540def convert_to_openai_messages(1541 messages: _MultipleMessages[Any],1542 *,1543 text_format: Literal["string", "block"] = "string",1544 include_id: bool = False,1545 pass_through_unknown_blocks: bool = True,1546) -> list[dict[str, Any]]: ...154715481549def convert_to_openai_messages(1550 messages: MessageLikeRepresentation | Sequence[MessageLikeRepresentation],1551 *,1552 text_format: Literal["string", "block"] = "string",1553 include_id: bool = False,1554 pass_through_unknown_blocks: bool = True,1555) -> dict[str, Any] | list[dict[str, Any]]:1556 """Convert LangChain messages into OpenAI message dicts.15571558 Args:1559 messages: Message-like object or iterable of objects whose contents are1560 in OpenAI, Anthropic, Bedrock Converse, or VertexAI formats.1561 text_format: How to format string or text block contents:1562 - `'string'`:1563 If a message has a string content, this is left as a string. If1564 a message has content blocks that are all of type `'text'`, these1565 are joined with a newline to make a single string. If a message has1566 content blocks and at least one isn't of type `'text'`, then1567 all blocks are left as dicts.1568 - `'block'`:1569 If a message has a string content, this is turned into a list1570 with a single content block of type `'text'`. If a message has1571 content blocks these are left as is.1572 include_id: Whether to include message IDs in the openai messages, if they1573 are present in the source messages.1574 pass_through_unknown_blocks: Whether to include content blocks with unknown1575 formats in the output. If `False`, an error is raised if an unknown1576 content block is encountered.15771578 Raises:1579 ValueError: if an unrecognized `text_format` is specified, or if a message1580 content block is missing expected keys.15811582 Returns:1583 The return type depends on the input type:15841585 - dict:1586 If a single message-like object is passed in, a single OpenAI message1587 dict is returned.1588 - list[dict]:1589 If a sequence of message-like objects are passed in, a list of OpenAI1590 message dicts is returned.15911592 Example:1593 ```python1594 from langchain_core.messages import (1595 convert_to_openai_messages,1596 AIMessage,1597 SystemMessage,1598 ToolMessage,1599 )16001601 messages = [1602 SystemMessage([{"type": "text", "text": "foo"}]),1603 {1604 "role": "user",1605 "content": [1606 {"type": "text", "text": "what's in this"},1607 {1608 "type": "image_url",1609 "image_url": {"url": "data:image/png;base64,'/9j/4AAQSk'"},1610 },1611 ],1612 },1613 AIMessage(1614 "",1615 tool_calls=[1616 {1617 "name": "analyze",1618 "args": {"baz": "buz"},1619 "id": "1",1620 "type": "tool_call",1621 }1622 ],1623 ),1624 ToolMessage("foobar", tool_call_id="1", name="bar"),1625 {"role": "assistant", "content": "that's nice"},1626 ]1627 oai_messages = convert_to_openai_messages(messages)1628 # -> [1629 # {'role': 'system', 'content': 'foo'},1630 # {'role': 'user', 'content': [{'type': 'text', 'text': 'what's in this'}, {'type': 'image_url', 'image_url': {'url': "data:image/png;base64,'/9j/4AAQSk'"}}]},1631 # {'role': 'assistant', 'tool_calls': [{'type': 'function', 'id': '1','function': {'name': 'analyze', 'arguments': '{"baz": "buz"}'}}], 'content': ''},1632 # {'role': 'tool', 'name': 'bar', 'content': 'foobar'},1633 # {'role': 'assistant', 'content': 'that's nice'}1634 # ]1635 ```16361637 !!! version-added "Added in `langchain-core` 0.3.11"16381639 """ # noqa: E5011640 if text_format not in {"string", "block"}:1641 err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'."1642 raise ValueError(err)16431644 oai_messages: list[dict[str, Any]] = []16451646 messages_: Sequence[MessageLikeRepresentation]1647 if is_single := isinstance(messages, (BaseMessage, dict, str)):1648 messages_ = [messages]1649 else:1650 messages_ = cast("Sequence[MessageLikeRepresentation]", messages)16511652 for i, message in enumerate(convert_to_messages(messages_)):1653 oai_msg: dict[str, Any] = {"role": _get_message_openai_role(message)}1654 tool_messages: list[dict[str, Any]] = []1655 content: str | list[dict[str, Any]]16561657 if message.name:1658 oai_msg["name"] = message.name1659 if isinstance(message, AIMessage) and message.tool_calls:1660 oai_msg["tool_calls"] = _convert_to_openai_tool_calls(message.tool_calls)1661 if message.additional_kwargs.get("refusal"):1662 oai_msg["refusal"] = message.additional_kwargs["refusal"]1663 if isinstance(message, ToolMessage):1664 oai_msg["tool_call_id"] = message.tool_call_id1665 if include_id and message.id:1666 oai_msg["id"] = message.id16671668 if not message.content:1669 content = "" if text_format == "string" else []1670 elif isinstance(message.content, str):1671 if text_format == "string":1672 content = message.content1673 else:1674 content = [{"type": "text", "text": message.content}]1675 elif text_format == "string" and all(1676 isinstance(block, str) or block.get("type") == "text"1677 for block in message.content1678 ):1679 content = "\n".join(1680 block if isinstance(block, str) else block["text"]1681 for block in message.content1682 )1683 else:1684 content = []1685 for j, block in enumerate(message.content):1686 # OpenAI format1687 if isinstance(block, str):1688 content.append({"type": "text", "text": block})1689 elif block.get("type") == "text":1690 if missing := [k for k in ("text",) if k not in block]:1691 err = (1692 f"Unrecognized content block at "1693 f"messages[{i}].content[{j}] has 'type': 'text' "1694 f"but is missing expected key(s) "1695 f"{missing}. Full content block:\n\n{block}"1696 )1697 raise ValueError(err)1698 content.append({"type": block["type"], "text": block["text"]})1699 elif block.get("type") == "image_url":1700 if missing := [k for k in ("image_url",) if k not in block]:1701 err = (1702 f"Unrecognized content block at "1703 f"messages[{i}].content[{j}] has 'type': 'image_url' "1704 f"but is missing expected key(s) "1705 f"{missing}. Full content block:\n\n{block}"1706 )1707 raise ValueError(err)1708 content.append(1709 {1710 "type": "image_url",1711 "image_url": block["image_url"],1712 }1713 )1714 # Standard multi-modal content block1715 elif is_data_content_block(block):1716 formatted_block = convert_to_openai_data_block(block)1717 if (1718 formatted_block.get("type") == "file"1719 and "file" in formatted_block1720 and "filename" not in formatted_block["file"]1721 ):1722 logger.info("Generating a fallback filename.")1723 formatted_block["file"]["filename"] = "LC_AUTOGENERATED"1724 content.append(formatted_block)1725 # Anthropic and Bedrock converse format1726 elif (block.get("type") == "image") or "image" in block:1727 # Anthropic1728 if source := block.get("source"):1729 if missing := [1730 k for k in ("media_type", "type", "data") if k not in source1731 ]:1732 err = (1733 f"Unrecognized content block at "1734 f"messages[{i}].content[{j}] has 'type': 'image' "1735 f"but 'source' is missing expected key(s) "1736 f"{missing}. Full content block:\n\n{block}"1737 )1738 raise ValueError(err)1739 content.append(1740 {1741 "type": "image_url",1742 "image_url": {1743 "url": (1744 f"data:{source['media_type']};"1745 f"{source['type']},{source['data']}"1746 )1747 },1748 }1749 )1750 # Bedrock converse1751 elif image := block.get("image"):1752 if missing := [1753 k for k in ("source", "format") if k not in image1754 ]:1755 err = (1756 f"Unrecognized content block at "1757 f"messages[{i}].content[{j}] has key 'image', "1758 f"but 'image' is missing expected key(s) "1759 f"{missing}. Full content block:\n\n{block}"1760 )1761 raise ValueError(err)1762 b64_image = _bytes_to_b64_str(image["source"]["bytes"])1763 content.append(1764 {1765 "type": "image_url",1766 "image_url": {1767 "url": (1768 f"data:image/{image['format']};base64,{b64_image}"1769 )1770 },1771 }1772 )1773 else:1774 err = (1775 f"Unrecognized content block at "1776 f"messages[{i}].content[{j}] has 'type': 'image' "1777 f"but does not have a 'source' or 'image' key. Full "1778 f"content block:\n\n{block}"1779 )1780 raise ValueError(err)1781 # OpenAI file format1782 elif (1783 block.get("type") == "file"1784 and isinstance(block.get("file"), dict)1785 and isinstance(block.get("file", {}).get("file_data"), str)1786 ):1787 if block.get("file", {}).get("filename") is None:1788 logger.info("Generating a fallback filename.")1789 block["file"]["filename"] = "LC_AUTOGENERATED"1790 content.append(block)1791 # OpenAI audio format1792 elif (1793 block.get("type") == "input_audio"1794 and isinstance(block.get("input_audio"), dict)1795 and isinstance(block.get("input_audio", {}).get("data"), str)1796 and isinstance(block.get("input_audio", {}).get("format"), str)1797 ):1798 content.append(block)1799 elif block.get("type") == "tool_use":1800 if missing := [1801 k for k in ("id", "name", "input") if k not in block1802 ]:1803 err = (1804 f"Unrecognized content block at "1805 f"messages[{i}].content[{j}] has 'type': "1806 f"'tool_use', but is missing expected key(s) "1807 f"{missing}. Full content block:\n\n{block}"1808 )1809 raise ValueError(err)1810 if not any(1811 tool_call["id"] == block["id"]1812 for tool_call in cast("AIMessage", message).tool_calls1813 ):1814 oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])1815 oai_msg["tool_calls"].append(1816 {1817 "type": "function",1818 "id": block["id"],1819 "function": {1820 "name": block["name"],1821 "arguments": json.dumps(1822 block["input"], ensure_ascii=False1823 ),1824 },1825 }1826 )1827 elif block.get("type") == "function_call": # OpenAI Responses1828 if not any(1829 tool_call["id"] == block.get("call_id")1830 for tool_call in cast("AIMessage", message).tool_calls1831 ):1832 if missing := [1833 k1834 for k in ("call_id", "name", "arguments")1835 if k not in block1836 ]:1837 err = (1838 f"Unrecognized content block at "1839 f"messages[{i}].content[{j}] has 'type': "1840 f"'tool_use', but is missing expected key(s) "1841 f"{missing}. Full content block:\n\n{block}"1842 )1843 raise ValueError(err)1844 oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])1845 oai_msg["tool_calls"].append(1846 {1847 "type": "function",1848 "id": block.get("call_id"),1849 "function": {1850 "name": block.get("name"),1851 "arguments": block.get("arguments"),1852 },1853 }1854 )1855 if pass_through_unknown_blocks:1856 content.append(block)1857 elif block.get("type") == "tool_result":1858 if missing := [1859 k for k in ("content", "tool_use_id") if k not in block1860 ]:1861 msg = (1862 f"Unrecognized content block at "1863 f"messages[{i}].content[{j}] has 'type': "1864 f"'tool_result', but is missing expected key(s) "1865 f"{missing}. Full content block:\n\n{block}"1866 )1867 raise ValueError(msg)1868 tool_message = ToolMessage(1869 block["content"],1870 tool_call_id=block["tool_use_id"],1871 status="error" if block.get("is_error") else "success",1872 )1873 # Recurse to make sure tool message contents are OpenAI format.1874 tool_messages.extend(1875 convert_to_openai_messages(1876 [tool_message], text_format=text_format1877 )1878 )1879 elif (block.get("type") == "json") or "json" in block:1880 if "json" not in block:1881 msg = (1882 f"Unrecognized content block at "1883 f"messages[{i}].content[{j}] has 'type': 'json' "1884 f"but does not have a 'json' key. Full "1885 f"content block:\n\n{block}"1886 )1887 raise ValueError(msg)1888 content.append(1889 {1890 "type": "text",1891 "text": json.dumps(block["json"]),1892 }1893 )1894 elif (block.get("type") == "guard_content") or "guard_content" in block:1895 if (1896 "guard_content" not in block1897 or "text" not in block["guard_content"]1898 ):1899 msg = (1900 f"Unrecognized content block at "1901 f"messages[{i}].content[{j}] has 'type': "1902 f"'guard_content' but does not have a "1903 f"messages[{i}].content[{j}]['guard_content']['text'] "1904 f"key. Full content block:\n\n{block}"1905 )1906 raise ValueError(msg)1907 text = block["guard_content"]["text"]1908 if isinstance(text, dict):1909 text = text["text"]1910 content.append({"type": "text", "text": text})1911 # VertexAI format1912 elif block.get("type") == "media":1913 if missing := [k for k in ("mime_type", "data") if k not in block]:1914 err = (1915 f"Unrecognized content block at "1916 f"messages[{i}].content[{j}] has 'type': "1917 f"'media' but does not have key(s) {missing}. Full "1918 f"content block:\n\n{block}"1919 )1920 raise ValueError(err)1921 if "image" not in block["mime_type"]:1922 err = (1923 f"OpenAI messages can only support text and image data."1924 f" Received content block with media of type:"1925 f" {block['mime_type']}"1926 )1927 raise ValueError(err)1928 b64_image = _bytes_to_b64_str(block["data"])1929 content.append(1930 {1931 "type": "image_url",1932 "image_url": {1933 "url": (f"data:{block['mime_type']};base64,{b64_image}")1934 },1935 }1936 )1937 elif (1938 block.get("type") in {"thinking", "reasoning"}1939 or pass_through_unknown_blocks1940 ):1941 content.append(block)1942 else:1943 err = (1944 f"Unrecognized content block at "1945 f"messages[{i}].content[{j}] does not match OpenAI, "1946 f"Anthropic, Bedrock Converse, or VertexAI format. Full "1947 f"content block:\n\n{block}"1948 )1949 raise ValueError(err)1950 if text_format == "string" and not any(1951 block["type"] != "text" for block in content1952 ):1953 content = "\n".join(block["text"] for block in content)1954 oai_msg["content"] = content1955 if message.content and not oai_msg["content"] and tool_messages:1956 oai_messages.extend(tool_messages)1957 else:1958 oai_messages.extend([oai_msg, *tool_messages])19591960 if is_single:1961 return oai_messages[0]1962 return oai_messages196319641965def _first_max_tokens(1966 messages: Sequence[BaseMessage],1967 *,1968 max_tokens: int,1969 token_counter: Callable[[list[BaseMessage]], int],1970 text_splitter: Callable[[str], list[str]],1971 partial_strategy: Literal["first", "last"] | None = None,1972 end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,1973) -> list[BaseMessage]:1974 messages = list(messages)1975 if not messages:1976 return messages19771978 # Check if all messages already fit within token limit1979 if token_counter(messages) <= max_tokens:1980 # When all messages fit, only apply end_on filtering if needed1981 if end_on:1982 for _ in range(len(messages)):1983 if not _is_message_type(messages[-1], end_on):1984 messages.pop()1985 else:1986 break1987 return messages19881989 # Use binary search to find the maximum number of messages within token limit1990 left, right = 0, len(messages)1991 max_iterations = len(messages).bit_length()1992 for _ in range(max_iterations):1993 if left >= right:1994 break1995 mid = (left + right + 1) // 21996 if token_counter(messages[:mid]) <= max_tokens:1997 left = mid1998 idx = mid1999 else:2000 right = mid - 1
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.