from __future__ import annotations import asyncio import json from types import SimpleNamespace import pytest from langchain.agents.middleware.types import ModelRequest, ModelResponse from langchain_core.language_models.fake_chat_models import FakeListChatModel from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from deerflow.agents.middlewares.clarification_middleware import ( ClarificationMiddleware, ) from deerflow.agents.middlewares.knowledge_rag_middleware import ( KnowledgeRagMiddleware, ) from deerflow.agents.middlewares.markdown_file_deliverable_middleware import ( MarkdownFileDeliverableMiddleware, ) from deerflow.agents.middlewares.rag_citation_middleware import ( RagCitationMiddleware, ) from deerflow.agents.middlewares.smart_reasoning_retrieval_middleware import ( SMART_REASONING_DUPLICATE_SYNTHESIS_FALLBACK, SMART_REASONING_FINAL_SYNTHESIS_PROMPT, SMART_REASONING_LIMIT_FALLBACK, SMART_REASONING_TOOL_NAMES, SMART_RETRIEVAL_HISTORY_MARKER, SmartReasoningRetrievalMiddleware, sanitize_smart_retrieval_history, ) def _request(messages, *, tools=None, runtime=None) -> ModelRequest: return ModelRequest( model=FakeListChatModel(responses=["unused"]), messages=list(messages), tools=list(tools or []), runtime=runtime, ) def _tool_names(request: ModelRequest) -> list[str]: names = [] for tool in request.tools: if isinstance(tool, dict): names.append( str( tool.get("name") or (tool.get("function") or {}).get("name") or "" ) ) else: names.append(str(getattr(tool, "name", "") or "")) return names def _assert_tool_protocol_is_valid(messages) -> None: pending: set[str] = set() for message in messages: if isinstance(message, AIMessage): assert not pending calls = list(message.tool_calls) if not calls: calls = list((message.additional_kwargs or {}).get("tool_calls") or []) pending = { str(call.get("id") or "") for call in calls if call.get("id") } elif isinstance(message, ToolMessage): assert message.tool_call_id in pending pending.remove(message.tool_call_id) else: assert not pending assert not pending def _raw_baseline( user: HumanMessage, *, query: str | None = None, results: list[dict] | None = None, ): raw = AIMessage( content="", tool_calls=[ { "id": "raw-baseline", "name": "ordinary_knowledge_search", "args": {"queries": [query or str(user.content)]}, } ], ) raw_result = ToolMessage( content=json.dumps( { "ok": True, "operation": "knowledge_search", "results": results or [], }, ensure_ascii=False, ), tool_call_id="raw-baseline", name="ordinary_knowledge_search", ) return [user, raw, raw_result] def test_new_question_drops_old_library_evidence_and_query_but_keeps_current_turn(): old_ai = AIMessage( content="我先核对知识库。", tool_calls=[ { "id": "old-smart", "name": "ordinary_knowledge_search", "args": {"queries": ["OLD-QUERY-SECRET"]}, }, { "id": "old-other", "name": "local_calculator", "args": {"expression": "1+1"}, }, ], additional_kwargs={ "tool_calls": [ { "id": "old-smart", "type": "function", "function": { "name": "ordinary_knowledge_search", "arguments": '{"queries":["OLD-QUERY-SECRET"]}', }, }, { "id": "old-other", "type": "function", "function": { "name": "local_calculator", "arguments": '{"expression":"1+1"}', }, }, ] }, response_metadata={"finish_reason": "tool_calls"}, ) old_smart_result = ToolMessage( content="OLD-KB-A-EVIDENCE-SECRET", tool_call_id="old-smart", name="ordinary_knowledge_search", additional_kwargs={"mapping_id": "old-library-a"}, ) old_other_result = ToolMessage( content="calculator-result-2", tool_call_id="old-other", name="local_calculator", ) current_ai = AIMessage( content="", tool_calls=[ { "id": "current-smart", "name": "ordinary_wiki_search", "args": {"patterns": ["CURRENT-LIBRARY-B"]}, } ], ) current_result = ToolMessage( content="CURRENT-KB-B-EVIDENCE", tool_call_id="current-smart", name="ordinary_wiki_search", ) original = [ HumanMessage(content="旧问题"), old_ai, old_smart_result, old_other_result, AIMessage(content="旧回答"), HumanMessage(content="切换到知识库 B 后的新问题"), current_ai, current_result, ] captured = {} def handler(request): captured["messages"] = request.messages return AIMessage(content="新回答") result = SmartReasoningRetrievalMiddleware().wrap_model_call( _request(original), handler ) assert isinstance(result, AIMessage) sent = captured["messages"] rendered = "\n".join(str(message.model_dump()) for message in sent) assert "OLD-KB-A-EVIDENCE-SECRET" not in rendered assert "OLD-QUERY-SECRET" not in rendered assert "old-library-a" not in rendered assert SMART_RETRIEVAL_HISTORY_MARKER in rendered assert "CURRENT-KB-B-EVIDENCE" in rendered assert "CURRENT-LIBRARY-B" in rendered sanitized_old_ai = sent[1] assert isinstance(sanitized_old_ai, AIMessage) assert [call["name"] for call in sanitized_old_ai.tool_calls] == [ "local_calculator" ] assert sent[2] is old_other_result assert sent[-2:] == [current_ai, current_result] _assert_tool_protocol_is_valid(sent) # Request rewriting is transient: checkpoint/UI objects retain the full # old trace and current evidence. assert old_ai.tool_calls[0]["args"]["queries"] == ["OLD-QUERY-SECRET"] assert old_smart_result.content == "OLD-KB-A-EVIDENCE-SECRET" def test_hidden_context_after_question_does_not_make_current_evidence_historical(): messages = [ HumanMessage(content="当前问题"), AIMessage( content="", tool_calls=[ { "id": "current-read", "name": "ordinary_document_chunks", "args": {"chunk_id": "safe-current"}, } ], ), ToolMessage( content="当前轮原文证据", tool_call_id="current-read", name="ordinary_document_chunks", ), HumanMessage( content="隐藏业务上下文", name="business_context", additional_kwargs={"hide_from_ui": True}, ), ] assert sanitize_smart_retrieval_history(messages) is None def test_skill_capability_question_is_answered_without_knowledge_retrieval(): middleware = SmartReasoningRetrievalMiddleware() request = _request( [HumanMessage(content="你有哪些技能?")], tools=[ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "skill_list"}, ], ) captured: list[ModelRequest] = [] def handler(current_request): captured.append(current_request) return AIMessage(content="我可以列出并按需使用当前可用技能。") result = middleware.wrap_model_call(request, handler) assert len(captured) == 1 assert captured[0].messages == request.messages assert result.content == "我可以列出并按需使用当前可用技能。" assert result.tool_calls == [] def test_non_knowledge_skill_tool_call_is_preserved_verbatim(): middleware = SmartReasoningRetrievalMiddleware() response = AIMessage( content="", tool_calls=[ { "id": "skill-list-call", "name": "skill_list", "args": {"category": "available"}, } ], response_metadata={"finish_reason": "tool_calls"}, ) result = middleware.wrap_model_call( _request( [HumanMessage(content="列出可用技能")], tools=[ {"name": "ordinary_knowledge_search"}, {"name": "skill_list"}, ], ), lambda _request: response, ) assert result is response assert result.tool_calls == [ { "id": "skill-list-call", "name": "skill_list", "args": {"category": "available"}, "type": "tool_call", } ] def test_model_selected_knowledge_search_is_preserved_without_extra_calls(): middleware = SmartReasoningRetrievalMiddleware() response = AIMessage( content="", tool_calls=[ { "id": "kb-search-call", "name": "ordinary_knowledge_search", "args": {"queries": ["全民普法院 机构"]}, } ], response_metadata={"finish_reason": "tool_calls"}, ) result = middleware.wrap_model_call( _request( [HumanMessage(content="库里的全民普法院是什么机构?")], tools=[ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, ], ), lambda _request: response, ) assert result is response assert result.tool_calls == [ { "id": "kb-search-call", "name": "ordinary_knowledge_search", "args": {"queries": ["全民普法院 机构"]}, "type": "tool_call", } ] def test_direct_answer_after_raw_search_does_not_inject_wiki_search(): middleware = SmartReasoningRetrievalMiddleware() user = HumanMessage(content="根据刚才命中的原文概括一下") raw_history = _raw_baseline( user, results=[ { "title": "普法工作说明", "content": "全民普法是面向社会公众开展的法治宣传教育。", } ], ) result = middleware.wrap_model_call( _request( raw_history, tools=[ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, ], ), lambda _request: AIMessage( content="全民普法是面向社会公众开展的法治宣传教育。" ), ) assert result.content == "全民普法是面向社会公众开展的法治宣传教育。" assert result.tool_calls == [] def test_current_turn_duplicate_queries_and_wiki_pages_are_removed(): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, ] user = HumanMessage(content="梳理工程进展") raw_history = _raw_baseline(user, query="星河工程 进展") wiki_search = AIMessage( content="", tool_calls=[ { "id": "wiki-dedupe-search", "name": "ordinary_wiki_search", "args": {"pattern": "星河工程|进展"}, } ], ) wiki_result = ToolMessage( content=json.dumps( { "ok": True, "results": [ {"page_ref": "wkpage-one"}, {"page_ref": "wkpage-two"}, ], }, ensure_ascii=False, ), tool_call_id="wiki-dedupe-search", name="ordinary_wiki_search", ) first_read = AIMessage( content="", tool_calls=[ { "id": "wiki-dedupe-read-one", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-one"}, } ], ) first_result = ToolMessage( content='{"ok":true,"results":[{"complete":true}]}', tool_call_id="wiki-dedupe-read-one", name="ordinary_wiki_read_page", ) history = [ *raw_history, wiki_search, wiki_result, first_read, first_result, ] result = middleware.wrap_model_call( _request(history, tools=tools), lambda _request: AIMessage( content="", tool_calls=[ { "id": "raw-dedupe-mixed", "name": "ordinary_knowledge_search", "args": { "queries": [ " 星河工程 进展 ", "星河工程 建设单位", "星河工程 建设单位", ] }, }, { "id": "wiki-dedupe-repeat", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-one"}, }, { "id": "wiki-dedupe-new", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-two"}, }, ], ), ) assert [call["name"] for call in result.tool_calls] == [ "ordinary_knowledge_search", "ordinary_wiki_read_page", ] assert result.tool_calls[0]["args"]["queries"] == ["星河工程 建设单位"] assert result.tool_calls[1]["args"] == {"page_ref": "wkpage-two"} assert result.additional_kwargs[ "knowledge_retrieval_duplicate_actions_skipped" ] == 3 def test_new_wiki_cursor_is_preserved_while_repeated_cursor_is_removed(): middleware = SmartReasoningRetrievalMiddleware() user = HumanMessage(content="继续读取相关 Wiki 页面") prior_read = AIMessage( content="", tool_calls=[ { "id": "wiki-cursor-prior", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-one", "cursor": "cursor-1"}, } ], ) prior_result = ToolMessage( content='{"ok":true,"results":[{"complete":false,"next_cursor":"cursor-2"}]}', tool_call_id="wiki-cursor-prior", name="ordinary_wiki_read_page", ) result = middleware.wrap_model_call( _request( [user, prior_read, prior_result], tools=[{"name": "ordinary_wiki_read_page"}], ), lambda _request: AIMessage( content="", tool_calls=[ { "id": "wiki-cursor-repeat", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-one", "cursor": "cursor-1"}, }, { "id": "wiki-cursor-next", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-one", "cursor": "cursor-2"}, }, ], ), ) assert [call["id"] for call in result.tool_calls] == ["wiki-cursor-next"] assert result.additional_kwargs[ "knowledge_retrieval_duplicate_actions_skipped" ] == 1 def test_regex_wiki_pattern_and_document_reads_are_deduplicated(): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, {"name": "ordinary_chunk_regex_search"}, {"name": "ordinary_document_chunks"}, ] user = HumanMessage(content="核对工程编号") raw_history = _raw_baseline(user) actions = [ AIMessage( content="", tool_calls=[ { "id": "wiki-pattern-prior", "name": "ordinary_wiki_search", "args": {"pattern": "工程|编号"}, } ], ), ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="wiki-pattern-prior", name="ordinary_wiki_search", ), AIMessage( content="", tool_calls=[ { "id": "regex-prior", "name": "ordinary_chunk_regex_search", "args": {"pattern": "编号-[0-9]+"}, } ], ), ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="regex-prior", name="ordinary_chunk_regex_search", ), AIMessage( content="", tool_calls=[ { "id": "chunk-prior", "name": "ordinary_document_chunks", "args": { "identifier": "chunk-1", "identifier_type": "chunk_id", "page": 1, "page_size": 20, }, } ], ), ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="chunk-prior", name="ordinary_document_chunks", ), AIMessage( content="", tool_calls=[ { "id": "document-page-prior", "name": "ordinary_document_chunks", "args": { "identifier": "document-1", "identifier_type": "knowledge_id", "page": 1, "page_size": 20, }, } ], ), ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="document-page-prior", name="ordinary_document_chunks", ), ] result = middleware.wrap_model_call( _request([*raw_history, *actions], tools=tools), lambda _request: AIMessage( content="", tool_calls=[ { "id": "wiki-pattern-repeat", "name": "ordinary_wiki_search", "args": {"pattern": "工程|编号"}, }, { "id": "regex-repeat", "name": "ordinary_chunk_regex_search", "args": {"pattern": "编号-[0-9]+"}, }, { "id": "chunk-repeat-with-inert-pagination", "name": "ordinary_document_chunks", "args": { "identifier": "chunk-1", "identifier_type": "chunk_id", "page": 99, "page_size": 1, }, }, { "id": "document-page-repeat", "name": "ordinary_document_chunks", "args": { "identifier": "document-1", "identifier_type": "knowledge_id", "page": 1, "page_size": 20, }, }, { "id": "document-page-next", "name": "ordinary_document_chunks", "args": { "identifier": "document-1", "identifier_type": "knowledge_id", "page": 2, "page_size": 20, }, }, ], ), ) assert [call["id"] for call in result.tool_calls] == [ "document-page-next" ] assert result.additional_kwargs[ "knowledge_retrieval_duplicate_actions_skipped" ] == 4 def test_duplicate_only_call_preserves_model_answer_and_ends_without_reread(): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, ] user = HumanMessage(content="梳理新闻") raw_history = _raw_baseline(user, query="新闻") wiki_search = AIMessage( content="", tool_calls=[ { "id": "wiki-only-duplicate-search", "name": "ordinary_wiki_search", "args": {"pattern": "新闻"}, } ], ) wiki_result = ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="wiki-only-duplicate-search", name="ordinary_wiki_search", ) result = middleware.wrap_model_call( _request([*raw_history, wiki_search, wiki_result], tools=tools), lambda _request: AIMessage( content="依据现有资料回答。", tool_calls=[ { "id": "raw-only-duplicate", "name": "ordinary_knowledge_search", "args": {"queries": ["新闻"]}, } ], response_metadata={"finish_reason": "tool_calls"}, ), ) assert result.content == "依据现有资料回答。" assert result.tool_calls == [] assert result.response_metadata["finish_reason"] == "stop" @pytest.mark.parametrize("asynchronous", [False, True]) def test_duplicate_only_empty_retries_once_for_tool_free_synthesis(asynchronous): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, {"name": "local_calculator"}, ] user = HumanMessage(content="开幕式主题曲是什么?") raw_history = _raw_baseline( user, results=[ { "title": "开幕式原文", "content": "开幕式主题曲《友爱之声》由合唱团演唱。", } ], ) wiki_search = AIMessage( content="", tool_calls=[ { "id": "wiki-synthesis-search", "name": "ordinary_wiki_search", "args": {"pattern": "开幕式|主题曲"}, } ], ) wiki_result = ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="wiki-synthesis-search", name="ordinary_wiki_search", ) requests: list[ModelRequest] = [] def response_for(request: ModelRequest) -> AIMessage: requests.append(request) if len(requests) == 1: return AIMessage( content="", tool_calls=[ { "id": "raw-duplicate-empty", "name": "ordinary_knowledge_search", "args": {"queries": ["开幕式主题曲是什么?"]}, } ], response_metadata={"finish_reason": "tool_calls"}, ) return AIMessage(content="开幕式主题曲是《互助之声》。") request = _request( [*raw_history, wiki_search, wiki_result], tools=tools, ) if asynchronous: async def handler(current_request): return response_for(current_request) result = asyncio.run(middleware.awrap_model_call(request, handler)) else: result = middleware.wrap_model_call(request, response_for) assert len(requests) == 2 assert _tool_names(requests[1]) == ["local_calculator"] assert requests[1].tool_choice == "none" assert requests[1].messages[-1].name == ( "smart_reasoning_duplicate_synthesis" ) assert requests[1].messages[-1].additional_kwargs["hide_from_ui"] is True assert result.content == "开幕式主题曲是《友爱之声》。" assert result.tool_calls == [] assert result.additional_kwargs["knowledge_retrieval_stop_reason"] == ( "duplicate_only_synthesis" ) def test_duplicate_only_empty_uses_safe_fallback_if_synthesis_is_still_empty(): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, ] user = HumanMessage(content="梳理新闻") raw_history = _raw_baseline(user, query="新闻") wiki_search = AIMessage( content="", tool_calls=[ { "id": "wiki-empty-synthesis-search", "name": "ordinary_wiki_search", "args": {"pattern": "新闻"}, } ], ) wiki_result = ToolMessage( content='{"ok":true,"results":[]}', tool_call_id="wiki-empty-synthesis-search", name="ordinary_wiki_search", ) call_count = 0 def handler(_request): nonlocal call_count call_count += 1 if call_count == 1: return AIMessage( content="", tool_calls=[ { "id": "raw-empty-synthesis-repeat", "name": "ordinary_knowledge_search", "args": {"queries": ["新闻"]}, } ], ) return AIMessage(content="") result = middleware.wrap_model_call( _request( [*raw_history, wiki_search, wiki_result], tools=tools, ), handler, ) assert call_count == 2 assert result.content == SMART_REASONING_DUPLICATE_SYNTHESIS_FALLBACK def test_final_answer_uses_raw_document_name_over_wiki_and_model_rewrite(): middleware = SmartReasoningRetrievalMiddleware() tools = [ {"name": "ordinary_knowledge_search"}, {"name": "ordinary_wiki_search"}, {"name": "ordinary_wiki_read_page"}, ] user = HumanMessage(content="开幕式有哪些内容?") wiki_search = AIMessage( content="", tool_calls=[ { "id": "wiki-search-name", "name": "ordinary_wiki_search", "args": {"pattern": "开幕式"}, } ], ) wiki_result = ToolMessage( content='{"ok":true,"results":[{"page_ref":"wkpage-opening"}]}', tool_call_id="wiki-search-name", name="ordinary_wiki_search", ) wiki_read = AIMessage( content="", tool_calls=[ { "id": "wiki-read-name", "name": "ordinary_wiki_read_page", "args": {"page_ref": "wkpage-opening"}, } ], ) wiki_page = ToolMessage( content=json.dumps( { "ok": True, "results": [ { "content_markdown": "Wiki 生成稿称主题曲《互助之声》。" } ], }, ensure_ascii=False, ), tool_call_id="wiki-read-name", name="ordinary_wiki_read_page", ) raw_search = AIMessage( content="", tool_calls=[ { "id": "raw-search-name", "name": "ordinary_knowledge_search", "args": {"queries": ["开幕式 主题曲"]}, } ], ) raw_result = ToolMessage( content=json.dumps( { "ok": True, "results": [ { "title": "开幕新闻原文", "content": "文体表演包括《锦绣之韵》和主题曲《友爱之声》。", } ], }, ensure_ascii=False, ), tool_call_id="raw-search-name", name="ordinary_knowledge_search", ) result = middleware.wrap_model_call( _request( [ user, wiki_search, wiki_result, wiki_read, wiki_page, raw_search, raw_result, ], tools=tools, ), lambda _request: AIMessage( content="开幕式文体表演包括《锦绣之韵》和主题曲《互助之声》。" ), ) assert isinstance(result, AIMessage) assert result.content == "开幕式文体表演包括《锦绣之韵》和主题曲《友爱之声》。" assert result.additional_kwargs["knowledge_retrieval_fidelity_corrections"] == 1 def test_raw_name_guard_leaves_supported_and_ambiguous_names_unchanged(): from deerflow.agents.middlewares import smart_reasoning_retrieval_middleware as smart supported, supported_count = smart._repair_raw_name_fidelity( "主题曲《友爱之声》", ["原文写明主题曲《友爱之声》。"], ) ambiguous, ambiguous_count = smart._repair_raw_name_fidelity( "主题曲《第三首歌》", ["甲文写主题曲《第一首歌》,乙文写主题曲《第二首歌》。"], ) swapped, swapped_count = smart._repair_raw_name_fidelity( "主题曲《锦绣之韵》", ["文体表演包括《锦绣之韵》和主题曲《友爱之声》。"], ) unrelated, unrelated_count = smart._repair_raw_name_fidelity( "作为一般背景,可参照文件名《十四五规划》。", ["库内材料只提到文件名《内部规定》。"], ) assert supported == "主题曲《友爱之声》" assert supported_count == 0 assert ambiguous == "主题曲《第三首歌》" assert ambiguous_count == 0 assert swapped == "主题曲《友爱之声》" assert swapped_count == 1 assert unrelated == "作为一般背景,可参照文件名《十四五规划》。" assert unrelated_count == 0 @pytest.mark.parametrize("tool_name", sorted(SMART_REASONING_TOOL_NAMES)) def test_every_smart_retrieval_tool_is_removed_from_an_old_turn(tool_name): messages = [ HumanMessage(content="旧问题"), AIMessage( content="", tool_calls=[ { "id": "old-call", "name": tool_name, "args": {"secret": "old-argument"}, } ], ), ToolMessage( content="old-result-body", tool_call_id="old-call", name=tool_name, ), HumanMessage(content="新问题"), ] sanitized = sanitize_smart_retrieval_history(messages) assert sanitized is not None rendered = "\n".join(str(message.model_dump()) for message in sanitized) assert "old-argument" not in rendered assert "old-result-body" not in rendered assert SMART_RETRIEVAL_HISTORY_MARKER in rendered _assert_tool_protocol_is_valid(sanitized) def test_raw_provider_non_retrieval_call_in_mixed_bundle_stays_paired(): messages = [ HumanMessage(content="旧问题"), AIMessage( content="", additional_kwargs={ "tool_calls": [ { "id": "raw-smart", "type": "function", "function": { "name": "ordinary_wiki_search", "arguments": '{"patterns":["secret"]}', }, }, { "id": "raw-other", "type": "function", "function": { "name": "local_calculator", "arguments": '{"expression":"2+2"}', }, }, ] }, response_metadata={"finish_reason": "tool_calls"}, ), ToolMessage( content="secret-result", tool_call_id="raw-smart", name="ordinary_wiki_search", ), ToolMessage( content="4", tool_call_id="raw-other", name="local_calculator", ), HumanMessage(content="新问题"), ] sanitized = sanitize_smart_retrieval_history(messages) assert sanitized is not None old_ai = sanitized[1] assert isinstance(old_ai, AIMessage) raw_calls = old_ai.additional_kwargs["tool_calls"] assert [call["id"] for call in raw_calls] == ["raw-other"] assert old_ai.response_metadata["finish_reason"] == "tool_calls" assert any( isinstance(message, ToolMessage) and message.tool_call_id == "raw-other" for message in sanitized ) assert "secret-result" not in "\n".join( str(message.model_dump()) for message in sanitized ) _assert_tool_protocol_is_valid(sanitized) @pytest.mark.parametrize("asynchronous", [False, True]) def test_fortieth_call_hides_retrieval_tools_and_forces_plain_final(asynchronous): messages = [HumanMessage(content="需要多轮检索的问题")] messages.extend(AIMessage(content=f"intermediate-{index}") for index in range(39)) middleware = SmartReasoningRetrievalMiddleware() request = _request( messages, tools=[ {"name": "ordinary_wiki_search"}, { "type": "function", "function": {"name": "ordinary_document_chunks"}, }, {"name": "local_calculator"}, ], ) captured = {} def response_for(patched): captured["request"] = patched return ModelResponse( result=[ AIMessage( content="", tool_calls=[ { "id": "ignored-call", "name": "ordinary_wiki_search", "args": {"patterns": ["repeat"]}, } ], additional_kwargs={ "tool_calls": [ { "id": "ignored-call", "type": "function", "function": { "name": "ordinary_wiki_search", "arguments": '{"patterns":["repeat"]}', }, } ] }, response_metadata={"finish_reason": "tool_calls"}, ) ] ) if asynchronous: async def async_handler(patched): return response_for(patched) result = asyncio.run(middleware.awrap_model_call(request, async_handler)) else: result = middleware.wrap_model_call(request, response_for) patched = captured["request"] assert _tool_names(patched) == ["local_calculator"] assert patched.tool_choice == "none" assert SMART_REASONING_FINAL_SYNTHESIS_PROMPT in str( patched.messages[-1].content ) assert patched.messages[-1].additional_kwargs["hide_from_ui"] is True assert isinstance(result, ModelResponse) final = result.result[0] assert isinstance(final, AIMessage) assert final.tool_calls == [] assert final.content == SMART_REASONING_LIMIT_FALLBACK assert final.additional_kwargs["knowledge_retrieval_stop_reason"] == ( "iteration_limit" ) assert final.response_metadata["finish_reason"] == "stop" def test_no_forty_first_model_call_is_allowed(): messages = [HumanMessage(content="当前问题")] messages.extend(AIMessage(content=f"call-{index}") for index in range(40)) called = False def handler(_request): nonlocal called called = True return AIMessage(content="should not run") result = SmartReasoningRetrievalMiddleware().wrap_model_call( _request(messages, tools=[{"name": "ordinary_wiki_search"}]), handler ) assert called is False assert isinstance(result, AIMessage) assert result.content == SMART_REASONING_LIMIT_FALLBACK assert result.additional_kwargs["knowledge_retrieval_stop_reason"] == ( "iteration_limit" ) def test_middleware_mounts_only_for_smart_mode_and_keeps_clarification_last( monkeypatch, ): import deerflow.agents.lead_agent.agent as factory app_config = SimpleNamespace( token_usage=SimpleNamespace(enabled=False), skill_evolution=SimpleNamespace( enabled=False, review_worker=SimpleNamespace(enabled=False) ), knowledge=SimpleNamespace(enabled=True, rag_limit=5, rag_enabled=True), tool_search=SimpleNamespace(enabled=False), skill_stop=SimpleNamespace(enabled=False, skill_names=[]), skills=SimpleNamespace(container_path="/mnt/skills"), get_model_config=lambda _name: None, ) monkeypatch.setattr(factory, "build_lead_runtime_middlewares", lambda **_kwargs: []) monkeypatch.setattr( factory, "_create_context_reduction_middleware", lambda **_kwargs: None ) monkeypatch.setattr( factory, "get_memory_runtime_settings", lambda: SimpleNamespace(enabled=False), ) monkeypatch.setattr("deerflow.models.vision.vision_enabled", lambda *_args, **_kwargs: False) classic = factory._build_middlewares( { "context": { "knowledge_retrieval_mode": "classic", "rag_mode_enabled": True, } }, model_name=None, app_config=app_config, ) smart = factory._build_middlewares( { "context": { "knowledge_retrieval_mode": "smart_reasoning", "rag_mode_enabled": True, } }, model_name=None, app_config=app_config, ) text_only_coordinator = factory._build_middlewares( { "context": { "allowed_tools": [ "agent_orchestration", "ask_clarification", "present_files", ], "excluded_tools": ["write_file"], } }, model_name=None, app_config=app_config, ) assert not any( isinstance(middleware, SmartReasoningRetrievalMiddleware) for middleware in classic ) assert sum( isinstance(middleware, SmartReasoningRetrievalMiddleware) for middleware in smart ) == 1 assert sum( isinstance(middleware, RagCitationMiddleware) for middleware in classic ) == 1 assert not any( isinstance(middleware, RagCitationMiddleware) for middleware in smart ) assert sum( isinstance(middleware, KnowledgeRagMiddleware) for middleware in classic ) == 1 assert not any( isinstance(middleware, KnowledgeRagMiddleware) for middleware in smart ) assert any( isinstance(middleware, MarkdownFileDeliverableMiddleware) for middleware in classic ) assert not any( isinstance(middleware, MarkdownFileDeliverableMiddleware) for middleware in text_only_coordinator ) assert isinstance(classic[-1], ClarificationMiddleware) assert isinstance(smart[-1], ClarificationMiddleware) assert isinstance(text_only_coordinator[-1], ClarificationMiddleware) def test_smart_agent_keeps_normal_tools_skills_and_mcp_with_selected_space( monkeypatch, ) -> None: import deerflow.agents.analysis_project as analysis_project import deerflow.agents.lead_agent.agent as factory import deerflow.tools as tool_factory captured: dict[str, object] = {} def fake_get_available_tools(**kwargs): captured["tool_kwargs"] = kwargs return [ {"name": "web_search"}, {"name": "skill_list"}, {"name": "ordinary_knowledge_search"}, ] def fake_apply_prompt_template(**kwargs): captured["prompt_kwargs"] = kwargs return "keep web citations" monkeypatch.setattr(tool_factory, "get_available_tools", fake_get_available_tools) monkeypatch.setattr(factory, "validate_agent_id", lambda value: value) monkeypatch.setattr( factory, "load_agent_config", lambda _agent_id: SimpleNamespace( name="普通智能体", skills=["normal-skill"], model=None, tool_groups=None, ), ) monkeypatch.setattr(factory, "load_agent_soul", lambda _agent_id: "") monkeypatch.setattr( factory, "_resolve_model_name", lambda _name, *, app_config: "test-model", ) monkeypatch.setattr(factory, "create_chat_model", lambda **_kwargs: "model") monkeypatch.setattr(factory, "apply_prompt_template", fake_apply_prompt_template) monkeypatch.setattr(factory, "_build_middlewares", lambda *_args, **_kwargs: []) monkeypatch.setattr(factory, "_log_es_query_routing_registration", lambda **_kwargs: None) monkeypatch.setattr(factory, "create_agent", lambda **kwargs: kwargs) monkeypatch.setattr(analysis_project, "project_system_prompt", lambda _cfg: "") app_config = SimpleNamespace( get_model_config=lambda _name: SimpleNamespace(supports_thinking=True), ) config = { "context": { "agent_id": "ordinary-agent", "knowledge_space_strict": True, "knowledge_retrieval_mode": "smart_reasoning", "subagent_enabled": True, } } built = factory._make_lead_agent(config, app_config=app_config) tool_kwargs = captured["tool_kwargs"] assert isinstance(tool_kwargs, dict) assert tool_kwargs["include_mcp"] is True excluded = set(tool_kwargs["excluded_tools"]) assert "llmwiki_search" in excluded assert "web_search" not in excluded assert "skill_list" not in excluded assert "task" not in excluded assert not (excluded & SMART_REASONING_TOOL_NAMES) assert built["tools"] == [ {"name": "web_search"}, {"name": "skill_list"}, {"name": "ordinary_knowledge_search"}, ] assert captured["prompt_kwargs"]["available_skills"] == {"normal-skill"} assert captured["prompt_kwargs"]["subagent_enabled"] is True assert "keep web citations" in built["system_prompt"] assert "不表示每个问题都必须检索" in built["system_prompt"] assert config["metadata"]["knowledge_space_strict"] is False def test_rag_citation_middleware_fails_closed_for_smart_mode() -> None: smart_request = _request( [HumanMessage(content="查制度")], runtime=SimpleNamespace( context={ "knowledge_retrieval_mode": "smart_reasoning", "rag_mode_enabled": True, }, config={ "configurable": {"rag_mode_enabled": True}, }, ), ) assert RagCitationMiddleware._enabled(smart_request) is False assert RagCitationMiddleware._patch(smart_request) is None classic_request = _request( [HumanMessage(content="查制度")], runtime=SimpleNamespace( context={ "knowledge_retrieval_mode": "classic", "rag_mode_enabled": True, }, config={}, ), ) assert RagCitationMiddleware._enabled(classic_request) is True def test_smart_prompt_keeps_normal_tools_and_makes_knowledge_optional() -> None: import deerflow.agents.lead_agent.agent as factory base = "beforemust add Sourcesmiddlemust cite [1]after" cleaned = factory._without_classic_citation_contracts(base) prompt = factory._SMART_REASONING_KNOWLEDGE_SCOPE_PROMPT assert cleaned == "beforemust add Sourcesmiddleafter" assert "不表示每个问题都必须检索" in prompt assert "你有哪些技能/会做什么/有什么工具" in prompt assert "直接回答,不得为了展示流程而调用知识库工具" in prompt assert "知识库工具与其它正常工具并列" in prompt assert "正常问答、技能、Web、MCP、子智能体及其它工具仍按原有权限" in prompt assert "不存在强制顺序" in prompt assert "不得重复相同 query" in prompt assert "专名、作品/文件/活动标题、日期、时间、数字、单位和数量关系" in prompt assert "证据未明确给出的细节不要补写" in prompt assert "不要求使用 [1]、[2] 等编号" in prompt assert "不要求生成文末参考文献列表" in prompt