feat: 报告结构支持检索方向/来源与智能新增,并修复协助卡停轮与宿主跳转

为深度研究报告结构增加检索方向与技能来源、对话式智能新增页与详情排版;澄清中间件在 after_model 可靠结束本轮,避免弱模型出卡后继续生成;任务按钮补充 host 跳转与相关深链能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-22 17:46:06 +08:00
1 parent a5a9c952e8
commit 51afebb394
67 files changed
+4511 -699

No files matched your search

@@ -11,11 +11,12 @@ from deerflow.agents.middlewares.memory_middleware import MemoryMiddleware
from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMiddleware
from deerflow.agents.middlewares.rag_citation_middleware import RagCitationMiddleware
from deerflow.agents.middlewares.setup_agent_approval_middleware import SetupAgentApprovalMiddleware
from deerflow.agents.middlewares.setup_report_structure_approval_middleware import SetupReportStructureApprovalMiddleware
from deerflow.agents.middlewares.setup_writing_approval_middleware import SetupWritingApprovalMiddleware
from deerflow.agents.middlewares.subagent_limit_middleware import SubagentLimitMiddleware
from deerflow.agents.middlewares.summarization_middleware import BeforeSummarizationHook, DeerFlowSummarizationMiddleware
from deerflow.agents.middlewares.task_external_context_middleware import TaskExternalContextMiddleware
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
from deerflow.agents.middlewares.subagent_limit_middleware import SubagentLimitMiddleware
from deerflow.agents.middlewares.summarization_middleware import BeforeSummarizationHook, DeerFlowSummarizationMiddleware
from deerflow.agents.middlewares.task_external_context_middleware import TaskExternalContextMiddleware
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
from deerflow.agents.middlewares.todo_middleware import TodoMiddleware
from deerflow.agents.middlewares.token_usage_middleware import TokenUsageMiddleware
from deerflow.agents.middlewares.tool_error_handling_middleware import build_lead_runtime_middlewares
@@ -31,6 +32,7 @@ logger = logging.getLogger(__name__)
# 写作配置 Q&A 现在「先生成大纲并与用户多轮确认 → 再提交配置」,需要更多来回,故上限放宽。
# 仍是兜底安全阀:到顶才用 tool_choice 强制提交,正常流程由用户确认大纲后自然提交。
WRITING_SETUP_MAX_ROUNDS = 8
REPORT_STRUCTURE_SETUP_MAX_ROUNDS = 8
def _get_runtime_config(config: RunnableConfig) -> dict:
@@ -292,6 +294,7 @@ def _build_middlewares(
is_plan_mode = cfg.get("is_plan_mode", False)
is_bootstrap = cfg.get("is_bootstrap", False)
is_writing_setup = cfg.get("is_writing_setup", False)
is_report_structure_setup = cfg.get("is_report_structure_setup", False)
todo_list_middleware = _create_todo_list_middleware(is_plan_mode)
if todo_list_middleware is not None:
middlewares.append(todo_list_middleware)
@@ -319,7 +322,7 @@ def _build_middlewares(
# Add MemoryMiddleware (after TitleMiddleware) — skip in bootstrap mode (agent creation flow) and ephemeral runs
memory_injection_enabled = cfg.get("memory_injection_enabled", True)
if not is_bootstrap and not is_writing_setup and not is_ephemeral:
if not is_bootstrap and not is_writing_setup and not is_report_structure_setup and not is_ephemeral:
middlewares.append(MemoryMiddleware(agent_name=agent_name, memory_config=resolved_app_config.memory, injection_enabled=memory_injection_enabled))
# KnowledgeRagMiddleware — inject knowledge-base recall (phase 2). Gated
@@ -327,16 +330,16 @@ def _build_middlewares(
# to the global ``knowledge.rag_enabled`` config. No-op when knowledge is
# disabled. Skipped in bootstrap/writing-setup/ephemeral like MemoryMiddleware.
knowledge_cfg = getattr(resolved_app_config, "knowledge", None)
if not is_bootstrap and not is_writing_setup and not is_ephemeral and knowledge_cfg is not None and knowledge_cfg.enabled:
from deerflow.agents.middlewares.knowledge_rag_middleware import KnowledgeRagMiddleware
middlewares.append(KnowledgeRagMiddleware(limit=knowledge_cfg.rag_limit, default_enabled=knowledge_cfg.rag_enabled))
# TaskExternalContextMiddleware — inject hidden per-run business context
# (for example rwfx's completed 3q report) into the model request only.
middlewares.append(TaskExternalContextMiddleware())
# Add ViewImageMiddleware only for the NATIVE-vision path (main model
if not is_bootstrap and not is_writing_setup and not is_report_structure_setup and not is_ephemeral and knowledge_cfg is not None and knowledge_cfg.enabled:
from deerflow.agents.middlewares.knowledge_rag_middleware import KnowledgeRagMiddleware
middlewares.append(KnowledgeRagMiddleware(limit=knowledge_cfg.rag_limit, default_enabled=knowledge_cfg.rag_enabled))
# TaskExternalContextMiddleware — inject hidden per-run business context
# (for example rwfx's completed 3q report) into the model request only.
middlewares.append(TaskExternalContextMiddleware())
# Add ViewImageMiddleware only for the NATIVE-vision path (main model
# ingests raw image data). In delegated mode the view_image tool already
# returns a text description and stores nothing in `viewed_images`, so the
# middleware would otherwise inject a spurious "No images have been viewed."
@@ -374,6 +377,7 @@ def _build_middlewares(
middlewares.append(SetupAgentApprovalMiddleware())
middlewares.append(SetupWritingApprovalMiddleware())
middlewares.append(SetupReportStructureApprovalMiddleware())
# SkillStopMiddleware — intercepts skill invocations and halts execution
# when a configured skill is about to run. Request-level options
@@ -421,7 +425,7 @@ def make_lead_agent(config: RunnableConfig):
def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
# Lazy import to avoid circular dependency
from deerflow.tools import get_available_tools
from deerflow.tools.builtins import ask_clarification_tool, setup_agent, setup_writing
from deerflow.tools.builtins import ask_clarification_tool, setup_agent, setup_report_structure, setup_writing
cfg = _get_runtime_config(config)
resolved_app_config = app_config
@@ -443,8 +447,12 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
is_writing_setup = cfg.get("is_writing_setup", False)
writing_setup_article_types = cfg.get("valid_article_types") if is_writing_setup else None
# 快捷模式:跳过逐项选项澄清,给标题直接出大纲(见 build_writing_setup_prompt)。
writing_setup_quick = bool(cfg.get("writing_setup_quick", False)) if is_writing_setup else False
writing_sample_context = cfg.get("writing_sample_context") if is_writing_setup else None
writing_setup_quick = bool(cfg.get("writing_setup_quick", False)) if is_writing_setup else False
writing_sample_context = cfg.get("writing_sample_context") if is_writing_setup else None
# 报告结构智能新增:与 is_writing_setup 同构的最小化 bootstrap。
is_report_structure_setup = cfg.get("is_report_structure_setup", False)
report_structure_setup_skills = cfg.get("valid_retrieval_skills") if is_report_structure_setup else None
report_structure_setup_quick = bool(cfg.get("writing_setup_quick", False)) if is_report_structure_setup else False
memory_injection_enabled = bool(cfg.get("memory_injection_enabled", True))
rag_mode_enabled = bool(cfg.get("rag_mode_enabled", False))
writing_mode = bool(cfg.get("writing_mode", False))
@@ -455,7 +463,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
requested_excluded_tools = cfg.get("excluded_tools") or None
try:
agent_config = load_agent_config(agent_id) if not is_bootstrap and not is_writing_setup else None
agent_config = load_agent_config(agent_id) if not is_bootstrap and not is_writing_setup and not is_report_structure_setup else None
except FileNotFoundError:
logger.warning("Agent directory not found for '%s'; running with default config", agent_id)
agent_config = None
@@ -499,6 +507,13 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
# Default lead agent keeps existing behavior (all enabled skills).
runtime_available_skills = None
from deerflow.agents.deep_research.retrieval import merge_collector_skill_allowlist, runtime_extra_skills
runtime_available_skills = merge_collector_skill_allowlist(
agent_id, runtime_available_skills, runtime_extra_skills(cfg)
)
prompt_available_skills = set(runtime_available_skills) if runtime_available_skills is not None else None
config["metadata"].update(
{
"agent_id": agent_id,
@@ -533,13 +548,46 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
*_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
WritingSetupRoundCapMiddleware(max_rounds=WRITING_SETUP_MAX_ROUNDS),
],
system_prompt=build_writing_setup_prompt(
writing_setup_article_types,
quick_mode=writing_setup_quick,
sample_context=writing_sample_context,
),
state_schema=ThreadState,
)
system_prompt=build_writing_setup_prompt(
writing_setup_article_types,
quick_mode=writing_setup_quick,
sample_context=writing_sample_context,
),
state_schema=ThreadState,
)
if is_report_structure_setup:
from deerflow.agents.lead_agent.prompt import build_report_structure_setup_prompt
from deerflow.agents.middlewares.writing_setup_round_cap_middleware import WritingSetupRoundCapMiddleware
setup_tools = (
[setup_report_structure]
if report_structure_setup_quick
else [setup_report_structure, ask_clarification_tool]
)
return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
tools=setup_tools,
middleware=[
*_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
WritingSetupRoundCapMiddleware(
max_rounds=REPORT_STRUCTURE_SETUP_MAX_ROUNDS,
tool_name="setup_report_structure",
force_instruction=(
f"【系统强制】意图澄清已达 {REPORT_STRUCTURE_SETUP_MAX_ROUNDS} 轮上限。"
"现在必须立即调用 setup_report_structure 提交报告结构配置,禁止再向用户提任何问题。"
"缺失或不确定的字段一律用合理默认值:"
"title 根据对话主题拟一个短标题、name 用一句话描述、structure_mode 默认 adaptive、"
"retrieval_directions / retrieval_skills 不确定就留空、content 必须给出一份 Markdown 大纲。"
),
),
],
system_prompt=build_report_structure_setup_prompt(
report_structure_setup_skills,
quick_mode=report_structure_setup_quick,
),
state_schema=ThreadState,
)
if is_bootstrap:
# Special bootstrap agent with minimal prompt for initial custom agent creation flow
@@ -600,7 +648,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
agent_name=agent_display_name,
subagent_enabled=subagent_enabled,
max_concurrent_subagents=max_concurrent_subagents,
available_skills=(set(agent_config.skills or []) if agent_config else None),
available_skills=prompt_available_skills,
app_config=resolved_app_config,
rag_mode_enabled=rag_mode_enabled,
writing_mode=writing_mode,
@@ -614,7 +662,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
agent_name=agent_display_name,
writing_mode=writing_mode,
writing_artifact_path=writing_artifact_path,
available_skills=(set(agent_config.skills or []) if agent_config else None),
available_skills=prompt_available_skills,
app_config=resolved_app_config,
memory_injection_enabled=memory_injection_enabled,
rag_mode_enabled=rag_mode_enabled,