"""DeepResearchEngine — mode-dispatching entry point (§12.1). The engine selects the appropriate runner for the request's ``mode`` and hands it the assembled :class:`AdapterBundle`. It is a thin dispatcher: all research logic lives in the runners; all infrastructure wiring lives in the adapter construction (done by the executor before calling ``run``). ``basic``/``quick`` use the compact path. ``detailed`` and ``deep`` use their dedicated runners; ``multi_agent`` runs the LangGraph editor/researcher/writer/ reviewer workflow with durable plan-review pause and resume. """ from __future__ import annotations import logging from typing import TYPE_CHECKING from deerflow.agents.deep_research.runners.basic import BasicRunner from deerflow.agents.deep_research.runners.deep import DeepRunner from deerflow.agents.deep_research.runners.detailed import DetailedRunner from deerflow.agents.deep_research.runners.multi_agent import MultiAgentRunner from deerflow.agents.deep_research.types import DeepResearchRequest, DeepResearchResult if TYPE_CHECKING: from deerflow.agents.deep_research.cancellation import CancellationToken from deerflow.agents.deep_research.types import AdapterBundle logger = logging.getLogger(__name__) class DeepResearchEngine: """Dispatches a research request to the mode-appropriate runner.""" def __init__(self) -> None: self._basic = BasicRunner() self._detailed = DetailedRunner() self._deep = DeepRunner() self._multi_agent = MultiAgentRunner() async def run( self, request: DeepResearchRequest, *, adapters: AdapterBundle, cancellation: CancellationToken | None = None, ) -> DeepResearchResult: mode = request.config.mode # Chat/regeneration entries use a static material provider. First chat # writes select exactly one channel in order (browser snapshot, # durable rows, checkpoint); regeneration reads its cloned durable # source pool directly. Once the selected path is empty, that is # authoritative: # bypass every planning/search/curation branch and enter the supported # no-material report path immediately, regardless of requested mode. material_count = getattr(adapters.materials, "material_count", None) if material_count == 0: return await self._basic._no_materials_result( request, adapters, { "queries": [request.query], "collection": "no_material_fallback", "requestedMode": mode, }, cancellation, collection_outcomes=[ { "query": request.query, "status": "completed", "resultCount": 0, "route": "no_material_fallback", } ], ) if mode in ("basic", "quick"): return await self._basic.run(request, adapters, cancellation) if mode == "detailed": return await self._detailed.run(request, adapters, cancellation) if mode == "deep": return await self._deep.run(request, adapters, cancellation) if mode == "multi_agent": return await self._multi_agent.run(request, adapters, cancellation) # Phase 1 scope guard — these modes are planned for later phases. raise NotImplementedError( f"Research mode '{mode}' is not implemented. " "Available modes: quick, basic, detailed, deep, multi_agent." ) __all__ = ["DeepResearchEngine"]