deer-flow/backend/tests/test_weknora_extension_live.py
Xuehao Xu c056ee556a
feat(knowledge): add WeKnora read-only extension (phase 3) (#6950)
* feat(knowledge): add WeKnora read-only extension (phase 3)

* fix(weknora): complete extension catalog and harden response validation

* test(extensions): cover WeKnora discovery in browser catalog

* docs(weknora): explain extension and MCP use cases

* docs(weknora): correct guide language and sync Chinese overview

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 10:28:30 +08:00

89 lines
5.4 KiB
Python

"""Opt-in read-only WeKnora v0.8.3 integration; needs existing indexed evidence."""
import json
import os
from pathlib import Path
import pytest
@pytest.mark.live
@pytest.mark.asyncio
async def test_weknora_live_selected_document_evidence(monkeypatch):
if os.getenv("DEER_FLOW_RUN_LIVE_TESTS") != "1":
pytest.skip("live knowledge checks are opt-in")
names = ["WEKNORA_BASE_URL", "WEKNORA_TENANT_ID", "WEKNORA_KNOWLEDGE_BASE_ID", "WEKNORA_DOCUMENT_ID", "WEKNORA_EVIDENCE_QUERY", "WEKNORA_API_KEY"]
if any(not os.getenv(name) for name in names):
pytest.fail("Set all documented WEKNORA_* variables for live verification")
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[2] / "examples/deerflow-extension-weknora"))
from deerflow_extension_weknora import WeKnoraProvider
from deerflow.knowledge import KnowledgeRequestContext, format_result, sources_from_messages
provider = WeKnoraProvider(
{"spaces": [{"base_url": os.environ["WEKNORA_BASE_URL"], "tenant_id": os.environ["WEKNORA_TENANT_ID"], "api_key_env": "WEKNORA_API_KEY", "user_ids": ["live-probe"], "knowledge_base_ids": [os.environ["WEKNORA_KNOWLEDGE_BASE_ID"]]}]}
)
context = KnowledgeRequestContext("live-probe", "lead_agent")
catalog = await provider.catalog(context=context, page=1, page_size=100, search="")
assert len(catalog.items) == 1, "Configured base must be visible"
dataset_id = catalog.items[0].id
document_id = os.environ["WEKNORA_DOCUMENT_ID"]
# Traverse actual catalog pagination, not a guessed first-page selection.
page = 1
while True:
documents = await provider.documents(dataset_id, context=context, page=page, page_size=100, search="")
match = next((item for item in documents.items if item.id == document_id), None)
if match:
assert match.selectable, "Configured document must be searchable"
break
assert documents.items and page * 100 < documents.total and page < 100, "Configured document not found within the bounded live scan"
page += 1
scope = {"version": 1, "mode": "selected", "dataset_ids": [dataset_id], "document_filters": [{"dataset_id": dataset_id, "document_ids": [document_id]}]}
result = await provider.search_scoped(os.environ["WEKNORA_EVIDENCE_QUERY"], context=KnowledgeRequestContext("live-probe", "lead_agent", scope))
content, artifact = format_result(result)
persisted = json.loads(json.dumps({"type": "tool", "content": content, "artifact": artifact}))
sources = sources_from_messages([persisted])
assert sources, "Live query must retrieve attributable document evidence"
for source in sources:
assert source.provider == "weknora" and source.page is None and source.url is None
assert any(hit.source and hit.source.title == source.title and source.excerpt.rstrip("…") in hit.content for hit in result.hits)
# Exercise the host's actual tool/artifact path with the live provider.
# The AIMessage is deterministic: this checks tool execution, not LLM quality.
from types import SimpleNamespace
from langchain_core.messages import AIMessage
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode
from deerflow.extensions import EXTENSION_SNAPSHOT_CONTEXT_KEY, ExtensionRegistry
from deerflow.knowledge import tools
from deerflow.knowledge.sources import prune_sources
from deerflow.runtime.serialization import serialize_channel_values_for_api
from deerflow.tools.builtins.task_tool import _task_result_command
registry = ExtensionRegistry()
with registry.attributed_to("deerflow_extension_weknora:install"):
registry.knowledge_provider("weknora", provider)
config = SimpleNamespace(get_tool_config=lambda _: SimpleNamespace(use="deerflow.knowledge.tools:knowledge_search_tool", model_extra={"provider": "weknora"}))
monkeypatch.setattr(tools, "get_app_config", lambda: config)
builder = StateGraph(MessagesState, context_schema=dict)
builder.add_node("tools", ToolNode([tools.knowledge_search_tool]))
builder.add_edge(START, "tools")
builder.add_edge("tools", END)
state = await builder.compile().ainvoke(
{"messages": [AIMessage(content="", tool_calls=[{"id": "live-search", "name": "knowledge_search", "args": {"query": os.environ["WEKNORA_EVIDENCE_QUERY"]}}])]},
context={"user_id": "live-probe", "is_subagent": True, "agent_id": "researcher", "__knowledge_scope_execution": scope, EXTENSION_SNAPSHOT_CONTEXT_KEY: registry.build()},
)
message = state["messages"][-1]
tool_sources = sources_from_messages([message])
assert tool_sources and all(source.provider == "weknora" for source in tool_sources)
history = json.loads(json.dumps(serialize_channel_values_for_api({"messages": [message]})))
assert sources_from_messages(history["messages"]) == tool_sources
forwarded = _task_result_command(tool_call_id="live-task", status="completed", result=message.content, knowledge_sources=[source.model_dump(mode="json", exclude_none=True) for source in tool_sources])
assert sources_from_messages(forwarded.update["messages"]) == tool_sources
_, pruned = prune_sources("removed by budget", message.artifact)
assert pruned["knowledge_sources"] == []
with pytest.raises(PermissionError):
await provider.search_scoped("test", context=KnowledgeRequestContext("unbound-live-user", "lead_agent", scope))