mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-20 19:46:16 +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>
213 lines
6.5 KiB
Python
213 lines
6.5 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
|
|
from app.gateway.knowledge_scope_admission import admit_message_knowledge_scope
|
|
from app.gateway.services import strip_internal_context_keys
|
|
from deerflow.config.tool_config import ToolConfig
|
|
from deerflow.knowledge_scope import KNOWLEDGE_SCOPE_KEY, KNOWLEDGE_SCOPE_RUNTIME_KEY
|
|
|
|
|
|
def _app_config(provider: str = "deerflow.community.ragflow.tools:knowledge_search_tool"):
|
|
tool_config = ToolConfig(
|
|
name="knowledge_search",
|
|
group="knowledge",
|
|
use=provider,
|
|
)
|
|
return SimpleNamespace(
|
|
knowledge_base=SimpleNamespace(enabled=True),
|
|
get_tool_config=lambda name: tool_config if name == "knowledge_search" else None,
|
|
)
|
|
|
|
|
|
def _agent_config(tool_groups=None):
|
|
return SimpleNamespace(tool_groups=tool_groups)
|
|
|
|
|
|
def _input(message):
|
|
return {"messages": [message]}
|
|
|
|
|
|
def test_admission_canonicalizes_custom_agent_human_message() -> None:
|
|
graph_input = _input(
|
|
HumanMessage(
|
|
content="question",
|
|
additional_kwargs={
|
|
KNOWLEDGE_SCOPE_KEY: {
|
|
"version": 1,
|
|
"mode": "selected",
|
|
"dataset_ids": [" dataset-a ", "dataset-a"],
|
|
}
|
|
},
|
|
)
|
|
)
|
|
|
|
admitted = admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id="agriculture-agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(None),
|
|
)
|
|
|
|
assert admitted == {
|
|
"version": 1,
|
|
"mode": "selected",
|
|
"dataset_ids": ["dataset-a"],
|
|
}
|
|
assert graph_input["messages"][0].additional_kwargs[KNOWLEDGE_SCOPE_KEY] == admitted
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("assistant_id", "provider", "tool_groups"),
|
|
[
|
|
(None, "deerflow.community.ragflow.tools:knowledge_search_tool", None),
|
|
("agent", "deerflow.community.lightrag.tools:knowledge_search_tool", None),
|
|
("agent", "deerflow.community.ragflow.tools:knowledge_search_tool", []),
|
|
("agent", "deerflow.community.ragflow.tools:knowledge_search_tool", ["web"]),
|
|
],
|
|
)
|
|
def test_scope_is_rejected_outside_supported_custom_agent(
|
|
assistant_id: str | None,
|
|
provider: str,
|
|
tool_groups: list[str] | None,
|
|
) -> None:
|
|
graph_input = _input(
|
|
HumanMessage(
|
|
content="question",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
)
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id=assistant_id,
|
|
app_config=_app_config(provider),
|
|
agent_config=_agent_config(tool_groups) if assistant_id not in {None, "lead_agent"} else None,
|
|
)
|
|
|
|
assert exc_info.value.status_code == 422
|
|
|
|
|
|
def test_admission_accepts_main_agent_with_configured_ragflow_provider() -> None:
|
|
graph_input = _input(
|
|
HumanMessage(
|
|
content="question",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
)
|
|
)
|
|
|
|
admitted = admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id="lead_agent",
|
|
app_config=_app_config(),
|
|
agent_config=None,
|
|
)
|
|
|
|
assert admitted == {"version": 1, "mode": "all"}
|
|
|
|
|
|
def test_scope_on_non_human_or_multiple_humans_is_rejected() -> None:
|
|
invalid_ai = _input(
|
|
AIMessage(
|
|
content="answer",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
)
|
|
)
|
|
with pytest.raises(HTTPException, match="HumanMessage"):
|
|
admit_message_knowledge_scope(
|
|
invalid_ai,
|
|
assistant_id="agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(),
|
|
)
|
|
|
|
duplicate = {
|
|
"messages": [
|
|
HumanMessage(
|
|
content="one",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
),
|
|
HumanMessage(
|
|
content="two",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
),
|
|
]
|
|
}
|
|
with pytest.raises(HTTPException, match="one new HumanMessage"):
|
|
admit_message_knowledge_scope(
|
|
duplicate,
|
|
assistant_id="agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(),
|
|
)
|
|
|
|
|
|
def test_scope_on_an_earlier_human_message_is_rejected() -> None:
|
|
graph_input = {
|
|
"messages": [
|
|
HumanMessage(
|
|
content="historical",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
),
|
|
HumanMessage(content="current"),
|
|
]
|
|
}
|
|
|
|
with pytest.raises(HTTPException, match="current HumanMessage"):
|
|
admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id="agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(),
|
|
)
|
|
|
|
|
|
def test_recovery_scope_replaces_client_forgery_and_legacy_removes_it() -> None:
|
|
graph_input = _input(
|
|
HumanMessage(
|
|
content="question",
|
|
additional_kwargs={KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"}},
|
|
)
|
|
)
|
|
recovered = admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id="agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(),
|
|
recovery_scope={"version": 1, "mode": "disabled"},
|
|
recovery=True,
|
|
)
|
|
assert recovered == {"version": 1, "mode": "disabled"}
|
|
assert graph_input["messages"][0].additional_kwargs[KNOWLEDGE_SCOPE_KEY] == recovered
|
|
|
|
admitted = admit_message_knowledge_scope(
|
|
graph_input,
|
|
assistant_id="agent",
|
|
app_config=_app_config(),
|
|
agent_config=_agent_config(),
|
|
recovery_scope=None,
|
|
recovery=True,
|
|
)
|
|
assert admitted is None
|
|
assert KNOWLEDGE_SCOPE_KEY not in graph_input["messages"][0].additional_kwargs
|
|
|
|
|
|
def test_free_form_runtime_scope_fields_are_scrubbed() -> None:
|
|
config = {
|
|
"context": {
|
|
KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"},
|
|
KNOWLEDGE_SCOPE_RUNTIME_KEY: {"version": 1, "mode": "disabled"},
|
|
},
|
|
"configurable": {
|
|
KNOWLEDGE_SCOPE_KEY: {"version": 1, "mode": "all"},
|
|
KNOWLEDGE_SCOPE_RUNTIME_KEY: {"version": 1, "mode": "disabled"},
|
|
},
|
|
}
|
|
|
|
strip_internal_context_keys(config)
|
|
|
|
assert config == {"context": {}, "configurable": {}}
|