fix(brain): recover colon-JSON and single tool_call object forms
qwen2.5:3b emits tool calls in text shapes the parser dropped, breaking
two reviewer-reported behaviours:
- `getWeather: {"location": "Seoul"}` (a JSON object after the colon) was
dumped wholesale into {"query": "{...}"}, so `location` never reached the
tool. getWeather then ran with empty args, returned the auto-detected
location's weather, the model noticed the mismatch and retried — looping up
to 8 times before giving up with an English error. Now the JSON object after
the colon is parsed directly as the argument dict.
- `call_stop: {"id":..., "function": {"name": "setBroadcast",
"arguments": "{\"action\": \"stop\"}"}}` — a single tool_call object without
the `tool_calls: [...]` array wrapper, behind a `call_xxx:` label — matched
no form, so the raw JSON leaked to the user AND setBroadcast never ran
("방송 꺼줘" did nothing). Now name + arguments are pulled from the embedded
`function` object when the name is in the allow-list.
Field-captured from the live qwen2.5:3b brain (2026-06-12). Tests cover both
shapes, non-ASCII args, dict/string arguments, and unknown-tool rejection.
This commit is contained in:
@@ -84,6 +84,75 @@ class TestSimplifiedColonForm:
|
||||
assert name is None
|
||||
|
||||
|
||||
class TestColonFormWithJsonObjectValue:
|
||||
"""Form 2b: `toolName: {json object}`.
|
||||
|
||||
Field-captured from qwen2.5:3b (2026-06-12): the model emits the weather
|
||||
call as ``getWeather: {"location": "Seoul"}``. The whole JSON object must
|
||||
become the argument dict. Before the fix it was dumped into
|
||||
``{"query": "{...}"}``, so ``location`` never reached the tool, the tool
|
||||
fell back to the auto-detected location, and the model looped retrying
|
||||
different cities until the turn cap (observed: 8 getWeather calls, then an
|
||||
English error fallback).
|
||||
"""
|
||||
|
||||
def test_json_object_after_colon_becomes_args(self):
|
||||
content = 'getWeather: {"location": "Seoul"}'
|
||||
name, args, _ = _extract(content, tool_name="getWeather")
|
||||
assert name == "getWeather"
|
||||
assert args.get("location") == "Seoul"
|
||||
assert "query" not in args
|
||||
|
||||
def test_empty_json_object_after_colon(self):
|
||||
content = "getWeather: {}"
|
||||
name, args, _ = _extract(content, tool_name="getWeather")
|
||||
assert name == "getWeather"
|
||||
assert args == {}
|
||||
|
||||
def test_non_ascii_location_after_colon(self):
|
||||
content = 'getWeather: {"location": "서울"}'
|
||||
name, args, _ = _extract(content, tool_name="getWeather")
|
||||
assert name == "getWeather"
|
||||
assert args.get("location") == "서울"
|
||||
|
||||
|
||||
class TestSingleToolCallObjectForm:
|
||||
"""Form 2c: a single tool_call object without the `tool_calls: [...]` array.
|
||||
|
||||
Field-captured from qwen2.5:3b (2026-06-12) on "방송 꺼줘": the model picked
|
||||
the right tool but emitted it behind a `call_xxx:` label as a bare object.
|
||||
The name + arguments must be pulled from the embedded ``function`` object;
|
||||
before the fix this leaked the raw JSON to the user and the tool never ran.
|
||||
"""
|
||||
|
||||
def test_single_object_with_string_arguments(self):
|
||||
content = (
|
||||
'call_stop: {"id": "call_stop", "type": "function", '
|
||||
'"function": {"name": "setBroadcast", '
|
||||
'"arguments": "{\\"action\\": \\"stop\\"}"}}'
|
||||
)
|
||||
name, args, _ = _extract(content, tool_name="setBroadcast")
|
||||
assert name == "setBroadcast"
|
||||
assert args.get("action") == "stop"
|
||||
|
||||
def test_single_object_with_dict_arguments(self):
|
||||
content = (
|
||||
'{"id": "c1", "type": "function", '
|
||||
'"function": {"name": "getWeather", "arguments": {"location": "Seoul"}}}'
|
||||
)
|
||||
name, args, _ = _extract(content, tool_name="getWeather")
|
||||
assert name == "getWeather"
|
||||
assert args.get("location") == "Seoul"
|
||||
|
||||
def test_single_object_rejects_unknown_tool(self):
|
||||
content = (
|
||||
'{"function": {"name": "fileSystem_write", '
|
||||
'"arguments": "{\\"path\\": \\"/tmp/x\\"}"}}'
|
||||
)
|
||||
name, _args, _ = _extract(content, tool_name="setBroadcast")
|
||||
assert name is None
|
||||
|
||||
|
||||
class TestFunctionCallForm:
|
||||
"""Form 3: `toolName(...)`."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user