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():
|
if params_content[position:].strip():
|
||||||
return content
|
return content
|
||||||
|
|
||||||
results.append(
|
results.append(FunctionCall(name_match.group(1), json.dumps(arguments, ensure_ascii=False)))
|
||||||
FunctionCall(name_match.group(1), json.dumps(arguments, ensure_ascii=False))
|
|
||||||
)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -1038,19 +1036,16 @@ class LFM2ToolUtils(ToolUtils):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _ast_to_value(node: ast.AST) -> Any:
|
def _ast_to_value(node: ast.AST) -> Any:
|
||||||
"""Convert an AST node to a Python value, handling JSON-style booleans/null."""
|
"""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)
|
class JsonLiteralTransformer(ast.NodeTransformer):
|
||||||
return ast.literal_eval(node)
|
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
|
@override
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -449,6 +449,30 @@ def test_lfm2_tool_round_trip():
|
|||||||
assert json.loads(extracted[0][1]) == original["arguments"]
|
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"])
|
@pytest.mark.runs_on(["cpu", "mps", "xpu"])
|
||||||
def test_minicpm5_function_formatter():
|
def test_minicpm5_function_formatter():
|
||||||
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
|
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
|
||||||
|
|||||||
Reference in New Issue
Block a user