feat(ai-writing): 检索类型加回「知识库检索」(直连内网 ES 接口)
弱模型常识别不了配置技能(deepseek-chat 等),恢复旧的"不用技能"检索路径 作为独立检索类型 knowledge_base:绕过技能编排,直接调 ai_writing.intranet_search_url 内网知识库 ES 接口(IntranetSearchProvider) 对检索词并发检索;未配地址 → 无素材 + skill_error 告警。 - researcher.py: material_source 归一保留 knowledge_base + 新增直连内网分支 - state.py: material_source 三值注释 - 前端 AIWritingForm/WritingFormContext/types: 下拉新增「知识库检索」 - tests/test_ai_writing_knowledge_base.py: 直连/未配/不编排技能 3 用例 - backend CLAUDE.md: 检索类型段更新为三值 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a400989f64
commit
3d60651491
7 files changed
+182
-16
No files matched your search
@@ -0,0 +1,105 @@
|
||||
"""researcher_node:检索类型 `knowledge_base`(知识库检索)= 直连内网 ES 接口。
|
||||
|
||||
弱模型识别不了技能时用这条路:不编排任何技能、不跑通用 provider,
|
||||
直接调 ai_writing.intranet_search_url 内网知识库 ES 接口检索。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.agents.ai_writing.nodes import researcher as R
|
||||
from deerflow.agents.ai_writing.search.base import SearchResult
|
||||
|
||||
|
||||
class _FakeIntranet:
|
||||
instances: list = []
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.errors = []
|
||||
self.raw_responses = []
|
||||
self.kw = kw
|
||||
_FakeIntranet.instances.append(self)
|
||||
|
||||
async def search(self, query, max_results=5, **_):
|
||||
return [SearchResult(
|
||||
title=f"内网-{query}", content="内网正文", source="ES",
|
||||
published_at="", url=f"http://es/{query}", source_type="intranet",
|
||||
)]
|
||||
|
||||
|
||||
class _SkillProviderShouldNotRun:
|
||||
"""knowledge_base 模式不应触碰通用 provider —— 调到就让测试爆。"""
|
||||
errors: list = []
|
||||
raw_responses: list = []
|
||||
|
||||
async def search(self, query, max_results=5, **_): # pragma: no cover - 不应被调用
|
||||
raise AssertionError("knowledge_base 模式不应调用通用 provider / 技能检索")
|
||||
|
||||
|
||||
def _patch(monkeypatch, intranet_url="http://es/api"):
|
||||
_FakeIntranet.instances = []
|
||||
cfg = {
|
||||
"max_materials": 20,
|
||||
"intranet_search_url": intranet_url,
|
||||
"intranet_knowledge_base": "samples",
|
||||
}
|
||||
monkeypatch.setattr(R, "get_ai_writing_config", lambda: cfg)
|
||||
monkeypatch.setattr(R, "use_builtin_agent", lambda *_: False)
|
||||
monkeypatch.setattr(R, "safe_get_stream_writer", lambda: (lambda *a, **k: None))
|
||||
monkeypatch.setattr(R, "use_skill_search_agent", lambda: False)
|
||||
|
||||
async def fake_llm_json(*a, **k):
|
||||
return {"keywords": ["特朗普竞选"]}
|
||||
|
||||
monkeypatch.setattr(R, "llm_json", fake_llm_json)
|
||||
# knowledge_base 模式不该加载/编排技能,但仍打桩以防意外路径
|
||||
monkeypatch.setattr(R, "_load_configured_skills",
|
||||
lambda: [SimpleNamespace(name="knowledge-search-v2", description="内网检索")])
|
||||
monkeypatch.setattr(R, "get_search_provider", lambda _cfg: _SkillProviderShouldNotRun())
|
||||
monkeypatch.setattr(
|
||||
"deerflow.agents.ai_writing.search.intranet_stub.IntranetSearchProvider",
|
||||
_FakeIntranet,
|
||||
)
|
||||
|
||||
|
||||
def _state(material_source="knowledge_base"):
|
||||
return {
|
||||
"user_intent": "分析特朗普近期动向",
|
||||
"article_type": "政治分析",
|
||||
"target_audience": "普通读者",
|
||||
"keyword_count": 1,
|
||||
"material_source": material_source,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_knowledge_base_hits_intranet_directly(monkeypatch):
|
||||
_patch(monkeypatch)
|
||||
out = await R.researcher_node(_state(), {"configurable": {}})
|
||||
mats = out["material_package"]["materials"]
|
||||
assert mats, "知识库检索应返回内网素材"
|
||||
assert all(m["source_type"] == "intranet" for m in mats)
|
||||
assert mats[0]["title"] == "内网-特朗普竞选"
|
||||
assert _FakeIntranet.instances, "应实例化 IntranetSearchProvider 直连内网接口"
|
||||
# 走素材确认暂停点
|
||||
assert out["status"] == "awaiting_material_confirm"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_knowledge_base_no_url_empty(monkeypatch):
|
||||
"""未配置内网接口地址:无素材,且不实例化 provider。"""
|
||||
_patch(monkeypatch, intranet_url="")
|
||||
out = await R.researcher_node(_state(), {"configurable": {}})
|
||||
assert out["material_package"]["materials"] == []
|
||||
assert _FakeIntranet.instances == [], "无地址时不应实例化内网 provider"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_knowledge_base_does_not_orchestrate_skills(monkeypatch):
|
||||
"""knowledge_base 不编排技能:通用 provider 一旦被调用就会 raise。"""
|
||||
_patch(monkeypatch)
|
||||
# 不抛异常即证明没走 _SkillProviderShouldNotRun
|
||||
out = await R.researcher_node(_state(), {"configurable": {}})
|
||||
assert out["material_package"]["materials"]
|
||||
Reference in new issue
Block a user