mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 11:06:18 +00:00
* feat(knowledge): integrate RAGFlow retrieval and management * test(knowledge): cover merged listing tool * feat(knowledge): add per-message retrieval scope * chore(docs): remove unrelated document * docs(knowledge): add interaction screenshots * feat(knowledge): simplify scope selector trigger * docs(knowledge): refresh selector screenshot * feat(knowledge): defer standalone management * docs(knowledge): show chat-only scope UI * fix(knowledge): honor scope on clarification replies * fix(knowledge): harden scoped replay validation * docs(knowledge): clarify replay scope precedence * fix(knowledge): keep provider settings on tools * fix(config): preserve tools-only knowledge settings * fix(knowledge): submit custom assistant identity * refactor(knowledge): trim PR scope changes * fix(knowledge): sanitize document scope display * feat(knowledge): enable scope selection in main chat * fix(knowledge): emphasize active scope icon without button frame * fix(knowledge): close context scrubbing and refresh e2e checks * fix(knowledge): preserve idempotent canonical retries * fix(knowledge): accept promptless conversation runs * style(knowledge): format backend regression tests * chore(knowledge): trim PR scope and fix frontend format * fix(knowledge): remove shared-scope notice * fix(knowledge): remove scope persistence notice * docs(knowledge): include main chat in catalog scope * fix(knowledge): preserve scope recovery and upgrades * fix(config): preserve LightRAG knowledge upgrades --------- Co-authored-by: foreleven <for-eleven@hotmail.com>
182 lines
5.4 KiB
Python
182 lines
5.4 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
from langchain_core.tools import tool
|
|
from langgraph.runtime import Runtime
|
|
|
|
from deerflow.agents.middlewares.knowledge_scope_middleware import (
|
|
KnowledgeScopeMiddleware,
|
|
)
|
|
from deerflow.knowledge_scope import KNOWLEDGE_SCOPE_KEY, KNOWLEDGE_SCOPE_RUNTIME_KEY
|
|
from deerflow.runtime.context_keys import CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY
|
|
|
|
|
|
@tool
|
|
def knowledge_search(query: str) -> str:
|
|
"""Search."""
|
|
return query
|
|
|
|
|
|
@tool
|
|
def other_tool(query: str) -> str:
|
|
"""Other."""
|
|
return query
|
|
|
|
|
|
class _ModelRequest:
|
|
def __init__(self, messages, *, tools=(), runtime=None):
|
|
self.messages = list(messages)
|
|
self.tools = list(tools)
|
|
self.runtime = runtime
|
|
|
|
def override(self, **kwargs):
|
|
return _ModelRequest(
|
|
kwargs.get("messages", self.messages),
|
|
tools=kwargs.get("tools", self.tools),
|
|
runtime=self.runtime,
|
|
)
|
|
|
|
|
|
def _scope(mode: str = "selected") -> dict:
|
|
if mode != "selected":
|
|
return {"version": 1, "mode": mode}
|
|
return {
|
|
"version": 1,
|
|
"mode": "selected",
|
|
"dataset_ids": ["dataset-a"],
|
|
"display": {
|
|
"datasets": [{"id": "dataset-a", "name": "Agriculture"}],
|
|
},
|
|
}
|
|
|
|
|
|
def test_before_agent_projects_only_current_message_execution_scope() -> None:
|
|
historical = HumanMessage(
|
|
content="old",
|
|
id="old",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: _scope("all")},
|
|
)
|
|
current = HumanMessage(
|
|
content="new",
|
|
id="new",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: _scope()},
|
|
)
|
|
runtime = Runtime(context={CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY: frozenset({"old"})})
|
|
|
|
KnowledgeScopeMiddleware().before_agent(
|
|
{"messages": [historical, current]},
|
|
runtime,
|
|
)
|
|
|
|
assert runtime.context[KNOWLEDGE_SCOPE_RUNTIME_KEY] == {
|
|
"version": 1,
|
|
"mode": "selected",
|
|
"dataset_ids": ["dataset-a"],
|
|
}
|
|
|
|
|
|
def test_before_agent_uses_server_admitted_runtime_scope_for_recovery() -> None:
|
|
runtime = Runtime(
|
|
context={
|
|
CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY: frozenset({"source"}),
|
|
KNOWLEDGE_SCOPE_RUNTIME_KEY: _scope("disabled"),
|
|
}
|
|
)
|
|
source = HumanMessage(
|
|
content="source",
|
|
id="source",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: _scope("selected")},
|
|
)
|
|
|
|
KnowledgeScopeMiddleware().before_agent({"messages": [source]}, runtime)
|
|
|
|
assert runtime.context[KNOWLEDGE_SCOPE_RUNTIME_KEY] == {
|
|
"version": 1,
|
|
"mode": "disabled",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("method_name", ["wrap_model_call", "awrap_model_call"])
|
|
@pytest.mark.anyio
|
|
async def test_model_paths_strip_every_historical_scope_and_hide_disabled_tool(
|
|
method_name: str,
|
|
) -> None:
|
|
runtime = Runtime(context={KNOWLEDGE_SCOPE_RUNTIME_KEY: _scope("disabled")})
|
|
messages = [
|
|
HumanMessage(
|
|
content="old",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: _scope("all"), "keep": True},
|
|
),
|
|
AIMessage(
|
|
content="answer",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: _scope(), "keep": True},
|
|
),
|
|
]
|
|
request = _ModelRequest(
|
|
messages,
|
|
tools=[knowledge_search, other_tool],
|
|
runtime=runtime,
|
|
)
|
|
captured = []
|
|
middleware = KnowledgeScopeMiddleware()
|
|
|
|
if method_name == "wrap_model_call":
|
|
middleware.wrap_model_call(
|
|
request,
|
|
lambda value: captured.append(value) or "ok",
|
|
)
|
|
else:
|
|
|
|
async def handler(value):
|
|
captured.append(value)
|
|
return "ok"
|
|
|
|
await middleware.awrap_model_call(request, handler)
|
|
|
|
assert [item.name for item in captured[0].tools] == ["other_tool"]
|
|
assert all(KNOWLEDGE_SCOPE_KEY not in message.additional_kwargs for message in captured[0].messages)
|
|
assert all(message.additional_kwargs["keep"] for message in captured[0].messages)
|
|
assert KNOWLEDGE_SCOPE_KEY in request.messages[0].additional_kwargs
|
|
|
|
|
|
def test_disabled_execution_guard_blocks_knowledge_tool() -> None:
|
|
runtime = Runtime(context={KNOWLEDGE_SCOPE_RUNTIME_KEY: _scope("disabled")})
|
|
request = SimpleNamespace(
|
|
tool_call={"name": "knowledge_search", "id": "call-1"},
|
|
runtime=runtime,
|
|
)
|
|
|
|
result = KnowledgeScopeMiddleware().wrap_tool_call(
|
|
request,
|
|
lambda _request: pytest.fail("disabled call must not execute"),
|
|
)
|
|
|
|
assert isinstance(result, ToolMessage)
|
|
assert result.status == "error"
|
|
assert result.tool_call_id == "call-1"
|
|
assert "disabled" in str(result.content).lower()
|
|
|
|
|
|
def test_legacy_message_without_scope_keeps_tools_and_runtime_unset() -> None:
|
|
runtime = Runtime(context={CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY: frozenset()})
|
|
middleware = KnowledgeScopeMiddleware()
|
|
middleware.before_agent(
|
|
{"messages": [HumanMessage(content="legacy", id="new")]},
|
|
runtime,
|
|
)
|
|
request = _ModelRequest(
|
|
[HumanMessage(content="legacy")],
|
|
tools=[knowledge_search],
|
|
runtime=runtime,
|
|
)
|
|
captured = []
|
|
|
|
middleware.wrap_model_call(
|
|
request,
|
|
lambda value: captured.append(value) or "ok",
|
|
)
|
|
|
|
assert KNOWLEDGE_SCOPE_RUNTIME_KEY not in runtime.context
|
|
assert [item.name for item in captured[0].tools] == ["knowledge_search"]
|