diff --git a/haystack/dataclasses/chat_message.py b/haystack/dataclasses/chat_message.py index a305e4187c1..e18a6221db7 100644 --- a/haystack/dataclasses/chat_message.py +++ b/haystack/dataclasses/chat_message.py @@ -16,6 +16,20 @@ logger = logging.getLogger(__name__) +def _parse_openai_tool_call_arguments(raw_arguments: Any) -> dict[str, Any]: + """ + Parse OpenAI-style tool call arguments into a dictionary. + + A dict is returned as is, a missing, null, or empty value becomes `{}`, and anything else is parsed as a + JSON string. + + :raises json.JSONDecodeError: If the arguments are not valid JSON. + """ + if isinstance(raw_arguments, dict): + return raw_arguments + return json.loads(raw_arguments) if raw_arguments else {} + + class ChatRole(str, Enum): """ Enumeration representing the roles within a chat. @@ -815,11 +829,12 @@ def from_openai_dict_format(cls, message: dict[str, Any]) -> "ChatMessage": for tc in tool_calls: # Zero-argument tool calls from OpenAI-compatible servers may send an # empty string, null, or omit `arguments` entirely; treat all as {}. + # Some servers also send a parsed dict instead of a JSON string. raw_arguments = tc["function"].get("arguments") haystack_tc = ToolCall( id=tc.get("id"), tool_name=tc["function"]["name"], - arguments=json.loads(raw_arguments) if raw_arguments else {}, + arguments=_parse_openai_tool_call_arguments(raw_arguments), ) haystack_tool_calls.append(haystack_tc) return cls.from_assistant(text=content, name=name, tool_calls=haystack_tool_calls) diff --git a/releasenotes/notes/fix-openai-dict-tool-arguments-c8e1a4b27f90d3a1.yaml b/releasenotes/notes/fix-openai-dict-tool-arguments-c8e1a4b27f90d3a1.yaml new file mode 100644 index 00000000000..4fe034a59c8 --- /dev/null +++ b/releasenotes/notes/fix-openai-dict-tool-arguments-c8e1a4b27f90d3a1.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``ChatMessage.from_openai_dict_format`` now accepts tool-call ``arguments`` that are already a dictionary + instead of raising a ``TypeError``. Some OpenAI-compatible servers send a parsed object rather than a JSON string. diff --git a/test/dataclasses/test_chat_message.py b/test/dataclasses/test_chat_message.py index b476b59c80c..5b4fd1e042e 100644 --- a/test/dataclasses/test_chat_message.py +++ b/test/dataclasses/test_chat_message.py @@ -1082,6 +1082,28 @@ def test_from_openai_dict_format_tool_call_with_missing_arguments(self): assert message.tool_call is not None assert message.tool_call.arguments == {} + def test_from_openai_dict_format_tool_call_with_dict_arguments(self): + # Some OpenAI-compatible servers already parse arguments into a dict. + openai_msg = { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_123", "function": {"name": "get_weather", "arguments": {"location": "Berlin"}}} + ], + } + message = ChatMessage.from_openai_dict_format(openai_msg) + assert message.tool_call is not None + assert message.tool_call.arguments == {"location": "Berlin"} + + def test_from_openai_dict_format_tool_call_with_invalid_json_arguments(self): + openai_msg = { + "role": "assistant", + "content": None, + "tool_calls": [{"id": "call_1", "function": {"name": "now", "arguments": "{not-json"}}], + } + with pytest.raises(json.JSONDecodeError): + ChatMessage.from_openai_dict_format(openai_msg) + def test_from_openai_dict_format_tool_message(self): openai_msg = {"role": "tool", "content": "The weather is sunny", "tool_call_id": "call_123"} message = ChatMessage.from_openai_dict_format(openai_msg)