feat: 笔记本 Studio 模块 — RAG 聊天、引用弹窗、侧边栏工作室
- 新增 Studio 笔记本列表页与详情页(RAG 流式聊天、历史线程切换) - 修复 streamSubgraphs: true 使 AI 流式消息正常展示 - 引用圆圈 [[cite:N]] 渲染 + 点击弹窗显示文档 chunk 内容 - 侧边栏「工作室」区域支持折叠/展开,笔记本过滤 is_owner=true - 后端 lead_agent 支持 notebook_source_ids / notebook_token 参数 - 线程 metadata 改为 thread_type=notebook,侧边栏过滤同步 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d738c13e9e
commit
ff8eb930f7
17 files changed
+1325
-12
No files matched your search
+18
-8
@@ -466,16 +466,24 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
)
|
||||
|
||||
# Default lead agent (unchanged behavior)
|
||||
is_notebook_mode = bool(cfg.get("notebook_search_space_id"))
|
||||
notebook_source_ids: list[int] = cfg.get("notebook_source_ids") or []
|
||||
default_tools = get_available_tools(
|
||||
model_name=model_name,
|
||||
groups=agent_config.tool_groups if agent_config else None,
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
app_config=resolved_app_config,
|
||||
)
|
||||
if is_notebook_mode:
|
||||
from deerflow.tools.builtins.notebook_search_tool import notebook_search_tool
|
||||
|
||||
default_tools = [notebook_search_tool] + default_tools
|
||||
|
||||
return create_agent(
|
||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, app_config=resolved_app_config),
|
||||
tools=get_available_tools(
|
||||
model_name=model_name,
|
||||
groups=agent_config.tool_groups if agent_config else None,
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
app_config=resolved_app_config,
|
||||
),
|
||||
tools=default_tools,
|
||||
middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config),
|
||||
system_prompt=apply_prompt_template(
|
||||
subagent_enabled=subagent_enabled,
|
||||
@@ -487,6 +495,8 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
available_skills=(set(agent_config.skills or []) if agent_config else None),
|
||||
app_config=resolved_app_config,
|
||||
memory_injection_enabled=memory_injection_enabled,
|
||||
is_notebook_mode=is_notebook_mode,
|
||||
notebook_source_ids=notebook_source_ids,
|
||||
),
|
||||
state_schema=ThreadState,
|
||||
)
|
||||
+45
-1
@@ -868,6 +868,8 @@ def apply_prompt_template(
|
||||
is_bootstrap: bool = False,
|
||||
app_config: AppConfig | None = None,
|
||||
memory_injection_enabled: bool = True,
|
||||
is_notebook_mode: bool = False,
|
||||
notebook_source_ids: list[int] | None = None,
|
||||
) -> str:
|
||||
storage_agent_id = agent_id if agent_id is not None else agent_name
|
||||
|
||||
@@ -955,4 +957,46 @@ Writing mode is enabled for this run.
|
||||
acp_section=acp_and_mounts_section,
|
||||
)
|
||||
|
||||
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
||||
prompt = prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
||||
|
||||
if is_notebook_mode:
|
||||
source_ids = notebook_source_ids or []
|
||||
if source_ids:
|
||||
source_line = f"当前检索范围:{len(source_ids)} 个来源(IDs: {', '.join(str(i) for i in source_ids)})"
|
||||
else:
|
||||
source_line = "当前检索范围:全部来源"
|
||||
|
||||
prompt += f"""
|
||||
|
||||
<notebook_mode>
|
||||
## 角色
|
||||
你是一个笔记本 AI 助手,**只能**基于笔记本文档来源回答用户问题,禁止使用任何其他知识。
|
||||
|
||||
## 可用知识来源
|
||||
{source_line}
|
||||
|
||||
## 强制工作流程(每次用户提问都必须严格执行,无例外)
|
||||
1. **第一步(必须)**:立即调用 `search_notebook_sources` 工具,`query` 填写用户问题或精炼的检索关键词。
|
||||
- 无论问题多简单,都必须先检索,不得跳过。
|
||||
- 若检索结果不足,可再次调用不同 query 补充检索。
|
||||
2. **第二步**:仅基于检索到的内容组织回答,**不得使用训练知识或推断**。
|
||||
3. **第三步**:若检索结果中确实无相关内容,明确告知用户"暂未在来源中找到相关信息"。
|
||||
|
||||
## 引用格式(强制)
|
||||
- 每个事实/句子后立即插入 `[[cite:<chunk_id>]]` 标记,chunk_id 来自检索结果中每个文本块的 id 字段。
|
||||
- 多个来源支持同一句话时连续插入:`[[cite:101]][[cite:102]]`
|
||||
- 引用标记紧跟被引用内容末尾,不加空格,不单独成行。
|
||||
|
||||
## 示例
|
||||
用户:合同中违约金是多少?
|
||||
(调用 search_notebook_sources query="违约金",返回 chunk_id=101 的结果)
|
||||
回答:根据合同第五条,违约金为合同总额的 0.5%[[cite:101]],乙方拒绝收货视为违约[[cite:101]]。
|
||||
|
||||
## 绝对禁止
|
||||
- 未调用 `search_notebook_sources` 就直接回答
|
||||
- 引用不在检索结果中的 chunk_id
|
||||
- 省略引用标记
|
||||
- 使用笔记本来源之外的知识
|
||||
</notebook_mode>"""
|
||||
|
||||
return prompt
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"""笔记本 RAG 检索工具。
|
||||
|
||||
从 LangGraph configurable 中读取 notebook_search_space_id / notebook_source_ids /
|
||||
notebook_token,调用外部检索接口返回相关文本块,供 LLM 在笔记本模式下引用回答。
|
||||
|
||||
API base URL 通过环境变量 DEER_FLOW_NOTEBOOK_API_BASE 配置。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from langchain.tools import tool
|
||||
from langgraph.config import get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SEARCH_PATH = "/api/v1/source/mentioned-documents/search"
|
||||
|
||||
|
||||
def _notebook_api_base() -> str:
|
||||
return (os.getenv("DEER_FLOW_NOTEBOOK_API_BASE") or "").rstrip("/")
|
||||
|
||||
|
||||
@tool("search_notebook_sources", parse_docstring=True)
|
||||
def notebook_search_tool(query: str, top_k: int = 10) -> str:
|
||||
"""Search the notebook's knowledge sources to find relevant content.
|
||||
|
||||
Call this tool for EVERY user question. After retrieving results, cite
|
||||
sources inline using [[cite:<document_id>]] markers immediately after
|
||||
each claim, e.g. "违约金为0.5%[[cite:101]]".
|
||||
|
||||
Args:
|
||||
query: The user's question or a refined search query.
|
||||
top_k: Number of top chunks to retrieve (default 10).
|
||||
"""
|
||||
base = _notebook_api_base()
|
||||
if not base:
|
||||
return "Notebook API base URL not configured (DEER_FLOW_NOTEBOOK_API_BASE)."
|
||||
|
||||
cfg = get_config().get("configurable", {})
|
||||
search_space_id: int | None = cfg.get("notebook_search_space_id")
|
||||
source_ids: list[int] = cfg.get("notebook_source_ids") or []
|
||||
token: str = cfg.get("notebook_token") or ""
|
||||
|
||||
if not search_space_id:
|
||||
return "No notebook search space configured."
|
||||
|
||||
logger.info("notebook_search_tool query=%r source_ids=%s search_space_id=%s", query, source_ids, search_space_id)
|
||||
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{base}{_SEARCH_PATH}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"query": query,
|
||||
"source_ids": [682],
|
||||
"search_space_id": search_space_id,
|
||||
"top_k": top_k,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
content = data.get("content") if isinstance(data, dict) else None
|
||||
return content if content else "No relevant content found."
|
||||
except Exception as e:
|
||||
logger.warning("notebook_search_tool failed: %s", e)
|
||||
return f"Search failed: {e}"
|
||||
Reference in new issue
Block a user