mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(ollama): surface malformed tool-call JSON as OutputParserException
The ollama client parses streamed tool-call arguments internally and raises a bare ResponseError (e.g. "error parsing tool call: raw='...', err=...") when a model streams invalid JSON, before langchain_ollama ever sees the raw arguments. This opaque, non-langchain_core exception propagated straight out of invoke/stream calls and crashed the entire agent run instead of being recognized as a tool-call parsing failure, matching how malformed arguments are already handled elsewhere in this file via OutputParserException. Closes #34746 Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
1 parent
0cd7003c11
commit
f6ea0ae395
2 files changed
+92
-10
No files matched your search
@@ -83,7 +83,7 @@ from langchain_core.utils.function_calling import (
|
||||
convert_to_openai_tool,
|
||||
)
|
||||
from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
|
||||
from ollama import AsyncClient, Client, Message
|
||||
from ollama import AsyncClient, Client, Message, ResponseError
|
||||
from pydantic import BaseModel, PrivateAttr, field_validator, model_validator
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
@@ -117,6 +117,30 @@ def _get_usage_metadata_from_generation_info(
|
||||
return None
|
||||
|
||||
|
||||
def _reraise_if_malformed_tool_call_response(exc: ResponseError) -> None:
|
||||
"""Re-raise an Ollama tool-call parsing failure as an `OutputParserException`.
|
||||
|
||||
The `ollama` client parses streamed tool-call arguments internally and
|
||||
raises a bare `ResponseError` (e.g. `"error parsing tool call: raw='...',
|
||||
err=..."`) when the model produces malformed JSON. That error never
|
||||
reaches `_parse_json_string`, so it otherwise propagates as an opaque,
|
||||
non-`langchain_core` exception that crashes the entire agent run instead
|
||||
of being recognized as a tool-call parsing issue.
|
||||
|
||||
Args:
|
||||
exc: The `ResponseError` raised by the `ollama` client.
|
||||
|
||||
Raises:
|
||||
OutputParserException: If `exc` is a tool-call parsing failure.
|
||||
"""
|
||||
if "error parsing tool call" in str(exc.error):
|
||||
msg = (
|
||||
"Ollama returned a tool call with malformed JSON arguments that "
|
||||
f"could not be parsed: {exc.error}"
|
||||
)
|
||||
raise OutputParserException(msg) from exc
|
||||
|
||||
|
||||
def _parse_json_string(
|
||||
json_string: str,
|
||||
*,
|
||||
@@ -1109,11 +1133,15 @@ class ChatOllama(BaseChatModel):
|
||||
raise RuntimeError(msg)
|
||||
chat_params = self._chat_params(messages, stop, **kwargs)
|
||||
|
||||
if chat_params["stream"]:
|
||||
async for part in await self._async_client.chat(**chat_params):
|
||||
yield part
|
||||
else:
|
||||
yield await self._async_client.chat(**chat_params)
|
||||
try:
|
||||
if chat_params["stream"]:
|
||||
async for part in await self._async_client.chat(**chat_params):
|
||||
yield part
|
||||
else:
|
||||
yield await self._async_client.chat(**chat_params)
|
||||
except ResponseError as e:
|
||||
_reraise_if_malformed_tool_call_response(e)
|
||||
raise
|
||||
|
||||
def _create_chat_stream(
|
||||
self,
|
||||
@@ -1129,10 +1157,14 @@ class ChatOllama(BaseChatModel):
|
||||
raise RuntimeError(msg)
|
||||
chat_params = self._chat_params(messages, stop, **kwargs)
|
||||
|
||||
if chat_params["stream"]:
|
||||
yield from self._client.chat(**chat_params)
|
||||
else:
|
||||
yield self._client.chat(**chat_params)
|
||||
try:
|
||||
if chat_params["stream"]:
|
||||
yield from self._client.chat(**chat_params)
|
||||
else:
|
||||
yield self._client.chat(**chat_params)
|
||||
except ResponseError as e:
|
||||
_reraise_if_malformed_tool_call_response(e)
|
||||
raise
|
||||
|
||||
def _chat_stream_with_aggregation(
|
||||
self,
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from langchain_core.exceptions import OutputParserException
|
||||
from langchain_core.messages import AIMessage, BaseMessage, ChatMessage, HumanMessage
|
||||
from langchain_tests.unit_tests import ChatModelUnitTests
|
||||
from ollama import ResponseError
|
||||
|
||||
from langchain_ollama.chat_models import (
|
||||
ChatOllama,
|
||||
@@ -788,6 +789,55 @@ def test_invoke_raises_when_client_none() -> None:
|
||||
llm.invoke([HumanMessage("Hello")])
|
||||
|
||||
|
||||
def test_malformed_tool_call_response_error_is_output_parser_exception() -> None:
|
||||
"""A malformed tool-call `ResponseError` from `ollama` becomes an
|
||||
`OutputParserException` instead of propagating as an opaque error.
|
||||
|
||||
See issue #34746: Ollama's client raises a bare `ResponseError` when the
|
||||
model streams invalid JSON tool-call arguments, crashing the agent with
|
||||
a confusing error instead of a recognizable parsing failure.
|
||||
"""
|
||||
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
mock_client.chat.side_effect = ResponseError(
|
||||
"error parsing tool call: raw='{\"foo\": ' err=unexpected end of JSON input"
|
||||
)
|
||||
|
||||
llm = ChatOllama(model=MODEL_NAME)
|
||||
with pytest.raises(OutputParserException, match="error parsing tool call"):
|
||||
llm.invoke([HumanMessage("Hello")])
|
||||
|
||||
|
||||
async def test_malformed_tool_call_response_error_output_parser_async() -> None:
|
||||
"""Async counterpart of the malformed tool-call `ResponseError` handling."""
|
||||
with patch("langchain_ollama.chat_models.AsyncClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
mock_client.chat = AsyncMock(
|
||||
side_effect=ResponseError(
|
||||
"error parsing tool call: raw='{\"foo\": ' err=unexpected end of "
|
||||
"JSON input"
|
||||
)
|
||||
)
|
||||
|
||||
llm = ChatOllama(model=MODEL_NAME)
|
||||
with pytest.raises(OutputParserException, match="error parsing tool call"):
|
||||
await llm.ainvoke([HumanMessage("Hello")])
|
||||
|
||||
|
||||
def test_unrelated_response_error_is_not_wrapped() -> None:
|
||||
"""A `ResponseError` unrelated to tool-call parsing propagates unchanged."""
|
||||
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
mock_client.chat.side_effect = ResponseError("model not found", 404)
|
||||
|
||||
llm = ChatOllama(model=MODEL_NAME)
|
||||
with pytest.raises(ResponseError, match="model not found"):
|
||||
llm.invoke([HumanMessage("Hello")])
|
||||
|
||||
|
||||
def test_chat_ollama_ignores_strict_arg() -> None:
|
||||
"""Test that ChatOllama ignores the 'strict' argument."""
|
||||
response = [
|
||||
|
||||
Reference in new issue
Block a user