deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/engine.py
2026-09-07 18:24:55 +08:00

91 lines
3.6 KiB
Python

"""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"]