Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion haystack/dataclasses/chat_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_openai_dict_format is meant to mirror to_openai_dict_format, which always emits arguments as a JSON string. Which OpenAI-compatible server sends a parsed dict here? Our chat generators don't route responses through this method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I hit this by calling from_openai_dict_format directly with arguments already parsed as a dict (some OpenAI-compatible SDKs / providers return tool call args that way). Haystack's own chat generators do not go through this method, so I do not have a production Haystack path that needs it. Happy to close this if you would rather keep the contract mirrored to to_openai_dict_format only.

return raw_arguments
return json.loads(raw_arguments) if raw_arguments else {}


class ChatRole(str, Enum):
"""
Enumeration representing the roles within a chat.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions test/dataclasses/test_chat_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading