mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* 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>
89 lines
5.4 KiB
Python
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))
|