I am betting that this weirdness is a result of some artifacts of training - which was later discarded.
Fortunately, it’s a consistent pattern. I wrote a simple work-around in Rust that has been working for months without issue:
#[derive(Deserialize, Debug, Clone)]
pub struct GPTHallcuinatedFunctionCall {
pub tool_uses: Vec<HallucinatedToolCalls>
}
#[derive(Deserialize, Debug, Clone)]
pub struct HallucinatedToolCalls {
pub recipient_name: String,
pub parameters: Value
}
// Sometimes GPT the decides to wrap the tools in a `multi_tool_use.parallel`
//{ tool_calls: [ToolCall {
// id: "call_y38JQQUmdYjTbYdJ3dIgAdFR",
// call_type: "function",
// function: Function {
// name: "multi_tool_use.parallel",
// arguments: "{\"tool_uses\":[{\"recipient_name\":\"functions.submit_document\",\"parameters\":{}}]}"
// }
//}
// Search through `tool_request` and determine if any name is "multi_tool_use.parallel"
for tool in tool_requests {
let function = &tool.function;
if function.name == "multi_tool_use.parallel" {
// Caught
// We need to deseralize the arguments
let caught_calls = serde_json::from_str::<GPTHallcuinatedFunctionCall>(&function.arguments).unwrap();
let tool_uses = caught_calls.tool_uses;
for tool_use in tool_uses {
let tool = ToolCall {
id: tool.id.clone(),
call_type: tool.call_type.clone(),
function: Function {
name: tool_use
.recipient_name
.clone()
.rsplit('.')
.next()
.unwrap()
.to_string(),
arguments: serde_json::to_string(&tool_use.parameters).unwrap(),
},
};
tools_requested.push(tool);
}
The idea is that the actual arguments are found nested inside of the arguments
properties.
I asked Claude to convert this code into Python:
from typing import List
from dataclasses import dataclass
import json
@dataclass
class GPTHallucinatedFunctionCall:
tool_uses: List['HallucinatedToolCalls']
@dataclass
class HallucinatedToolCalls:
recipient_name: str
parameters: dict
@dataclass
class Function:
name: str
arguments: str
@dataclass
class ToolCall:
id: str
call_type: str
function: Function
def process_tool_requests(tool_requests):
tools_requested = []
for tool in tool_requests:
function = tool.function
if function.name == "multi_tool_use.parallel":
# We need to deserialize the arguments
caught_calls = json.loads(function.arguments, object_hook=lambda d: GPTHallucinatedFunctionCall(**d))
tool_uses = caught_calls.tool_uses
for tool_use in tool_uses:
new_tool = ToolCall(
id=tool.id,
call_type=tool.call_type,
function=Function(
name=tool_use.recipient_name.rsplit('.', 1)[-1],
arguments=json.dumps(tool_use.parameters)
)
)
tools_requested.append(new_tool)
return tools_requested
And of course a unit test to ensure that it’s working (I did not test the Python code). It just prints out the result. Lazy, I know.
#[test]
fn test_openai_be_like_iTs_pARAlLeL_guIsE(){
let request2 = vec![
ToolCall {
id: "call_Vdmu1Lo2A7GXN82xAIvK1vHk".to_string(),
call_type: "function".to_string(),
function: Function {
name: "multi_tool_use.parallel".to_string(),
arguments: "{\"tool_uses\":[{\"recipient_name\":\"functions.patch_properties\",\"parameters\":{\"corrected_properties\":[{\"key\":\"is_refund\",\"updated_value\":\"true\"}]}},{\"recipient_name\":\"functions.submit_document\",\"parameters\":{}}]}".to_string()
}
}
];
let result = handle_tool_request(&request2).unwrap();
println!("Result: {:#?}", result);
}
I think Claude actually made it a workable unit test during conversion for Python. Nice(?)
import unittest
import json
from typing import List
from dataclasses import dataclass
# Assuming the previous code is in a file named gpt_function_call.py
from gpt_function_call import ToolCall, Function, process_tool_requests
class TestOpenAIParallelFunction(unittest.TestCase):
def test_openai_be_like_its_parallel_guise(self):
request2 = [
ToolCall(
id="call_Vdmu1Lo2A7GXN82xAIvK1vHk",
call_type="function",
function=Function(
name="multi_tool_use.parallel",
arguments=json.dumps({
"tool_uses": [
{
"recipient_name": "functions.patch_properties",
"parameters": {
"corrected_properties": [
{
"key": "is_refund",
"updated_value": "true"
}
]
}
},
{
"recipient_name": "functions.submit_document",
"parameters": {}
}
]
})
)
)
]
result = process_tool_requests(request2)
# Add assertions to check the result
self.assertEqual(len(result), 2)
self.assertEqual(result[0].function.name, "patch_properties")
self.assertEqual(json.loads(result[0].function.arguments), {
"corrected_properties": [{"key": "is_refund", "updated_value": "true"}]
})
self.assertEqual(result[1].function.name, "submit_document")
self.assertEqual(json.loads(result[1].function.arguments), {})
# Print the result for debugging
print("Result:", result)
if __name__ == '__main__':
unittest.main()