diff --git a/libs/core/langchain_core/messages/utils.py b/libs/core/langchain_core/messages/utils.py index c53b951dc16..56fe7e00254 100644 --- a/libs/core/langchain_core/messages/utils.py +++ b/libs/core/langchain_core/messages/utils.py @@ -12,6 +12,7 @@ from __future__ import annotations import base64 import inspect import json +import logging from collections.abc import Iterable, Sequence from functools import partial from typing import ( @@ -46,6 +47,9 @@ if TYPE_CHECKING: from langchain_core.runnables.base import Runnable +logger = logging.getLogger(__name__) + + def _get_type(v: Any) -> str: """Get the type associated with the object for serialization purposes.""" if isinstance(v, dict) and "type" in v: @@ -993,11 +997,14 @@ def convert_to_openai_messages( f"but is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - logger.warning(err) - text = str({k: v for k, v in block.items() if k != "type"}) if len(block) > 1 else "" + _raise_or_warn(err, coerce) + text = ( + _try_json_dumps( + {k: v for k, v in block.items() if k != "type"} + ) + if len(block) > 1 + else "" + ) else: text = block["text"] content.append({"type": block["type"], "text": text}) @@ -1009,10 +1016,8 @@ def convert_to_openai_messages( f"but is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - continue + _raise_or_warn(err, coerce) + continue content.append( {"type": "image_url", "image_url": block["image_url"]} ) @@ -1031,10 +1036,8 @@ def convert_to_openai_messages( f"but 'source' is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - continue + _raise_or_warn(err, coerce) + continue content.append( { "type": "image_url", @@ -1057,10 +1060,8 @@ def convert_to_openai_messages( f"but 'image' is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - continue + _raise_or_warn(err, coerce) + continue b64_image = _bytes_to_b64_str(image["source"]["bytes"]) content.append( { @@ -1080,10 +1081,8 @@ def convert_to_openai_messages( f"but does not have a 'source' or 'image' key. Full " f"content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - continue + _raise_or_warn(err, coerce) + continue elif block.get("type") == "tool_use": if missing := [ k for k in ("id", "name", "input") if k not in block @@ -1094,10 +1093,8 @@ def convert_to_openai_messages( f"'tool_use', but is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(err) - else: - continue + _raise_or_warn(err, coerce) + continue if not any( tool_call["id"] == block["id"] for tool_call in cast(AIMessage, message).tool_calls @@ -1123,10 +1120,8 @@ def convert_to_openai_messages( f"'tool_result', but is missing expected key(s) " f"{missing}. Full content block:\n\n{block}" ) - if not coerce: - raise ValueError(msg) - else: - continue + _raise_or_warn(err, coerce) + continue tool_message = ToolMessage( block["content"], tool_call_id=block["tool_use_id"], @@ -1146,10 +1141,8 @@ def convert_to_openai_messages( f"but does not have a 'json' key. Full " f"content block:\n\n{block}" ) - if not coerce: - raise ValueError(msg) - else: - continue + _raise_or_warn(msg, coerce) + continue content.append( {"type": "text", "text": json.dumps(block["json"])} ) @@ -1167,8 +1160,16 @@ def convert_to_openai_messages( f"messages[{i}].content[{j}]['guard_content']['text'] " f"key. Full content block:\n\n{block}" ) - raise ValueError(msg) - text = block["guard_content"]["text"] + _raise_or_warn(msg, coerce) + text = ( + _try_json_dumps( + {k: v for k, v in block.items() if k != "type"} + ) + if len(block) > 1 + else "" + ) + else: + text = block["guard_content"]["text"] if isinstance(text, dict): text = text["text"] content.append({"type": "text", "text": text}) @@ -1183,14 +1184,16 @@ def convert_to_openai_messages( f"'media' but does not have key(s) {missing}. Full " f"content block:\n\n{block}" ) - raise ValueError(err) + _raise_or_warn(err, coerce) + continue if "image" not in block["mime_type"]: err = ( f"OpenAI messages can only support text and image data." f" Received content block with media of type:" f" {block['mime_type']}" ) - raise ValueError(err) + _raise_or_warn(err, coerce) + continue b64_image = _bytes_to_b64_str(block["data"]) content.append( { @@ -1209,7 +1212,8 @@ def convert_to_openai_messages( f"Anthropic, Bedrock Converse, or VertexAI format. Full " f"content block:\n\n{block}" ) - raise ValueError(err) + _raise_or_warn(err, coerce) + continue if text_format == "string" and not any( block["type"] != "text" for block in content ): @@ -1432,3 +1436,17 @@ def _convert_to_openai_tool_calls(tool_calls: list[ToolCall]) -> list[dict]: } for tool_call in tool_calls ] + + +def _try_json_dumps(o: Any) -> str: + try: + return json.dumps(o) + except Exception: + return str(o) + + +def _raise_or_warn(msg: str, silence: bool) -> None: + if not silence: + raise ValueError(msg) + else: + logger.warning(msg) diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index f37458b257b..39a653708b2 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -861,7 +861,7 @@ class BaseChatOpenAI(BaseChatModel): ) def _coerce_messages(self, messages: list[BaseMessage]) -> list[BaseMessage]: - return convert_to_messages(convert_to_openai_messages(messages)) + return convert_to_messages(convert_to_openai_messages(messages, coerce=True)) @property def _identifying_params(self) -> Dict[str, Any]: