1377 lines
45 KiB
Python
1377 lines
45 KiB
Python
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 "<citations>keep web citations</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 "<citations>keep web citations</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 = "before<citations>must add Sources</citations>middle<rag_citation_mode>must cite [1]</rag_citation_mode>after"
|
|
cleaned = factory._without_classic_citation_contracts(base)
|
|
prompt = factory._SMART_REASONING_KNOWLEDGE_SCOPE_PROMPT
|
|
assert cleaned == "before<citations>must add Sources</citations>middleafter"
|
|
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
|