Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 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,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[:],
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