mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(models): parse numeric MindIE XML tool arguments (#6030)
* fix(models): parse numeric MindIE XML tool arguments Signed-off-by: lovett55 <xbzz101800@163.com> * chore(models): narrow MindIE numeric parsing change Signed-off-by: lovett55 <xbzz101800@163.com> * fix(models): reject unsafe MindIE numeric literals * fix(models): preserve nested MindIE JSON numbers on conversion errors Keep the entire XML argument as raw text when JSON numeric conversion fails. Reserve Python-literal fallback for JSON syntax errors so nested underflow cannot silently become zero. Port the malformed-number regression tests from PR #6207 and cover nested underflow, representable tiny values, and the existing literal fallback. Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> --------- Signed-off-by: lovett55 <xbzz101800@163.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
This commit is contained in:
parent
534142dfed
commit
d85f5bf6cc
@ -2279,6 +2279,11 @@ DeerFlow is model-agnostic — it works with any LLM that implements the OpenAI-
|
||||
- **Multimodal inputs** for image understanding and video comprehension
|
||||
- **Strong tool-use** for reliable function calling and structured outputs
|
||||
|
||||
MindIE XML tool-call arguments support signed, fractional, and exponent JSON
|
||||
numbers. If a number overflows, underflows to zero, or exceeds Python's integer
|
||||
digit limit, the complete argument stays a string, including nested JSON lists
|
||||
and objects, so parsing does not silently change its value.
|
||||
|
||||
## Embedded Python Client
|
||||
|
||||
For `DeerFlowClient(agent_name="researcher")`, the named agent's `mcp_plugins`
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
### MindIE XML numeric arguments (`mindie_provider.py`)
|
||||
|
||||
The XML parser recognizes JSON numeric syntax, including signed, fractional,
|
||||
and exponent values. Invalid numeric forms such as `007`, `3.`, and `+3` remain
|
||||
strings. Numeric conversion failures preserve the entire argument, including
|
||||
lists or objects with overflowing or underflowing floats or oversized integers.
|
||||
Only JSON syntax errors may use the existing Python-literal fallback; never retry
|
||||
numeric conversion failures through `ast.literal_eval`, which can silently turn
|
||||
nested underflow into zero. Regression coverage is in `tests/test_mindie_provider.py`.
|
||||
|
||||
### Model Factory (`packages/harness/deerflow/models/factory.py`)
|
||||
|
||||
Request-admission waits follow the next scheduled admission and configured
|
||||
|
||||
@ -1,15 +1,42 @@
|
||||
import ast
|
||||
import html
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
import httpx
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, ChatResult
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
_JSON_NUMBER_RE = re.compile(r"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$")
|
||||
|
||||
|
||||
def _parse_json_float(value: str) -> float:
|
||||
"""Parse a JSON float without silently overflowing or underflowing."""
|
||||
try:
|
||||
precise = Decimal(value)
|
||||
except InvalidOperation as exc:
|
||||
raise ValueError(f"invalid JSON number: {value}") from exc
|
||||
converted = float(precise)
|
||||
if not math.isfinite(converted) or (precise != 0 and converted == 0.0):
|
||||
raise ValueError(f"JSON number is outside the representable float range: {value}")
|
||||
return converted
|
||||
|
||||
|
||||
def _safe_literal(value: object) -> bool:
|
||||
"""Reject non-finite floats nested in an AST fallback value."""
|
||||
if isinstance(value, float):
|
||||
return math.isfinite(value)
|
||||
if isinstance(value, list):
|
||||
return all(_safe_literal(item) for item in value)
|
||||
if isinstance(value, dict):
|
||||
return all(_safe_literal(item) for item in value.values())
|
||||
return True
|
||||
|
||||
|
||||
def _fix_messages(messages: list) -> list:
|
||||
"""Sanitize incoming messages for MindIE compatibility.
|
||||
@ -122,14 +149,22 @@ def _parse_xml_tool_call_to_dict(content: str) -> tuple[str, list[dict]]:
|
||||
# Attempt to deserialize string values into native Python types
|
||||
# to satisfy downstream Pydantic validation.
|
||||
parsed_value = raw_value
|
||||
if raw_value.startswith(("[", "{")) or raw_value in ("true", "false", "null") or raw_value.isdigit():
|
||||
if raw_value.startswith(("[", "{")) or raw_value in ("true", "false", "null") or _JSON_NUMBER_RE.fullmatch(raw_value):
|
||||
try:
|
||||
parsed_value = json.loads(raw_value)
|
||||
parsed_value = json.loads(raw_value, parse_float=_parse_json_float)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
parsed_value = ast.literal_eval(raw_value)
|
||||
candidate = ast.literal_eval(raw_value)
|
||||
if _safe_literal(candidate):
|
||||
parsed_value = candidate
|
||||
except (ValueError, SyntaxError):
|
||||
pass
|
||||
except ValueError:
|
||||
# Preserve the entire argument when JSON numeric conversion
|
||||
# rejects overflow, underflow, or the integer digit limit.
|
||||
# Retrying containers with literal_eval would turn nested
|
||||
# underflowing numbers into zero and bypass this validation.
|
||||
pass
|
||||
|
||||
args[key] = parsed_value
|
||||
|
||||
|
||||
@ -330,6 +330,66 @@ class TestParseXmlToolCalls:
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == 42
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_value", "expected"),
|
||||
[("-3", -3), ("3.14", 3.14), ("1e-3", 1e-3)],
|
||||
)
|
||||
def test_signed_fractional_and_exponent_params_deserialised(self, raw_value, expected):
|
||||
content = f"<tool_call><function=f><parameter=n>{raw_value}</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == expected
|
||||
|
||||
@pytest.mark.parametrize("raw_value", ["9" * 5000, "1e400", "1e-400"])
|
||||
def test_unsafe_numeric_params_stay_strings(self, raw_value):
|
||||
content = f"<tool_call><function=f><parameter=n>{raw_value}</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == raw_value
|
||||
|
||||
def test_leading_zero_param_stays_string(self):
|
||||
content = "<tool_call><function=f><parameter=n>007</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == "007"
|
||||
|
||||
def test_trailing_dot_param_stays_string(self):
|
||||
content = "<tool_call><function=f><parameter=n>3.</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == "3."
|
||||
|
||||
def test_leading_plus_param_stays_string(self):
|
||||
content = "<tool_call><function=f><parameter=n>+3</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == "+3"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw_value",
|
||||
["[1e-400]", '{"n":1e-400}', '[{"n":-1e-400}]', '{"n":[0.5,1e-400]}', '{"n":[1e400]}'],
|
||||
)
|
||||
def test_unsafe_nested_json_numbers_preserve_entire_argument(self, raw_value):
|
||||
content = f"<tool_call><function=f><parameter=n>{raw_value}</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == raw_value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_value", "expected"),
|
||||
[
|
||||
("[0.0,1e-308,5e-324]", [0.0, 1e-308, 5e-324]),
|
||||
('{"n":-1e-3,"zero":0.0}', {"n": -1e-3, "zero": 0.0}),
|
||||
],
|
||||
)
|
||||
def test_representable_nested_json_numbers_deserialised(self, raw_value, expected):
|
||||
content = f"<tool_call><function=f><parameter=n>{raw_value}</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_value", "expected"),
|
||||
[("[True,1.5]", [True, 1.5]), ("{'n':1.5}", {"n": 1.5}), ("[1e400,]", "[1e400,]")],
|
||||
)
|
||||
def test_python_literal_fallback_preserves_finite_value_policy(self, raw_value, expected):
|
||||
content = f"<tool_call><function=f><parameter=n>{raw_value}</parameter></function></tool_call>"
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
assert calls[0]["args"]["n"] == expected
|
||||
|
||||
def test_list_param_deserialised(self):
|
||||
content = '<tool_call><function=f><parameter=lst>["a","b"]</parameter></function></tool_call>'
|
||||
_, calls = _parse_xml_tool_call_to_dict(content)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user