chore(adpator):Adapt MindIE engine model and improve testing and fixes (#2523)

* feat(models): 适配 MindIE引擎的模型

* test: add unit tests for MindIEChatModel adapter and fix PR review comments

* chore: update uv.lock with pytest-asyncio

* build: add pytest-asyncio to test dependencies

* fix: address PR review comments (lazy import, cache clients, safe newline escape, strict xml regex)

* fix(mindie): preserve string args without JSON quotes in XML tool call serialization

* fix(mindie): preserve string args without JSON quotes in XML tool call serialization

* test_mindie_provider:format

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(mindie): prevent nested tool_call params from leaking into outer args

* fixed by escaping XML entities in _fix_messages and unescaping during parse, with regression tests added.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
pyp0327
2026-04-28 15:09:31 +08:00
committed by GitHub
parent 6bd88fe14c
commit 395c14357b
2 changed files with 100 additions and 7 deletions
+82 -1
View File
@@ -91,7 +91,7 @@ class TestFixMessages:
assert isinstance(out, AIMessage)
assert "<tool_call>" in out.content
assert "<function=get_weather>" in out.content
assert '<parameter=city>"London"</parameter>' in out.content
assert "<parameter=city>London</parameter>" in out.content
assert not getattr(out, "tool_calls", [])
def test_ai_message_text_preserved_before_xml(self):
@@ -116,6 +116,22 @@ class TestFixMessages:
assert "<function=tool_a>" in content
assert "<function=tool_b>" in content
def test_ai_message_tool_args_are_xml_escaped(self):
msg = AIMessage(
content="",
tool_calls=[
{
"name": "fn<&>",
"args": {"k<&>": "v<&>"},
"id": "id1",
}
],
)
result = _fix_messages([msg])
content = result[0].content
assert "<function=fn&lt;&amp;&gt;>" in content
assert "<parameter=k&lt;&amp;&gt;>v&lt;&amp;&gt;</parameter>" in content
# ── ToolMessage → HumanMessage ────────────────────────────────────────────
def test_tool_message_becomes_human_message(self):
@@ -185,6 +201,15 @@ class TestParseXmlToolCalls:
assert calls[0]["name"] == "a"
assert calls[1]["name"] == "b"
def test_nested_tool_call_blocks_do_not_break_parsing(self):
content = "<tool_call><function=outer><parameter=q>1</parameter><tool_call><function=inner><parameter=x>2</parameter></function></tool_call></function></tool_call>"
clean, calls = _parse_xml_tool_call_to_dict(content)
assert clean == ""
assert len(calls) == 1
assert calls[0]["name"] == "outer"
assert calls[0]["args"] == {"q": 1}
assert "x" not in calls[0]["args"]
def test_text_before_tool_call_preserved(self):
content = "Here is the answer.\n<tool_call><function=f><parameter=k>v</parameter></function></tool_call>"
clean, calls = _parse_xml_tool_call_to_dict(content)
@@ -226,6 +251,12 @@ class TestParseXmlToolCalls:
_, c2 = _parse_xml_tool_call_to_dict(block)
assert c1[0]["id"] != c2[0]["id"]
def test_escaped_entities_are_unescaped(self):
content = "<tool_call><function=fn&lt;&amp;&gt;><parameter=k&lt;&amp;&gt;>v&lt;&amp;&gt;</parameter></function></tool_call>"
_, calls = _parse_xml_tool_call_to_dict(content)
assert calls[0]["name"] == "fn<&>"
assert calls[0]["args"]["k<&>"] == "v<&>"
# ═════════════════════════════════════════════════════════════════════════════
# 3. MindIEChatModel._patch_result_with_tools
@@ -244,6 +275,12 @@ class TestPatchResult:
patched = model._patch_result_with_tools(result)
assert patched.generations[0].message.content == "line1\nline2"
def test_escaped_newlines_inside_code_fence_preserved(self):
model = self._model()
result = _make_chat_result('text\\n```json\n{"k":"a\\\\nb"}\n```\\nend')
patched = model._patch_result_with_tools(result)
assert patched.generations[0].message.content == 'text\n```json\n{"k":"a\\\\nb"}\n```\nend'
def test_xml_tool_calls_extracted(self):
model = self._model()
content = "<tool_call><function=calc><parameter=expr>1+1</parameter></function></tool_call>"
@@ -281,6 +318,50 @@ class TestPatchResult:
assert patched is not None
class TestMindIEInit:
def test_timeout_kwargs_are_normalized(self):
captured = {}
def fake_init(self, **kwargs):
captured.update(kwargs)
with patch("deerflow.models.mindie_provider.ChatOpenAI.__init__", new=fake_init):
MindIEChatModel(
model="mindie-test",
api_key="test-key",
connect_timeout=1.0,
read_timeout=2.0,
write_timeout=3.0,
pool_timeout=4.0,
)
timeout = captured.get("timeout")
assert timeout is not None
assert timeout.connect == 1.0
assert timeout.read == 2.0
assert timeout.write == 3.0
assert timeout.pool == 4.0
def test_explicit_timeout_takes_precedence(self):
captured = {}
def fake_init(self, **kwargs):
captured.update(kwargs)
with patch("deerflow.models.mindie_provider.ChatOpenAI.__init__", new=fake_init):
MindIEChatModel(
model="mindie-test",
api_key="test-key",
timeout=9.0,
connect_timeout=1.0,
read_timeout=2.0,
write_timeout=3.0,
pool_timeout=4.0,
)
assert captured.get("timeout") == 9.0
# ═════════════════════════════════════════════════════════════════════════════
# 4. MindIEChatModel._generate (sync)
# ═════════════════════════════════════════════════════════════════════════════