91 lines
3.6 KiB
Python
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"]
|