Skip to content

Commit 154057d

Browse files
[BUG] Handle escaped JSON in tool call arguments (#56)
* Handle escaped JSON in tool call arguments Handle escaped JSON in tool call arguments. Fix issue #52. * Handle JSON decode errors in tool call parsing Adds error handling for JSONDecodeError when parsing tool call results. If the string is not valid JSON, it is retained as a string instead of raising an exception.
1 parent a317f22 commit 154057d

File tree

2 files changed

+72
-1
lines changed

2 files changed

+72
-1
lines changed

libs/oci/langchain_oci/chat_models/oci_generative_ai.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,19 @@ def remove_signature_from_tool_description(name: str, description: str) -> str:
9494
@staticmethod
9595
def convert_oci_tool_call_to_langchain(tool_call: Any) -> ToolCall:
9696
"""Convert an OCI tool call to a LangChain ToolCall."""
97+
parsed = json.loads(tool_call.arguments)
98+
99+
# If the parsed result is a string, it means the JSON was escaped, so parse again
100+
if isinstance(parsed, str):
101+
try:
102+
parsed = json.loads(parsed)
103+
except json.JSONDecodeError:
104+
# If it's not valid JSON, keep it as a string
105+
pass
106+
97107
return ToolCall(
98108
name=tool_call.name,
99-
args=json.loads(tool_call.arguments)
109+
args=parsed
100110
if "arguments" in tool_call.attribute_map
101111
else tool_call.parameters,
102112
id=tool_call.id if "id" in tool_call.attribute_map else uuid.uuid4().hex[:],

libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,67 @@ def get_weather(location: str) -> str:
253253
assert tool_call["type"] == "function"
254254
assert tool_call["function"]["name"] == "get_weather"
255255

256+
# Test escaped JSON arguments (issue #52)
257+
def mocked_response_escaped(*args, **kwargs): # type: ignore[no-untyped-def]
258+
"""Mock response with escaped JSON arguments."""
259+
return MockResponseDict(
260+
{
261+
"status": 200,
262+
"data": MockResponseDict(
263+
{
264+
"chat_response": MockResponseDict(
265+
{
266+
"choices": [
267+
MockResponseDict(
268+
{
269+
"message": MockResponseDict(
270+
{
271+
"content": [
272+
MockResponseDict({"text": ""})
273+
],
274+
"tool_calls": [
275+
MockResponseDict(
276+
{
277+
"type": "FUNCTION",
278+
"id": "call_escaped",
279+
"name": "get_weather",
280+
# Escaped JSON (the bug scenario)
281+
"arguments": '"{\\\"location\\\": \\\"San Francisco\\\"}"',
282+
"attribute_map": {
283+
"id": "id",
284+
"type": "type",
285+
"name": "name",
286+
"arguments": "arguments",
287+
},
288+
}
289+
)
290+
],
291+
}
292+
),
293+
"finish_reason": "tool_calls",
294+
}
295+
)
296+
],
297+
"time_created": "2025-10-22T19:48:12.726000+00:00",
298+
}
299+
),
300+
"model_id": "meta.llama-3-70b-instruct",
301+
"model_version": "1.0.0",
302+
}
303+
),
304+
"request_id": "test_escaped",
305+
"headers": MockResponseDict({"content-length": "366"}),
306+
}
307+
)
308+
309+
monkeypatch.setattr(llm.client, "chat", mocked_response_escaped)
310+
response_escaped = llm.bind_tools(tools=[get_weather]).invoke(messages)
311+
312+
# Verify escaped JSON was correctly parsed to a dict
313+
assert len(response_escaped.tool_calls) == 1
314+
assert response_escaped.tool_calls[0]["name"] == "get_weather"
315+
assert response_escaped.tool_calls[0]["args"] == {"location": "San Francisco"}
316+
256317

257318
@pytest.mark.requires("oci")
258319
def test_cohere_tool_choice_validation(monkeypatch: MonkeyPatch) -> None:

0 commit comments

Comments
 (0)