deep-search-agent 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deep_search_agent/__init__.py +64 -0
- deep_search_agent/factory.py +385 -0
- deep_search_agent/metrics.py +434 -0
- deep_search_agent/middleware.py +257 -0
- deep_search_agent/prompts.py +304 -0
- deep_search_agent/py.typed +0 -0
- deep_search_agent/subagents.py +188 -0
- deep_search_agent/tools/__init__.py +6 -0
- deep_search_agent/tools/fetch.py +299 -0
- deep_search_agent/tools/search.py +286 -0
- deep_search_agent-0.4.1.dist-info/METADATA +217 -0
- deep_search_agent-0.4.1.dist-info/RECORD +14 -0
- deep_search_agent-0.4.1.dist-info/WHEEL +4 -0
- deep_search_agent-0.4.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Deep search agent library built on LangChain deepagents.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
- :func:`create_deep_search_agent` — factory returning a configured deep
|
|
6
|
+
agent implementing the orchestrator + specialized sub-agents + rubric-graded
|
|
7
|
+
refinement architecture.
|
|
8
|
+
- :data:`DEEP_SEARCH_RUBRIC` — the default grading rubric.
|
|
9
|
+
- :class:`DefaultRubricMiddleware` — auto-injects a rubric into the state.
|
|
10
|
+
- :class:`DeepSearchRubricMiddleware` — the evaluator/critic loop; grades the
|
|
11
|
+
orchestrator's final answer against the rubric in full (untruncated).
|
|
12
|
+
- :class:`SearchBudgetResetMiddleware` — resets the per-cycle search budget at
|
|
13
|
+
each research-cycle boundary.
|
|
14
|
+
- :class:`SearchBudget` — thread-safe per-cycle search budget shared with the
|
|
15
|
+
search tool.
|
|
16
|
+
- :class:`SessionMetrics` — thread-safe collector of per-cycle and global
|
|
17
|
+
observability metrics (tool calls, sub-agent invocations and timings, overall
|
|
18
|
+
execution time), wired in via ``create_deep_search_agent(metrics=...)``.
|
|
19
|
+
:class:`SubagentStats` and :class:`CycleMetrics` are its typed read models.
|
|
20
|
+
- :func:`create_searxng_search_tool` / :func:`create_fetch_url_tool` — the
|
|
21
|
+
tool factories used by the built-in sub-agents, reusable standalone.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from deep_search_agent.factory import create_deep_search_agent
|
|
25
|
+
from deep_search_agent.metrics import CycleMetrics, SessionMetrics, SubagentStats
|
|
26
|
+
from deep_search_agent.middleware import (
|
|
27
|
+
DeepSearchRubricMiddleware,
|
|
28
|
+
DefaultRubricMiddleware,
|
|
29
|
+
SearchBudgetResetMiddleware,
|
|
30
|
+
)
|
|
31
|
+
from deep_search_agent.prompts import (
|
|
32
|
+
DEEP_SEARCH_RUBRIC,
|
|
33
|
+
FACT_CHECK_AGENT_PROMPT,
|
|
34
|
+
FETCH_AGENT_PROMPT,
|
|
35
|
+
ORCHESTRATOR_PROMPT_TEMPLATE,
|
|
36
|
+
PERSPECTIVE_AGENT_PROMPT,
|
|
37
|
+
SEARCH_AGENT_PROMPT_TEMPLATE,
|
|
38
|
+
)
|
|
39
|
+
from deep_search_agent.tools import (
|
|
40
|
+
SearchBudget,
|
|
41
|
+
create_fetch_url_tool,
|
|
42
|
+
create_searxng_search_tool,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DEEP_SEARCH_RUBRIC",
|
|
47
|
+
"FACT_CHECK_AGENT_PROMPT",
|
|
48
|
+
"FETCH_AGENT_PROMPT",
|
|
49
|
+
"ORCHESTRATOR_PROMPT_TEMPLATE",
|
|
50
|
+
"PERSPECTIVE_AGENT_PROMPT",
|
|
51
|
+
"SEARCH_AGENT_PROMPT_TEMPLATE",
|
|
52
|
+
"CycleMetrics",
|
|
53
|
+
"DeepSearchRubricMiddleware",
|
|
54
|
+
"DefaultRubricMiddleware",
|
|
55
|
+
"SearchBudget",
|
|
56
|
+
"SearchBudgetResetMiddleware",
|
|
57
|
+
"SessionMetrics",
|
|
58
|
+
"SubagentStats",
|
|
59
|
+
"create_deep_search_agent",
|
|
60
|
+
"create_fetch_url_tool",
|
|
61
|
+
"create_searxng_search_tool",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
__version__ = "0.4.1"
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Factory for the deep search agent.
|
|
2
|
+
|
|
3
|
+
:func:`create_deep_search_agent` wires together the multi-agent deep-search
|
|
4
|
+
architecture on top of ``deepagents``:
|
|
5
|
+
|
|
6
|
+
- **Orchestrator** = the main deep agent, instructed to decompose the query,
|
|
7
|
+
delegate to sub-agents, and synthesize a cited answer.
|
|
8
|
+
- **Specialized sub-agents** = ``perspective-agent`` (multi-angle topic
|
|
9
|
+
exploration before decomposition, enabled by default), ``search-agent``
|
|
10
|
+
(SearxNG + optional extra search tools), ``fetch-agent`` (trafilatura +
|
|
11
|
+
pypdf), and ``fact-check-agent``; callers can add more via ``subagents``.
|
|
12
|
+
- **Shared scratchpad** = deepagents' filesystem (pluggable ``backend``),
|
|
13
|
+
where every sub-agent writes ``/findings/<source-slug>.md`` files with
|
|
14
|
+
provenance.
|
|
15
|
+
- **Evaluator/critic loop** = ``DeepSearchRubricMiddleware`` (a thin subclass
|
|
16
|
+
of deepagents' beta ``RubricMiddleware`` that grades the final answer in
|
|
17
|
+
full), which grades the final answer against a rubric and re-runs the
|
|
18
|
+
orchestrator up to ``max_research_cycles`` times; the default deep-search
|
|
19
|
+
rubric is auto-injected unless the caller provides one.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
from deepagents import create_deep_agent
|
|
27
|
+
from deepagents.backends import StateBackend
|
|
28
|
+
|
|
29
|
+
from deep_search_agent.metrics import (
|
|
30
|
+
_OrchestratorMetricsMiddleware,
|
|
31
|
+
_SubagentMetricsMiddleware,
|
|
32
|
+
)
|
|
33
|
+
from deep_search_agent.middleware import (
|
|
34
|
+
DeepSearchRubricMiddleware,
|
|
35
|
+
DefaultRubricMiddleware,
|
|
36
|
+
SearchBudgetResetMiddleware,
|
|
37
|
+
)
|
|
38
|
+
from deep_search_agent.prompts import (
|
|
39
|
+
DEEP_SEARCH_RUBRIC,
|
|
40
|
+
ORCHESTRATOR_PROMPT_TEMPLATE,
|
|
41
|
+
PERSPECTIVE_DECOMPOSE_HINT,
|
|
42
|
+
PERSPECTIVE_STEP_BLOCK,
|
|
43
|
+
)
|
|
44
|
+
from deep_search_agent.subagents import (
|
|
45
|
+
FACT_CHECK_AGENT_NAME,
|
|
46
|
+
FETCH_AGENT_NAME,
|
|
47
|
+
PERSPECTIVE_AGENT_NAME,
|
|
48
|
+
SEARCH_AGENT_NAME,
|
|
49
|
+
build_fact_check_subagent,
|
|
50
|
+
build_fetch_subagent,
|
|
51
|
+
build_perspective_subagent,
|
|
52
|
+
build_search_subagent,
|
|
53
|
+
)
|
|
54
|
+
from deep_search_agent.tools import create_fetch_url_tool, create_searxng_search_tool
|
|
55
|
+
from deep_search_agent.tools.search import DEFAULT_SEARXNG_BASE_URL, SearchBudget
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from collections.abc import Callable, Sequence
|
|
59
|
+
|
|
60
|
+
from deepagents.backends.protocol import BackendFactory, BackendProtocol
|
|
61
|
+
from deepagents.middleware.rubric import RubricEvaluation
|
|
62
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
63
|
+
from langchain_core.language_models import BaseChatModel
|
|
64
|
+
from langchain_core.messages import SystemMessage
|
|
65
|
+
from langchain_core.tools import BaseTool
|
|
66
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
67
|
+
|
|
68
|
+
from deep_search_agent.metrics import SessionMetrics
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_positive(name: str, value: int) -> None:
|
|
72
|
+
"""Raise ``ValueError`` unless ``value`` is a positive integer."""
|
|
73
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
74
|
+
msg = f"{name} must be a positive integer, got {value!r}"
|
|
75
|
+
raise ValueError(msg)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_positive_number(name: str, value: float) -> None:
|
|
79
|
+
"""Raise ``ValueError`` unless ``value`` is a positive real number."""
|
|
80
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
|
|
81
|
+
msg = f"{name} must be a positive number, got {value!r}"
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_deep_search_agent(
|
|
86
|
+
*,
|
|
87
|
+
model: str | BaseChatModel,
|
|
88
|
+
max_research_cycles: int = 3,
|
|
89
|
+
max_query_variants: int = 3,
|
|
90
|
+
max_search_results_per_query: int = 5,
|
|
91
|
+
max_urls_to_scrape_per_cycle: int = 3,
|
|
92
|
+
searxng_base_url: str = DEFAULT_SEARXNG_BASE_URL,
|
|
93
|
+
searxng_engines: Sequence[str] | None = None,
|
|
94
|
+
searxng_rate_limit: float | None = None,
|
|
95
|
+
searxng_budget: int | None = None,
|
|
96
|
+
request_timeout: float = 15.0,
|
|
97
|
+
max_content_chars_per_page: int = 20_000,
|
|
98
|
+
enable_js_render_fallback: bool = False,
|
|
99
|
+
js_render_timeout: float = 30.0,
|
|
100
|
+
search_tools: Sequence[BaseTool] | None = None,
|
|
101
|
+
enable_perspectives: bool = True,
|
|
102
|
+
rubric: str | None = None,
|
|
103
|
+
auto_rubric: bool = True,
|
|
104
|
+
on_evaluation: Callable[[RubricEvaluation], None] | None = None,
|
|
105
|
+
system_prompt: str | SystemMessage | None = None,
|
|
106
|
+
middleware: Sequence[AgentMiddleware] = (),
|
|
107
|
+
subagents_middleware: Sequence[AgentMiddleware] = (),
|
|
108
|
+
subagents: Sequence[Any] | None = None,
|
|
109
|
+
backend: BackendProtocol | BackendFactory | None = None,
|
|
110
|
+
metrics: SessionMetrics | None = None,
|
|
111
|
+
**create_deep_agent_kwargs: Any,
|
|
112
|
+
) -> CompiledStateGraph:
|
|
113
|
+
"""Create a deep-search agent (orchestrator + specialized sub-agents).
|
|
114
|
+
|
|
115
|
+
The returned agent decomposes the user's query into sub-questions,
|
|
116
|
+
delegates them to isolated sub-agents (web search, content fetching,
|
|
117
|
+
fact checking), accumulates sourced findings on the shared filesystem
|
|
118
|
+
backend, and synthesizes a cited answer. An LLM-as-a-judge loop
|
|
119
|
+
(deepagents' beta ``RubricMiddleware``) grades the answer against a
|
|
120
|
+
rubric and triggers additional research cycles until the rubric is
|
|
121
|
+
satisfied or ``max_research_cycles`` is reached.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
model: Model for the orchestrator and, unless overridden per
|
|
125
|
+
sub-agent, for the sub-agents too. The rubric grader inherits
|
|
126
|
+
this same model. Required — either a provider string (e.g.
|
|
127
|
+
``"anthropic:claude-sonnet-4-6"``) or a ``BaseChatModel``.
|
|
128
|
+
max_research_cycles: Maximum refinement cycles of the evaluator
|
|
129
|
+
loop (``RubricMiddleware.max_iterations``). Also quoted in the
|
|
130
|
+
orchestrator's instructions as its iteration budget.
|
|
131
|
+
max_query_variants: Number of distinct query reformulations the
|
|
132
|
+
search agent issues in parallel per sub-question (synonyms,
|
|
133
|
+
broader/narrower terms, English variants, different angle) to
|
|
134
|
+
widen recall before deduplicating results. Quoted in the search
|
|
135
|
+
agent's instructions.
|
|
136
|
+
max_search_results_per_query: Result budget per search query,
|
|
137
|
+
enforced by the SearxNG tool and quoted in the search agent's
|
|
138
|
+
instructions.
|
|
139
|
+
max_urls_to_scrape_per_cycle: URL-fetch budget per research cycle,
|
|
140
|
+
quoted in the orchestrator's instructions.
|
|
141
|
+
searxng_base_url: Root URL of the SearxNG instance used by the
|
|
142
|
+
built-in search tool.
|
|
143
|
+
searxng_engines: Optional SearxNG engine allowlist
|
|
144
|
+
(e.g. ``["duckduckgo", "wikipedia"]``).
|
|
145
|
+
searxng_rate_limit: Minimum number of seconds between two SearxNG
|
|
146
|
+
requests. Sub-agents may run searches concurrently through
|
|
147
|
+
deepagents' thread pool, so this is enforced by a thread-safe
|
|
148
|
+
min-interval limiter shared by the built-in search tool. When a
|
|
149
|
+
request would have to wait longer than ``request_timeout`` for a
|
|
150
|
+
free slot, the tool returns an ``ERROR: ...`` string instead of
|
|
151
|
+
performing it. ``None`` (default) disables rate limiting.
|
|
152
|
+
searxng_budget: Maximum number of SearxNG search operations allowed in
|
|
153
|
+
a single research cycle. Once exhausted, the search tool returns an
|
|
154
|
+
``ERROR: ...`` string telling the model no budget is left; the
|
|
155
|
+
counter is reset at each research-cycle boundary of the evaluator
|
|
156
|
+
loop. ``None`` (default) means unlimited.
|
|
157
|
+
request_timeout: HTTP timeout (seconds) for both the search and the
|
|
158
|
+
fetch tools.
|
|
159
|
+
max_content_chars_per_page: Truncation limit for content extracted
|
|
160
|
+
by the fetch tool.
|
|
161
|
+
enable_js_render_fallback: When ``True``, a page whose static HTML
|
|
162
|
+
yields no extractable content is re-fetched by the fetch tool
|
|
163
|
+
through a headless Chromium (Playwright) and extracted again,
|
|
164
|
+
recovering JavaScript-only pages and bot walls that would otherwise
|
|
165
|
+
be lost as sources. Requires the ``js-render`` extra plus installed
|
|
166
|
+
Playwright browsers (``pip install deep-search-agent[js-render]``
|
|
167
|
+
and ``playwright install chromium``); when either is missing the
|
|
168
|
+
fetch tool returns an ``ERROR: ...`` string rather than raising.
|
|
169
|
+
``False`` (default) keeps the library browser-free.
|
|
170
|
+
js_render_timeout: Seconds the headless renderer waits for a page to
|
|
171
|
+
settle. Distinct from ``request_timeout`` because rendering is much
|
|
172
|
+
slower than a plain HTTP request. Ignored unless
|
|
173
|
+
``enable_js_render_fallback`` is ``True``.
|
|
174
|
+
search_tools: Extra search tools (e.g. a Tavily tool or an internal
|
|
175
|
+
RAG retrieval tool) made available to ``search-agent`` and
|
|
176
|
+
``fact-check-agent`` alongside the built-in SearxNG tool.
|
|
177
|
+
enable_perspectives: When ``True`` (default), adds ``perspective-agent``
|
|
178
|
+
to the built-in sub-agents and instructs the orchestrator to
|
|
179
|
+
delegate to it before decomposing the query, so research explores
|
|
180
|
+
the topic from 3-6 distinct angles (analysis axes, stakeholder
|
|
181
|
+
viewpoints, dimensions of the problem) instead of a single flat
|
|
182
|
+
list of sub-questions. Set to ``False`` for simple queries where a
|
|
183
|
+
single-axis decomposition is sufficient, to skip the extra
|
|
184
|
+
delegation cycle and its token cost.
|
|
185
|
+
rubric: Custom grading rubric (newline-delimited checklist).
|
|
186
|
+
Defaults to :data:`~deep_search_agent.prompts.DEEP_SEARCH_RUBRIC`.
|
|
187
|
+
auto_rubric: When ``True`` (default), the rubric is auto-injected
|
|
188
|
+
into the invocation state so the evaluation loop works out of
|
|
189
|
+
the box. When ``False``, the loop only activates if the caller
|
|
190
|
+
passes a ``rubric`` key in the invocation state.
|
|
191
|
+
on_evaluation: Optional callback invoked with each
|
|
192
|
+
:class:`~deepagents.middleware.rubric.RubricEvaluation` after the
|
|
193
|
+
grader scores a research cycle, e.g. to log the per-criterion
|
|
194
|
+
verdicts or stream progress to a UI. Exceptions it raises are
|
|
195
|
+
logged and suppressed by the underlying ``RubricMiddleware``, so it
|
|
196
|
+
must not be used to enforce control flow. ``None`` (default)
|
|
197
|
+
registers no callback.
|
|
198
|
+
system_prompt: Override for the orchestrator system prompt. Defaults
|
|
199
|
+
to the built-in deep-search orchestrator prompt parametrized
|
|
200
|
+
with the cycle/URL budgets.
|
|
201
|
+
middleware: Extra middleware appended after the rubric middleware.
|
|
202
|
+
subagents_middleware: Extra middleware injected into each built-in
|
|
203
|
+
sub-agent (``perspective-agent`` when enabled, ``search-agent``,
|
|
204
|
+
``fetch-agent``, ``fact-check-agent``), e.g. for logging or rate
|
|
205
|
+
limiting. Sub-agents passed via ``subagents`` are caller-owned and
|
|
206
|
+
left untouched.
|
|
207
|
+
subagents: Extra sub-agents (e.g. a RAG retrieval agent over an
|
|
208
|
+
internal knowledge base) added alongside the built-in
|
|
209
|
+
``search-agent``, ``fetch-agent``, ``fact-check-agent``, and
|
|
210
|
+
(when enabled) ``perspective-agent``.
|
|
211
|
+
backend: Filesystem backend shared by the orchestrator and every
|
|
212
|
+
sub-agent, so that ``/findings/<source-slug>.md`` files written by
|
|
213
|
+
a sub-agent flow back to the orchestrator on the same virtual
|
|
214
|
+
filesystem. When omitted, a single :class:`~deepagents.backends.StateBackend`
|
|
215
|
+
instance is created and shared; when provided, that exact instance
|
|
216
|
+
is propagated to ``create_deep_agent``.
|
|
217
|
+
metrics: Optional :class:`~deep_search_agent.metrics.SessionMetrics`
|
|
218
|
+
collector. When provided, observation-only middleware are injected
|
|
219
|
+
into the orchestrator and each built-in sub-agent to record, over the
|
|
220
|
+
whole session, per-cycle and global tool-call counts, sub-agent
|
|
221
|
+
invocation counts and execution times (avg/min/max), and the overall
|
|
222
|
+
execution time. The caller owns the instance and reads the results
|
|
223
|
+
from it after the run (metrics accumulate until
|
|
224
|
+
:meth:`SessionMetrics.reset`). ``None`` (default) disables metrics.
|
|
225
|
+
**create_deep_agent_kwargs: Any remaining ``create_deep_agent``
|
|
226
|
+
parameter (``tools``, ``checkpointer``, ``store``, ``skills``,
|
|
227
|
+
``interrupt_on``, ...), passed through unchanged.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The compiled deep agent graph, ready for ``invoke`` / ``astream``.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ValueError: If ``model`` is missing, an integer budget parameter is
|
|
234
|
+
not a positive integer, ``searxng_rate_limit`` or (when the
|
|
235
|
+
fallback is enabled) ``js_render_timeout`` is not a positive
|
|
236
|
+
number, or a reserved sub-agent name is reused.
|
|
237
|
+
"""
|
|
238
|
+
if model is None:
|
|
239
|
+
msg = (
|
|
240
|
+
"create_deep_search_agent requires an explicit `model`: the "
|
|
241
|
+
"rubric grader inherits it and deepagents' implicit default "
|
|
242
|
+
"model is deprecated."
|
|
243
|
+
)
|
|
244
|
+
raise ValueError(msg)
|
|
245
|
+
_validate_positive("max_research_cycles", max_research_cycles)
|
|
246
|
+
_validate_positive("max_query_variants", max_query_variants)
|
|
247
|
+
_validate_positive("max_search_results_per_query", max_search_results_per_query)
|
|
248
|
+
_validate_positive("max_urls_to_scrape_per_cycle", max_urls_to_scrape_per_cycle)
|
|
249
|
+
if searxng_rate_limit is not None:
|
|
250
|
+
_validate_positive_number("searxng_rate_limit", searxng_rate_limit)
|
|
251
|
+
if enable_js_render_fallback:
|
|
252
|
+
_validate_positive_number("js_render_timeout", js_render_timeout)
|
|
253
|
+
if searxng_budget is not None:
|
|
254
|
+
_validate_positive("searxng_budget", searxng_budget)
|
|
255
|
+
|
|
256
|
+
# Resolve the shared backend up front so the orchestrator and the
|
|
257
|
+
# sub-agents provably operate on the same virtual filesystem: default to a
|
|
258
|
+
# single StateBackend when the caller does not supply one, otherwise
|
|
259
|
+
# propagate the caller's instance unchanged.
|
|
260
|
+
effective_backend = backend if backend is not None else StateBackend()
|
|
261
|
+
|
|
262
|
+
# --- Tools for the sub-agents -----------------------------------------
|
|
263
|
+
# The budget lives in the tool closure so it is shared across the
|
|
264
|
+
# (possibly concurrent) sub-agents that call the search tool; a middleware
|
|
265
|
+
# resets it at each research-cycle boundary (see below).
|
|
266
|
+
search_budget = SearchBudget(searxng_budget) if searxng_budget is not None else None
|
|
267
|
+
searxng_tool = create_searxng_search_tool(
|
|
268
|
+
base_url=searxng_base_url,
|
|
269
|
+
engines=searxng_engines,
|
|
270
|
+
timeout=request_timeout,
|
|
271
|
+
max_results=max_search_results_per_query,
|
|
272
|
+
min_request_interval=searxng_rate_limit,
|
|
273
|
+
budget=search_budget,
|
|
274
|
+
)
|
|
275
|
+
all_search_tools: list[BaseTool] = [searxng_tool, *(search_tools or [])]
|
|
276
|
+
fetch_tool = create_fetch_url_tool(
|
|
277
|
+
timeout=request_timeout,
|
|
278
|
+
max_content_chars=max_content_chars_per_page,
|
|
279
|
+
enable_js_render_fallback=enable_js_render_fallback,
|
|
280
|
+
js_render_timeout=js_render_timeout,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# --- Sub-agents --------------------------------------------------------
|
|
284
|
+
# When metrics are collected, each built-in sub-agent also carries its own
|
|
285
|
+
# metrics middleware (added after the caller's ``subagents_middleware``) so
|
|
286
|
+
# invocations, timings, and tool calls are attributed to the right agent.
|
|
287
|
+
def _subagent_middleware(name: str) -> Sequence[AgentMiddleware]:
|
|
288
|
+
base = list(subagents_middleware)
|
|
289
|
+
if metrics is not None:
|
|
290
|
+
base.append(_SubagentMetricsMiddleware(metrics, name))
|
|
291
|
+
return base
|
|
292
|
+
|
|
293
|
+
built_in_subagents = []
|
|
294
|
+
if enable_perspectives:
|
|
295
|
+
built_in_subagents.append(
|
|
296
|
+
build_perspective_subagent(
|
|
297
|
+
all_search_tools,
|
|
298
|
+
middleware=_subagent_middleware(PERSPECTIVE_AGENT_NAME),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
built_in_subagents.extend(
|
|
302
|
+
[
|
|
303
|
+
build_search_subagent(
|
|
304
|
+
all_search_tools,
|
|
305
|
+
max_query_variants=max_query_variants,
|
|
306
|
+
max_search_results_per_query=max_search_results_per_query,
|
|
307
|
+
middleware=_subagent_middleware(SEARCH_AGENT_NAME),
|
|
308
|
+
),
|
|
309
|
+
build_fetch_subagent(
|
|
310
|
+
fetch_tool, middleware=_subagent_middleware(FETCH_AGENT_NAME)
|
|
311
|
+
),
|
|
312
|
+
build_fact_check_subagent(
|
|
313
|
+
all_search_tools,
|
|
314
|
+
fetch_tool,
|
|
315
|
+
middleware=_subagent_middleware(FACT_CHECK_AGENT_NAME),
|
|
316
|
+
),
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
# perspective-agent is always reserved, even when enable_perspectives is
|
|
320
|
+
# False, so a caller-supplied sub-agent can never collide with it and
|
|
321
|
+
# toggling the flag never changes name-collision behavior.
|
|
322
|
+
reserved_names = {
|
|
323
|
+
SEARCH_AGENT_NAME,
|
|
324
|
+
FETCH_AGENT_NAME,
|
|
325
|
+
FACT_CHECK_AGENT_NAME,
|
|
326
|
+
PERSPECTIVE_AGENT_NAME,
|
|
327
|
+
}
|
|
328
|
+
extra_subagents = list(subagents or [])
|
|
329
|
+
for agent in extra_subagents:
|
|
330
|
+
name = (
|
|
331
|
+
agent["name"] if isinstance(agent, dict) else getattr(agent, "name", None)
|
|
332
|
+
)
|
|
333
|
+
if name in reserved_names:
|
|
334
|
+
msg = f"subagent name {name!r} is reserved by deep_search_agent"
|
|
335
|
+
raise ValueError(msg)
|
|
336
|
+
|
|
337
|
+
# --- Evaluator/critic loop ----------------------------------------------
|
|
338
|
+
effective_rubric = rubric if rubric is not None else DEEP_SEARCH_RUBRIC
|
|
339
|
+
agent_middleware: list[AgentMiddleware] = []
|
|
340
|
+
if metrics is not None:
|
|
341
|
+
# Observation-only; placed first so its before_agent stamps the run
|
|
342
|
+
# start before any other middleware runs. It never returns a jump_to,
|
|
343
|
+
# so it does not interfere with the rubric loop's cycle boundaries.
|
|
344
|
+
agent_middleware.append(_OrchestratorMetricsMiddleware(metrics))
|
|
345
|
+
if auto_rubric:
|
|
346
|
+
# Must precede RubricMiddleware so the rubric is in state when the
|
|
347
|
+
# grading loop initializes.
|
|
348
|
+
agent_middleware.append(DefaultRubricMiddleware(effective_rubric))
|
|
349
|
+
# DeepSearchRubricMiddleware (not the bare RubricMiddleware) so the grader
|
|
350
|
+
# sees the orchestrator's final report untruncated — otherwise long cited
|
|
351
|
+
# reports get cut at 4,000 chars and the grader flags a phantom
|
|
352
|
+
# "incomplete/truncated" gap (issue #22).
|
|
353
|
+
agent_middleware.append(
|
|
354
|
+
DeepSearchRubricMiddleware(
|
|
355
|
+
model=model,
|
|
356
|
+
max_iterations=max_research_cycles,
|
|
357
|
+
on_evaluation=on_evaluation,
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
# Reset the per-cycle search budget at each research-cycle boundary. Placed
|
|
361
|
+
# after RubricMiddleware so it participates in the same before/after_agent
|
|
362
|
+
# phases that delimit a research cycle.
|
|
363
|
+
if search_budget is not None:
|
|
364
|
+
agent_middleware.append(SearchBudgetResetMiddleware(search_budget))
|
|
365
|
+
agent_middleware.extend(middleware)
|
|
366
|
+
|
|
367
|
+
# --- Orchestrator --------------------------------------------------------
|
|
368
|
+
if system_prompt is None:
|
|
369
|
+
system_prompt = ORCHESTRATOR_PROMPT_TEMPLATE.format(
|
|
370
|
+
max_research_cycles=max_research_cycles,
|
|
371
|
+
max_urls_to_scrape_per_cycle=max_urls_to_scrape_per_cycle,
|
|
372
|
+
perspective_step=PERSPECTIVE_STEP_BLOCK if enable_perspectives else "",
|
|
373
|
+
perspective_decompose_hint=(
|
|
374
|
+
PERSPECTIVE_DECOMPOSE_HINT if enable_perspectives else ""
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return create_deep_agent(
|
|
379
|
+
model=model,
|
|
380
|
+
system_prompt=system_prompt,
|
|
381
|
+
middleware=agent_middleware,
|
|
382
|
+
subagents=[*built_in_subagents, *extra_subagents],
|
|
383
|
+
backend=effective_backend,
|
|
384
|
+
**create_deep_agent_kwargs,
|
|
385
|
+
)
|