import hashlib import logging from langchain.agents import create_agent from langchain.agents.middleware import AgentMiddleware from langchain_core.runnables import RunnableConfig from deerflow.agents.lead_agent.prompt import apply_prompt_template from deerflow.agents.middlewares.clarification_middleware import ClarificationMiddleware from deerflow.agents.middlewares.forced_research_middleware import ( FORCED_RESEARCH_AGENT_ID, ForcedResearchMiddleware, filter_forced_research_tools, ) from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware 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.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 from deerflow.agents.middlewares.view_image_middleware import ViewImageMiddleware from deerflow.agents.thread_state import ThreadState from deerflow.config.agents_config import load_agent_config, load_agent_soul, validate_agent_id from deerflow.config.app_config import AppConfig, get_app_config from deerflow.models import create_chat_model logger = logging.getLogger(__name__) # 写作配置 Q&A:意图澄清最多几轮(一条用户消息 = 一轮),到顶强制提交配置卡。 # 写作配置 Q&A 现在「先生成大纲并与用户多轮确认 → 再提交配置」,需要更多来回,故上限放宽。 # 仍是兜底安全阀:到顶才用 tool_choice 强制提交,正常流程由用户确认大纲后自然提交。 WRITING_SETUP_MAX_ROUNDS = 8 REPORT_STRUCTURE_SETUP_MAX_ROUNDS = 8 _ES_QUERY_ROUTING_OPEN_TAG = '' _SKILLS_BLOCKED_BY_EXCLUDED_TOOL: dict[str, frozenset[str]] = { # 内置 tool 与历史 custom skill 目录存在两种拼写;禁用编排工具时两者都必须从系统提示和 # runtime skill allowlist 中消失,防止误配的席位仍看到「加载编排技能」。 "agent_orchestration": frozenset({"agent_orchestration", "agent-orchestration"}), } _ROUNDTABLE_COORDINATOR_AGENT_ID = "roundtable-coordinator" _NON_COORDINATOR_ORCHESTRATION_BOUNDARY = """ 你不是多智能体圆桌总控,因此没有编排、派活或调度其它智能体的权限。 不得加载、读取、检索或调用 agent_orchestration / agent-orchestration, 不得声称准备使用编排技能;请直接完成当前问答或你的本职任务。 """.strip() def _enforce_coordinator_only_tool_exclusions( agent_id: str | None, excluded_tools: list[str] | None, ) -> list[str] | None: """Make orchestration unavailable to every agent except the fixed roundtable coordinator.""" if agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID: return excluded_tools effective = list(excluded_tools or []) if "agent_orchestration" not in effective: effective.append("agent_orchestration") return effective def _append_orchestration_capability_boundary(system_prompt: str, agent_id: str | None) -> str: """Reinforce the physical tool/skill gate in every non-coordinator system prompt.""" if agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID: return system_prompt return f"{system_prompt}\n\n{_NON_COORDINATOR_ORCHESTRATION_BOUNDARY}" def _filter_available_skills_for_excluded_tools( available_skills: list[str] | None, excluded_tools: list[str] | None, ) -> list[str] | None: """让工具级禁用同步收紧 skill allowlist,必要时把「全部」展开为安全列表。""" if not excluded_tools: return available_skills excluded = {name.strip().lower() for name in excluded_tools if name} blocked = { skill_name for tool_name, skill_names in _SKILLS_BLOCKED_BY_EXCLUDED_TOOL.items() if tool_name in excluded for skill_name in skill_names } if not blocked: return available_skills if available_skills is None: # ``None`` 原本表示「全部启用技能可见」,无法表达「除了总控专属技能之外的全部」。 # 此时显式展开存储中的启用技能;扫描失败则 fail closed,避免角色隔离失效。 try: from deerflow.skills.storage import get_or_new_skill_storage available_skills = [ skill.name for skill in get_or_new_skill_storage().load_skills(enabled_only=True) ] except Exception: # noqa: BLE001 logger.warning("Failed to expand skills while applying excluded-tool boundary", exc_info=True) available_skills = [] return [name for name in available_skills if name.strip().lower() not in blocked] def _log_es_query_routing_registration( *, system_prompt: str, app_config: AppConfig, agent_id: str | None, available_skills: set[str] | None, ) -> None: """Log whether the exact configured ES routing block reached this Q&A prompt.""" routing = app_config.skills.es_query_routing configured_prompt = (routing.prompt or "").strip() expected_block = ( f"{_ES_QUERY_ROUTING_OPEN_TAG}\n{configured_prompt}\n" if configured_prompt else "" ) eligible = available_skills is None or "es_query" in available_skills registered = bool(expected_block) and expected_block in system_prompt fingerprint = ( hashlib.sha256(configured_prompt.encode("utf-8")).hexdigest()[:12] if configured_prompt else "-" ) log = logger.info if registered or not routing.enabled or not eligible else logger.warning log( "ES query routing prompt registration: agent_id=%s enabled=%s eligible=%s " "registered=%s prompt_chars=%d prompt_sha256=%s", agent_id or "default", routing.enabled, eligible, registered, len(configured_prompt), fingerprint, ) def _get_runtime_config(config: RunnableConfig) -> dict: """Merge legacy configurable options with LangGraph runtime context.""" cfg = dict(config.get("configurable", {}) or {}) context = config.get("context", {}) or {} if isinstance(context, dict): cfg.update(context) return cfg def _resolve_model_name(requested_model_name: str | None = None, *, app_config: AppConfig | None = None) -> str: """Resolve a runtime model name safely, falling back to default if invalid. Returns None if no models are configured.""" app_config = app_config or get_app_config() default_model_name = app_config.models[0].name if app_config.models else None if default_model_name is None: raise ValueError("No chat models are configured. Please configure at least one model in config.yaml.") if requested_model_name and app_config.get_model_config(requested_model_name): return requested_model_name if requested_model_name and requested_model_name != default_model_name: logger.warning(f"Model '{requested_model_name}' not found in config; fallback to default model '{default_model_name}'.") return default_model_name def _create_summarization_middleware(*, app_config: AppConfig | None = None, force_enabled: bool = False) -> DeerFlowSummarizationMiddleware | None: """Create and configure the summarization middleware from config. Args: force_enabled: When True, build the middleware regardless of the config ``summarization.enabled`` flag. The per-request 上下文压缩 toggle (chat input box, default off) drives this — the config file no longer owns the on/off switch, only the tuning (trigger/keep/...). """ resolved_app_config = app_config or get_app_config() config = resolved_app_config.summarization if not (config.enabled or force_enabled): return None # Prepare trigger parameter trigger = None if config.trigger is not None: if isinstance(config.trigger, list): trigger = [t.to_tuple() for t in config.trigger] else: trigger = config.trigger.to_tuple() # Prepare keep parameter keep = config.keep.to_tuple() # Prepare model parameter. # Bind "middleware:summarize" tag so RunJournal identifies these LLM calls # as middleware rather than lead_agent (SummarizationMiddleware is a # LangChain built-in, so we tag the model at creation time). if config.model_name: model = create_chat_model(name=config.model_name, thinking_enabled=False, app_config=resolved_app_config) else: model = create_chat_model(thinking_enabled=False, app_config=resolved_app_config) model = model.with_config(tags=["middleware:summarize"]) # Prepare kwargs kwargs = { "model": model, "trigger": trigger, "keep": keep, } if config.trim_tokens_to_summarize is not None: kwargs["trim_tokens_to_summarize"] = config.trim_tokens_to_summarize if config.summary_prompt is not None: kwargs["summary_prompt"] = config.summary_prompt # Memory V2:不再有后台抽取队列,无需 summarization 前的 flush hook。 hooks: list[BeforeSummarizationHook] = [] # The logic below relies on two assumptions holding true: this factory is # the sole entry point for DeerFlowSummarizationMiddleware, and the runtime # config is not expected to change after startup. skills_container_path = resolved_app_config.skills.container_path or "/mnt/skills" return DeerFlowSummarizationMiddleware( **kwargs, skills_container_path=skills_container_path, skill_file_read_tool_names=config.skill_file_read_tool_names, before_summarization=hooks, preserve_recent_skill_count=config.preserve_recent_skill_count, preserve_recent_skill_tokens=config.preserve_recent_skill_tokens, preserve_recent_skill_tokens_per_skill=config.preserve_recent_skill_tokens_per_skill, ) def _create_todo_list_middleware(is_plan_mode: bool) -> TodoMiddleware | None: """Create and configure the TodoList middleware. Args: is_plan_mode: Whether to enable plan mode with TodoList middleware. Returns: TodoMiddleware instance if plan mode is enabled, None otherwise. """ if not is_plan_mode: return None # Custom prompts matching DeerFlow's style system_prompt = """ You have access to the `write_todos` tool to help you manage and track complex multi-step objectives. **CRITICAL RULES:** - Mark todos as completed IMMEDIATELY after finishing each step - do NOT batch completions - Keep EXACTLY ONE task as `in_progress` at any time (unless tasks can run in parallel) - Update the todo list in REAL-TIME as you work - this gives users visibility into your progress - DO NOT use this tool for simple tasks (< 3 steps) - just complete them directly **When to Use:** This tool is designed for complex objectives that require systematic tracking: - Complex multi-step tasks requiring 3+ distinct steps - Non-trivial tasks needing careful planning and execution - User explicitly requests a todo list - User provides multiple tasks (numbered or comma-separated list) - The plan may need revisions based on intermediate results **When NOT to Use:** - Single, straightforward tasks - Trivial tasks (< 3 steps) - Purely conversational or informational requests - Simple tool calls where the approach is obvious **Best Practices:** - Break down complex tasks into smaller, actionable steps - Use clear, descriptive task names - Remove tasks that become irrelevant - Add new tasks discovered during implementation - Don't be afraid to revise the todo list as you learn more **Task Management:** Writing todos takes time and tokens - use it when helpful for managing complex problems, not for simple requests. """ tool_description = """Use this tool to create and manage a structured task list for complex work sessions. **IMPORTANT: Only use this tool for complex tasks (3+ steps). For simple requests, just do the work directly.** ## When to Use Use this tool in these scenarios: 1. **Complex multi-step tasks**: When a task requires 3 or more distinct steps or actions 2. **Non-trivial tasks**: Tasks requiring careful planning or multiple operations 3. **User explicitly requests todo list**: When the user directly asks you to track tasks 4. **Multiple tasks**: When users provide a list of things to be done 5. **Dynamic planning**: When the plan may need updates based on intermediate results ## When NOT to Use Skip this tool when: 1. The task is straightforward and takes less than 3 steps 2. The task is trivial and tracking provides no benefit 3. The task is purely conversational or informational 4. It's clear what needs to be done and you can just do it ## How to Use 1. **Starting a task**: Mark it as `in_progress` BEFORE beginning work 2. **Completing a task**: Mark it as `completed` IMMEDIATELY after finishing 3. **Updating the list**: Add new tasks, remove irrelevant ones, or update descriptions as needed 4. **Multiple updates**: You can make several updates at once (e.g., complete one task and start the next) ## Task States - `pending`: Task not yet started - `in_progress`: Currently working on (can have multiple if tasks run in parallel) - `completed`: Task finished successfully ## Task Completion Requirements **CRITICAL: Only mark a task as completed when you have FULLY accomplished it.** Never mark a task as completed if: - There are unresolved issues or errors - Work is partial or incomplete - You encountered blockers preventing completion - You couldn't find necessary resources or dependencies - Quality standards haven't been met If blocked, keep the task as `in_progress` and create a new task describing what needs to be resolved. ## Best Practices - Create specific, actionable items - Break complex tasks into smaller, manageable steps - Use clear, descriptive task names - Update task status in real-time as you work - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) - Remove tasks that are no longer relevant - **IMPORTANT**: When you write the todo list, mark your first task(s) as `in_progress` immediately - **IMPORTANT**: Unless all tasks are completed, always have at least one task `in_progress` to show progress Being proactive with task management demonstrates thoroughness and ensures all requirements are completed successfully. **Remember**: If you only need a few tool calls to complete a task and it's clear what to do, it's better to just do the task directly and NOT use this tool at all. """ return TodoMiddleware(system_prompt=system_prompt, tool_description=tool_description) # ThreadDataMiddleware must be before SandboxMiddleware to ensure thread_id is available # UploadsMiddleware should be after ThreadDataMiddleware to access thread_id # DanglingToolCallMiddleware patches missing ToolMessages before model sees the history # SummarizationMiddleware should be early to reduce context before other processing # TodoListMiddleware should be before ClarificationMiddleware to allow todo management # TitleMiddleware generates title after first exchange # MemoryMiddleware queues conversation for memory update (after TitleMiddleware) # ViewImageMiddleware should be before ClarificationMiddleware to inject image details before LLM # ToolErrorHandlingMiddleware should be before ClarificationMiddleware to convert tool exceptions to ToolMessages # ClarificationMiddleware should be last to intercept clarification requests after model calls def _build_middlewares( config: RunnableConfig, model_name: str | None, agent_name: str | None = None, custom_middlewares: list[AgentMiddleware] | None = None, *, app_config: AppConfig | None = None, ): """Build middleware chain based on runtime configuration. Args: config: Runtime configuration containing configurable options like is_plan_mode. agent_name: If provided, MemoryMiddleware will use per-agent memory storage. custom_middlewares: Optional list of custom middlewares to inject into the chain. Returns: List of middleware instances. """ resolved_app_config = app_config or get_app_config() cfg = _get_runtime_config(config) middlewares = build_lead_runtime_middlewares(app_config=resolved_app_config, lazy_init=True) # Inject the admin prompt prefix into the model request (kept out of the # persisted/displayed message so the user never sees it). middlewares.append(PromptPrefixMiddleware()) if cfg.get("rag_mode_enabled", False): middlewares.append(RagCitationMiddleware()) # Add summarization middleware. The on/off switch now lives in the chat UI # (上下文压缩 toggle, default off) and arrives as ``summarization_enabled`` in # the runtime config; the config file only supplies tuning. force_enabled # lets the per-request toggle turn it on even when config.enabled is false, # while non-UI paths (channels/scheduled) still fall back to config.enabled. summarization_requested = cfg.get("summarization_enabled", False) summarization_middleware = _create_summarization_middleware(app_config=resolved_app_config, force_enabled=summarization_requested) if summarization_middleware is not None: middlewares.append(summarization_middleware) # Add TodoList middleware if plan mode is enabled 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) # Add TokenUsageMiddleware when token_usage tracking is enabled if resolved_app_config.token_usage.enabled: middlewares.append(TokenUsageMiddleware()) # Add TitleMiddleware — but skip it for scheduled runs. Each scheduled run # executes on a throwaway thread whose title is never shown to a user, and # generating one would fire an extra LLM call on the *default* model # (config.title.model_name is usually unset → models[0]), which is not the # model the task selected. Skipping it keeps scheduled/HTML runs on exactly # the chosen model. if not cfg.get("is_scheduled_run", False): middlewares.append(TitleMiddleware(app_config=resolved_app_config)) # Ephemeral runs (e.g. the open Q&A API /api/open/chat) must NOT touch any # server-side per-user state: no memory recall/retain, no knowledge-base # recall/auto-ingest. Those runs use the shared "default" user bucket, so # persisting their conversations there would both store records the caller # explicitly asked NOT to keep and leak content across unrelated callers. # Skip both write-capable middlewares entirely. is_ephemeral = cfg.get("ephemeral", False) # 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_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 # per-conversation by the ``knowledge_rag_enabled`` runtime flag, defaulting # 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_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." # message. Use the resolved runtime model_name to avoid stale config values. from deerflow.models.vision import is_delegated_vision, vision_enabled if vision_enabled(model_name, app_config=resolved_app_config) and not is_delegated_vision(app_config=resolved_app_config): middlewares.append(ViewImageMiddleware()) # Add DeferredToolFilterMiddleware to hide deferred tool schemas from model binding if resolved_app_config.tool_search.enabled: from deerflow.agents.middlewares.deferred_tool_filter_middleware import DeferredToolFilterMiddleware middlewares.append(DeferredToolFilterMiddleware()) # Add SubagentLimitMiddleware to truncate excess parallel task calls subagent_enabled = cfg.get("subagent_enabled", False) if subagent_enabled: max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3) middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents)) # SkillReviewMiddleware — trigger background skill review after each turn. # Gated by skill_evolution.enabled + skill_evolution.review_worker.enabled. if resolved_app_config.skill_evolution.enabled and resolved_app_config.skill_evolution.review_worker.enabled: from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware middlewares.append(SkillReviewMiddleware()) # PositionArtifactCapMiddleware — position-roundtable seats (and summary) # may only emit N outputs write_file calls per user turn. Action-plan skips # the context flag so it can still write report + JSON. from deerflow.agents.middlewares.position_artifact_cap_middleware import PositionArtifactCapMiddleware middlewares.append(PositionArtifactCapMiddleware()) # The built-in forced-research responder has a hard per-turn collection # gate plus temporary-Python-only sandbox IO. This is runtime enforcement, # not merely SOUL prompt steering, so weak models cannot answer before a # successful retrieval/skill-script result exists. if agent_name == FORCED_RESEARCH_AGENT_ID: middlewares.append(ForcedResearchMiddleware()) # 写作模式首轮:允许先聊/检索;只有模型开始写 md 时才改走 deep_research_report。 # 不设 tool_choice——thinking 模式的兼容接口会 400。 writing_mode = bool(cfg.get("writing_mode", False)) writing_artifact_path = cfg.get("writing_artifact_path") if writing_mode else None if writing_mode: from deerflow.tools.builtins.deep_research_report_tool import is_writing_mode_report_path if not is_writing_mode_report_path(writing_artifact_path if isinstance(writing_artifact_path, str) else None): from deerflow.agents.middlewares.writing_mode_report_middleware import WritingModeReportMiddleware middlewares.append(WritingModeReportMiddleware()) # LoopDetectionMiddleware — detect and break repetitive tool call loops middlewares.append(LoopDetectionMiddleware()) # Inject custom middlewares before ClarificationMiddleware if custom_middlewares: middlewares.extend(custom_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 # (skill_stop_names / skill_stop_message) take precedence; static # config in ``app_config.skill_stop`` is the fallback. skill_stop_names = cfg.get("skill_stop_names") skill_stop_message = cfg.get("skill_stop_message") if skill_stop_names: from deerflow.agents.middlewares.skill_stop_middleware import SkillStopMiddleware middlewares.append( SkillStopMiddleware( skill_names=skill_stop_names, skills_root=resolved_app_config.skills.container_path or "/mnt/skills", stop_message=skill_stop_message, stop_on_match=True, ) ) logger.info("SkillStopMiddleware enabled (from request): %s", skill_stop_names) elif resolved_app_config.skill_stop.enabled and resolved_app_config.skill_stop.skill_names: from deerflow.agents.middlewares.skill_stop_middleware import SkillStopMiddleware middlewares.append( SkillStopMiddleware( skill_names=resolved_app_config.skill_stop.skill_names, skills_root=resolved_app_config.skills.container_path or "/mnt/skills", stop_message=resolved_app_config.skill_stop.stop_message, stop_on_match=resolved_app_config.skill_stop.stop_on_match, ) ) logger.info("SkillStopMiddleware enabled (from config): %s", resolved_app_config.skill_stop.skill_names) # ClarificationMiddleware should always be last middlewares.append(ClarificationMiddleware()) return middlewares def make_lead_agent(config: RunnableConfig): """LangGraph graph factory; keep the signature compatible with LangGraph Server.""" runtime_config = _get_runtime_config(config) runtime_app_config = runtime_config.get("app_config") return _make_lead_agent(config, app_config=runtime_app_config or get_app_config()) 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_report_structure, setup_writing cfg = _get_runtime_config(config) resolved_app_config = app_config thinking_enabled = cfg.get("thinking_enabled", True) # Hard "thinking off" switch for internal vLLM/Qwen models whose config only # declares `supports_thinking: true` (no disable shape) — passes through to # create_chat_model(force_disable_thinking=...) so enable_thinking=false is # actually injected. Default off → no behavior change for normal chats. thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False)) reasoning_effort = cfg.get("reasoning_effort", None) requested_model_name: str | None = cfg.get("model_name") or cfg.get("model") is_plan_mode = cfg.get("is_plan_mode", False) disable_tools = bool(cfg.get("disable_tools", False)) subagent_enabled = cfg.get("subagent_enabled", False) max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3) is_bootstrap = cfg.get("is_bootstrap", False) # 写作配置 Q&A:与 is_bootstrap 同构的另一种「最小化 bootstrap」——挂 setup_writing 工具、 # 走写作配置专用 prompt,只负责帮用户把写作表单聊清并提交,不真正开始写作。 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 # 报告结构智能新增:与 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)) writing_artifact_path = cfg.get("writing_artifact_path") if writing_mode else None if not isinstance(writing_artifact_path, str) or not writing_artifact_path.strip(): writing_artifact_path = None # 只有写作模式产物 report.md 才走续写;其它 leftover .md 不得跳过管线。 if writing_artifact_path is not None: from deerflow.tools.builtins.deep_research_report_tool import is_writing_mode_report_path if not is_writing_mode_report_path(writing_artifact_path): writing_artifact_path = None agent_id = validate_agent_id(cfg.get("agent_id") or cfg.get("agent_name")) requested_excluded_tools = _enforce_coordinator_only_tool_exclusions( agent_id, cfg.get("excluded_tools") or None, ) try: 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 agent_display_name = agent_config.name if agent_config else agent_id # Custom agent model from agent config (if any), or None to let _resolve_model_name pick the default agent_model_name = agent_config.model if agent_config and agent_config.model else None # Final model name resolution: request → agent config → global default, with fallback for unknown names model_name = _resolve_model_name(requested_model_name or agent_model_name, app_config=resolved_app_config) model_config = resolved_app_config.get_model_config(model_name) if model_config is None: raise ValueError("No chat model could be resolved. Please configure at least one model in config.yaml or provide a valid 'model_name'/'model' in the request.") if thinking_enabled and not model_config.supports_thinking: logger.warning(f"Thinking mode is enabled but model '{model_name}' does not support it; fallback to non-thinking mode.") thinking_enabled = False logger.info( "Create Agent(%s) -> thinking_enabled: %s, reasoning_effort: %s, model_name: %s, is_plan_mode: %s, subagent_enabled: %s, max_concurrent_subagents: %s, writing_mode: %s", agent_id or "default", thinking_enabled, reasoning_effort, model_name, is_plan_mode, subagent_enabled, max_concurrent_subagents, writing_mode, ) # Inject run metadata for LangSmith trace tagging if "metadata" not in config: config["metadata"] = {} if is_bootstrap: runtime_available_skills: list[str] | None = None # all enabled skills visible in bootstrap elif agent_config: # Custom agents must use an explicit allowlist. If omitted, default to none. runtime_available_skills = list(agent_config.skills or []) else: # 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) ) runtime_available_skills = _filter_available_skills_for_excluded_tools( runtime_available_skills, requested_excluded_tools, ) prompt_available_skills = set(runtime_available_skills) if runtime_available_skills is not None else None config["metadata"].update( { "agent_id": agent_id, "agent_name": agent_display_name or "default", "model_name": model_name or "default", "thinking_enabled": thinking_enabled, "reasoning_effort": reasoning_effort, "is_plan_mode": is_plan_mode, "subagent_enabled": subagent_enabled, "writing_mode": writing_mode, "writing_artifact_path": writing_artifact_path, "tool_groups": agent_config.tool_groups if agent_config else None, "available_skills": runtime_available_skills, } ) if is_writing_setup: # 写作配置 Q&A:最小化 agent —— 只挂 setup_writing(提交配置)+ ask_clarification(弹澄清卡)。 # 关键:绝不给全套工具、绝不复用 lead 超级体提示,否则模型会把「新能源汽车市场分析」之类 # 长得像任务的输入直接做掉,而不是转成写作配置提交。 # 末尾追加 WritingSetupRoundCapMiddleware:澄清最多 3 轮,到顶用 tool_choice 强制提交配置卡。 # 快捷模式:用默认选项、不逐项澄清提问(但仍生成并确认大纲)——直接**不挂** ask_clarification, # 让模型物理上问不了澄清问题,比只靠提示词约束更可靠。 from deerflow.agents.lead_agent.prompt import build_writing_setup_prompt from deerflow.agents.middlewares.writing_setup_round_cap_middleware import WritingSetupRoundCapMiddleware setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, 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=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, ) 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 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=get_available_tools( model_name=model_name, subagent_enabled=subagent_enabled, is_scheduled_run=cfg.get("is_scheduled_run", False), excluded_tools=requested_excluded_tools, allow_agent_orchestration=agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID, app_config=resolved_app_config, ) + [setup_agent], middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), system_prompt=apply_prompt_template( subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, available_skills=None, is_bootstrap=True, app_config=resolved_app_config, memory_injection_enabled=memory_injection_enabled, rag_mode_enabled=rag_mode_enabled, ), state_schema=ThreadState, ) # 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 = ( [] if disable_tools else 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, allow_agent_orchestration=agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID, app_config=resolved_app_config, ) ) if agent_id == FORCED_RESEARCH_AGENT_ID: default_tools = filter_forced_research_tools(default_tools) if is_notebook_mode: from deerflow.tools.builtins.notebook_search_tool import notebook_search_tool default_tools = [notebook_search_tool] + default_tools # 写作模式首轮(尚无 report.md):挂深度研究报告工具,报告由管线生成而非 # 模型手工 write_file。续写轮(已有 report.md)不挂,保持原有「直接编辑 # 该文件」行为。不受 tool_groups/excluded_tools 过滤——它是写作模式合约 # 的核心交付手段。 if writing_mode and not writing_artifact_path: from deerflow.tools.builtins.deep_research_report_tool import deep_research_report_tool default_tools = [*default_tools, deep_research_report_tool] logger.info( "Writing mode first report turn: deep_research_report tool mounted for agent=%s", agent_id, ) # Custom agents are steered ONLY by their own SOUL.md — no generic # super-agent system template and no personal memory injection. We branch to # the agent-only prompt builder whenever a custom agent config carries a # non-empty SOUL.md. The plain default lead agent (no agent_config) and # notebook mode keep the full template. See apply_agent_only_prompt_template. has_agent_soul = bool(agent_config) and bool(load_agent_soul(agent_id)) use_agent_only_prompt = has_agent_soul and not is_notebook_mode if use_agent_only_prompt: from deerflow.agents.lead_agent.prompt import apply_agent_only_prompt_template system_prompt = apply_agent_only_prompt_template( agent_id=agent_id, agent_name=agent_display_name, subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, available_skills=prompt_available_skills, app_config=resolved_app_config, rag_mode_enabled=rag_mode_enabled, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, ) else: system_prompt = apply_prompt_template( subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, agent_id=agent_id, agent_name=agent_display_name, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, available_skills=prompt_available_skills, app_config=resolved_app_config, memory_injection_enabled=memory_injection_enabled, rag_mode_enabled=rag_mode_enabled, is_notebook_mode=is_notebook_mode, notebook_source_ids=notebook_source_ids, is_scheduled_run=bool(cfg.get("is_scheduled_run", False)), ) # 前端可提供当前视觉明暗模式给页面生成类任务。把值放入系统提示而不是 # 指望模型从 ToolRuntime.context 猜测,确保自定义 SOUL 也能稳定使用。 presentation_theme = str(cfg.get("presentation_theme") or "").strip().lower() if presentation_theme in {"dark", "light"}: system_prompt += ( "\n\n【当前界面主题】\n" f"当前页面呈现主题为 `{presentation_theme}`。若本轮生成汇报页面,请优先选择同色系 HTML 模板;" "除非用户明确要求覆盖该主题。" ) system_prompt = _append_orchestration_capability_boundary(system_prompt, agent_id) _log_es_query_routing_registration( system_prompt=system_prompt, app_config=resolved_app_config, agent_id=agent_id, available_skills=prompt_available_skills, ) return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, 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=system_prompt, state_schema=ThreadState, )