mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(fireworks): preserve malformed tool arguments as diagnostic JSON (#40818)
`ChatFireworks` can replay historical tool calls with malformed or non-object JSON arguments without forwarding invalid argument strings to Fireworks. --- Fireworks rejects conversation history containing malformed or non-object tool-call arguments, preventing an agent from recovering on its next turn. Wrap these arguments in a JSON object under `__invalid_tool_call_arguments` when serializing history, preserving the original payload, call IDs, and tool-result pairing. Apply this to parsed and raw tool-call history without mutating messages or executing repaired arguments; valid object strings stay unchanged. Made by [Open SWE](https://github.com/langchain-ai/open-swe) · [view thread](https://openswe.vercel.app/agents/0da06f8d-3ea1-5f01-aa55-953acb9a71fa) · openai:gpt-6-astra (medium) Co-authored-by: Mason Daugherty <mdrxy@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
2 files changed
+89
-1
No files matched your search
@@ -343,6 +343,21 @@ def _format_message_content(content: Any) -> Any:
|
||||
return formatted
|
||||
|
||||
|
||||
def _format_tool_call_arguments(arguments: str | dict | None) -> str | dict:
|
||||
"""Preserve invalid historical arguments inside a JSON object for replay."""
|
||||
if isinstance(arguments, dict):
|
||||
return arguments
|
||||
try:
|
||||
parsed = json.loads(arguments) if arguments is not None else None
|
||||
if isinstance(parsed, dict) and arguments is not None:
|
||||
json.dumps(parsed, allow_nan=False)
|
||||
return arguments
|
||||
except ValueError:
|
||||
logger.debug("Invalid JSON in historical Fireworks tool call arguments")
|
||||
logger.warning("Wrapping invalid historical Fireworks tool call arguments")
|
||||
return json.dumps({"__invalid_tool_call_arguments": arguments}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _convert_message_to_dict(message: BaseMessage) -> dict:
|
||||
"""Convert a LangChain message to a dictionary.
|
||||
|
||||
@@ -392,6 +407,19 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
|
||||
]
|
||||
elif "tool_calls" in message.additional_kwargs:
|
||||
message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]
|
||||
if "tool_calls" in message_dict:
|
||||
message_dict["tool_calls"] = [
|
||||
{
|
||||
**tool_call,
|
||||
"function": {
|
||||
**tool_call["function"],
|
||||
"arguments": _format_tool_call_arguments(
|
||||
tool_call["function"]["arguments"]
|
||||
),
|
||||
},
|
||||
}
|
||||
for tool_call in message_dict["tool_calls"]
|
||||
]
|
||||
# If tool calls only, content is None not empty string
|
||||
if "tool_calls" in message_dict and message_dict["content"] == "":
|
||||
message_dict["content"] = None
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
@@ -205,12 +206,71 @@ def test_convert_v1_message_filters_invalid_tool_call_content() -> None:
|
||||
{
|
||||
"type": "function",
|
||||
"id": "call_invalid",
|
||||
"function": {"name": "get_weather", "arguments": '{"city":'},
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": json.dumps(
|
||||
{"__invalid_tool_call_arguments": '{"city":'}
|
||||
),
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw_history", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("arguments", "wrapped"),
|
||||
[
|
||||
('{"city":', True),
|
||||
("[]", True),
|
||||
("null", True),
|
||||
(None, True),
|
||||
('{"x": NaN}', True),
|
||||
('{"city": "Paris"}', False),
|
||||
],
|
||||
)
|
||||
def test_replay_invalid_tool_call_arguments(
|
||||
arguments: str | None, *, wrapped: bool, raw_history: bool
|
||||
) -> None:
|
||||
message = AIMessage(
|
||||
content="",
|
||||
tool_calls=[{"name": "get_weather", "args": {"city": "Paris"}, "id": "valid"}],
|
||||
invalid_tool_calls=[
|
||||
{"name": "get_weather", "args": arguments, "id": "invalid", "error": "bad"}
|
||||
],
|
||||
)
|
||||
if raw_history:
|
||||
message.additional_kwargs["tool_calls"] = [
|
||||
{
|
||||
"type": "function",
|
||||
"id": "invalid",
|
||||
"function": {"name": "get_weather", "arguments": arguments},
|
||||
}
|
||||
]
|
||||
message.tool_calls = []
|
||||
message.invalid_tool_calls = []
|
||||
original = message.model_dump()
|
||||
|
||||
result = _convert_message_to_dict(message)
|
||||
|
||||
assert message.model_dump() == original
|
||||
invalid = result["tool_calls"][-1]
|
||||
assert invalid["id"] == "invalid"
|
||||
assert invalid["function"]["name"] == "get_weather"
|
||||
if wrapped:
|
||||
assert json.loads(invalid["function"]["arguments"]) == {
|
||||
"__invalid_tool_call_arguments": arguments
|
||||
}
|
||||
else:
|
||||
assert invalid["function"]["arguments"] == arguments
|
||||
if not raw_history:
|
||||
assert json.loads(result["tool_calls"][0]["function"]["arguments"]) == {
|
||||
"city": "Paris"
|
||||
}
|
||||
tool_result = ToolMessage(content="Invalid JSON", tool_call_id="invalid")
|
||||
assert _convert_message_to_dict(tool_result)["tool_call_id"] == invalid["id"]
|
||||
|
||||
|
||||
def test_sanitize_chat_completions_content_passthrough_non_text_block() -> None:
|
||||
blocks = [{"type": "image_url", "image_url": {"url": "https://x/y.png"}}]
|
||||
assert _sanitize_chat_completions_content(blocks) == blocks
|
||||
|
||||
Reference in new issue
Block a user