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
12 changes: 11 additions & 1 deletion libs/oci/langchain_oci/chat_models/oci_generative_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,19 @@ 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):
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,
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[:],
Expand Down
63 changes: 62 additions & 1 deletion libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down