mirror of
https://github.com/hiyouga/LLaMA-Factory.git
synced 2026-11-07 07:30:00 +08:00
[data] fix: parse nested JSON literals in LFM2 tools (#10940)
Co-authored-by: LimbC-C <329404569+LimbC-C@users.noreply.github.com>
This commit is contained in:
@@ -934,9 +934,7 @@ class Xing4ToolUtils(ToolUtils):
|
||||
if params_content[position:].strip():
|
||||
return content
|
||||
|
||||
results.append(
|
||||
FunctionCall(name_match.group(1), json.dumps(arguments, ensure_ascii=False))
|
||||
)
|
||||
results.append(FunctionCall(name_match.group(1), json.dumps(arguments, ensure_ascii=False)))
|
||||
|
||||
return results
|
||||
|
||||
@@ -1038,19 +1036,16 @@ class LFM2ToolUtils(ToolUtils):
|
||||
@staticmethod
|
||||
def _ast_to_value(node: ast.AST) -> Any:
|
||||
"""Convert an AST node to a Python value, handling JSON-style booleans/null."""
|
||||
# Handle JSON-style true/false/null as Name nodes
|
||||
if isinstance(node, ast.Name):
|
||||
if node.id == "true":
|
||||
return True
|
||||
elif node.id == "false":
|
||||
return False
|
||||
elif node.id == "null":
|
||||
return None
|
||||
else:
|
||||
raise ValueError(f"Unknown identifier: {node.id}")
|
||||
|
||||
# Use literal_eval for other cases (strings, numbers, lists, dicts)
|
||||
return ast.literal_eval(node)
|
||||
class JsonLiteralTransformer(ast.NodeTransformer):
|
||||
def visit_Name(self, name: ast.Name) -> ast.AST:
|
||||
literals = {"true": True, "false": False, "null": None}
|
||||
if name.id in literals:
|
||||
return ast.copy_location(ast.Constant(value=literals[name.id]), name)
|
||||
return name
|
||||
|
||||
# Normalize literals at every depth without evaluating names or expressions.
|
||||
return ast.literal_eval(JsonLiteralTransformer().visit(node))
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
|
||||
@@ -449,6 +449,30 @@ def test_lfm2_tool_round_trip():
|
||||
assert json.loads(extracted[0][1]) == original["arguments"]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[
|
||||
{"options": {"enabled": True, "fallback": None}},
|
||||
{"items": [True, False, None]},
|
||||
{"options": [{"active": [True, False, None], "label": "true false null"}]},
|
||||
{"enabled": True, "fallback": None},
|
||||
],
|
||||
)
|
||||
def test_lfm2_tool_round_trip_nested_json_literals(arguments):
|
||||
formatter = FunctionFormatter(slots=["{{content}}"], tool_format="lfm2")
|
||||
extractor = ToolFormatter(tool_format="lfm2")
|
||||
rendered = formatter.apply(content=json.dumps({"name": "test_tool", "arguments": arguments}))[0]
|
||||
assert extractor.extract(rendered) == [("test_tool", json.dumps(arguments, ensure_ascii=False))]
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
@pytest.mark.parametrize("value", ['{"enabled": unknown}', "[1 + 2]"])
|
||||
def test_lfm2_tool_extractor_rejects_nonliteral_values(value):
|
||||
content = f"<|tool_call_start|>[test_tool(options={value})]<|tool_call_end|>"
|
||||
assert ToolFormatter(tool_format="lfm2").extract(content) == content
|
||||
|
||||
|
||||
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||
def test_minicpm5_function_formatter():
|
||||
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
|
||||
|
||||
Reference in New Issue
Block a user