From 864c5069730bc7750bc5d33aed60edfd80cc57df Mon Sep 17 00:00:00 2001 From: paxiaatucsdedu Date: Thu, 30 Oct 2025 10:23:28 -0700 Subject: [PATCH 1/2] Handle escaped JSON in tool call arguments Handle escaped JSON in tool call arguments. Fix issue https://github.com/oracle/langchain-oracle/issues/52. --- .../chat_models/oci_generative_ai.py | 8 ++- .../chat_models/test_oci_generative_ai.py | 63 ++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/libs/oci/langchain_oci/chat_models/oci_generative_ai.py b/libs/oci/langchain_oci/chat_models/oci_generative_ai.py index b3e79ac..f7fedde 100644 --- a/libs/oci/langchain_oci/chat_models/oci_generative_ai.py +++ b/libs/oci/langchain_oci/chat_models/oci_generative_ai.py @@ -94,9 +94,15 @@ def remove_signature_from_tool_description(name: str, description: str) -> str: @staticmethod def convert_oci_tool_call_to_langchain(tool_call: Any) -> ToolCall: """Convert an OCI tool call to a LangChain ToolCall.""" + parsed = json.loads(tool_call.arguments) + + # If the parsed result is a string, it means the JSON was escaped, so parse again + if isinstance(parsed, str): + parsed = json.loads(parsed) + return ToolCall( name=tool_call.name, - args=json.loads(tool_call.arguments) + args=parsed if "arguments" in tool_call.attribute_map else tool_call.parameters, id=tool_call.id if "id" in tool_call.attribute_map else uuid.uuid4().hex[:], diff --git a/libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py b/libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py index f81d2e9..b571d88 100644 --- a/libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py +++ b/libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py @@ -14,7 +14,7 @@ class MockResponseDict(dict): def __getattr__(self, val): # type: ignore[no-untyped-def] - return self[val] + return self.get(val) class MockToolCall(dict): @@ -253,6 +253,67 @@ def get_weather(location: str) -> str: assert tool_call["type"] == "function" assert tool_call["function"]["name"] == "get_weather" + # Test escaped JSON arguments (issue #52) + def mocked_response_escaped(*args, **kwargs): # type: ignore[no-untyped-def] + """Mock response with escaped JSON arguments.""" + return MockResponseDict( + { + "status": 200, + "data": MockResponseDict( + { + "chat_response": MockResponseDict( + { + "choices": [ + MockResponseDict( + { + "message": MockResponseDict( + { + "content": [ + MockResponseDict({"text": ""}) + ], + "tool_calls": [ + MockResponseDict( + { + "type": "FUNCTION", + "id": "call_escaped", + "name": "get_weather", + # Escaped JSON (the bug scenario) + "arguments": '"{\\\"location\\\": \\\"San Francisco\\\"}"', + "attribute_map": { + "id": "id", + "type": "type", + "name": "name", + "arguments": "arguments", + }, + } + ) + ], + } + ), + "finish_reason": "tool_calls", + } + ) + ], + "time_created": "2025-10-22T19:48:12.726000+00:00", + } + ), + "model_id": "meta.llama-3-70b-instruct", + "model_version": "1.0.0", + } + ), + "request_id": "test_escaped", + "headers": MockResponseDict({"content-length": "366"}), + } + ) + + monkeypatch.setattr(llm.client, "chat", mocked_response_escaped) + response_escaped = llm.bind_tools(tools=[get_weather]).invoke(messages) + + # Verify escaped JSON was correctly parsed to a dict + assert len(response_escaped.tool_calls) == 1 + assert response_escaped.tool_calls[0]["name"] == "get_weather" + assert response_escaped.tool_calls[0]["args"] == {"location": "San Francisco"} + @pytest.mark.requires("oci") def test_cohere_tool_choice_validation(monkeypatch: MonkeyPatch) -> None: From 7565f58fde1b7ae450e6f243819af10d3255d522 Mon Sep 17 00:00:00 2001 From: paxiaatucsdedu Date: Mon, 3 Nov 2025 15:07:46 -0800 Subject: [PATCH 2/2] 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. --- libs/oci/langchain_oci/chat_models/oci_generative_ai.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/oci/langchain_oci/chat_models/oci_generative_ai.py b/libs/oci/langchain_oci/chat_models/oci_generative_ai.py index f7fedde..11ed892 100644 --- a/libs/oci/langchain_oci/chat_models/oci_generative_ai.py +++ b/libs/oci/langchain_oci/chat_models/oci_generative_ai.py @@ -98,7 +98,11 @@ def convert_oci_tool_call_to_langchain(tool_call: Any) -> ToolCall: # If the parsed result is a string, it means the JSON was escaped, so parse again if isinstance(parsed, str): - parsed = json.loads(parsed) + try: + parsed = json.loads(parsed) + except json.JSONDecodeError: + # If it's not valid JSON, keep it as a string + pass return ToolCall( name=tool_call.name,