Skip to content

Commit 26d60d6

Browse files
committed
Honor --candidate-k/--page-dedup/--content-types in agentic retrieval
These retrieval-shaping flags were silently dropped in agentic mode. Thread them from QueryRetrievalOptions into AgenticRetrievalConfig and forward to the per-hop Retriever.query call (candidate_k floored at the hop's top_k), matching how the dense path applies them. Signed-off-by: Mahika Wason <mwason@nvidia.com>
1 parent a3ae980 commit 26d60d6

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

nemo_retriever/src/nemo_retriever/query/agentic.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ class AgenticRetrievalConfig:
139139
# Drives the ReAct target, the RRF/selection cut, and the per-hop fetch depth
140140
# (which is raised to at least this). Defaults to 10.
141141
top_k: int = AGENTIC_TARGET_TOP_K
142+
# Per-hop retrieval shaping, forwarded to ``Retriever.query`` on every agent
143+
# retrieval hop (mirrors the dense path's --candidate-k/--page-dedup/--content-types).
144+
candidate_k: Optional[int] = None
145+
page_dedup: bool = False
146+
content_types: str | Sequence[str] | None = None
142147

143148
def __post_init__(self) -> None:
144149
if self.llm_model is None or not str(self.llm_model).strip():
@@ -276,8 +281,17 @@ def _retrieve_for_agent(self, query_text: str, top_k: int) -> list[dict[str, Any
276281
which still run concurrently under ``num_concurrent > 1``.
277282
"""
278283

284+
# candidate_k must be >= the hop's top_k, which grows as the agent paginates,
285+
# so floor it at top_k when the caller requested a wider pool.
286+
candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k else None
279287
with self._lock:
280-
hits = self._retriever.query(str(query_text), top_k=int(top_k))
288+
hits = self._retriever.query(
289+
str(query_text),
290+
top_k=int(top_k),
291+
candidate_k=candidate_k,
292+
page_dedup=bool(self._cfg.page_dedup),
293+
content_types=self._cfg.content_types,
294+
)
281295

282296
docs: list[dict[str, Any]] = []
283297
doc_id_field = getattr(self, "_doc_id_field", None)

nemo_retriever/src/nemo_retriever/query/workflow.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def agentic_query_documents(request: QueryRequest) -> list[dict[str, Any]]:
116116
"vdb_op": "lancedb",
117117
"vdb_kwargs": vdb_kwargs,
118118
"top_k": int(request.retrieval.top_k),
119+
"candidate_k": request.retrieval.candidate_k,
120+
"page_dedup": bool(request.retrieval.page_dedup),
121+
"content_types": request.retrieval.content_types,
119122
"embedding_endpoint": request.embed.embed_invoke_url,
120123
"embedding_api_key": api_key or "",
121124
"llm_model": request.agentic.llm_model,

nemo_retriever/tests/test_agentic_eval.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ def __init__(self, **kwargs):
3636
self.kwargs = kwargs
3737
self.graph = kwargs.get("graph")
3838
self.top_k = int(kwargs.get("top_k", 10))
39+
self.query_calls: list[dict] = []
3940

40-
def query(self, query: str, *, top_k: int | None = None):
41+
def query(self, query: str, *, top_k=None, candidate_k=None, page_dedup=False, content_types=None):
4142
if self.graph is not None:
4243
return self.queries([query], top_k=top_k)[0]
4344
_ = query
45+
self.query_calls.append(
46+
{"top_k": top_k, "candidate_k": candidate_k, "page_dedup": page_dedup, "content_types": content_types}
47+
)
4448
hits = [
4549
{
4650
"source": "/tmp/doc.pdf",
@@ -125,6 +129,38 @@ def test_agentic_retriever_honors_top_k(mock_react_step, mock_selection_step):
125129
assert result["rank"].tolist() == list(range(1, 6)) # 5 rows, honoring top_k=5
126130

127131

132+
@patch("nemo_retriever.operators.graph_ops.selection_agent_operator.invoke_chat_completion_step")
133+
@patch("nemo_retriever.operators.graph_ops.react_agent_operator.invoke_chat_completion_step")
134+
@patch("nemo_retriever.query.agentic.Retriever", FakeRetriever)
135+
def test_agentic_retriever_forwards_retrieval_knobs(mock_react_step, mock_selection_step):
136+
"""candidate_k/page_dedup/content_types reach the per-hop Retriever.query call."""
137+
from nemo_retriever.query.agentic import AgenticRetrievalConfig, AgenticRetriever
138+
139+
mock_react_step.return_value = _make_tool_call_response(
140+
"final_results", {"doc_ids": ["doc_1"], "message": "done", "search_successful": "true"}
141+
)
142+
mock_selection_step.return_value = _make_tool_call_response(
143+
"log_selected_documents", {"doc_ids": ["doc_1"], "message": "best"}
144+
)
145+
146+
cfg = AgenticRetrievalConfig(
147+
llm_model="m",
148+
invoke_url="http://localhost/v1/chat/completions",
149+
top_k=1,
150+
candidate_k=40,
151+
page_dedup=True,
152+
content_types="text",
153+
)
154+
retriever = AgenticRetriever(cfg, match_mode="pdf_page")
155+
retriever.retrieve(["0"], ["find doc"])
156+
157+
calls = retriever._retriever.query_calls
158+
assert calls, "expected at least one per-hop retriever.query call"
159+
assert all(c["page_dedup"] is True for c in calls)
160+
assert all(c["content_types"] == "text" for c in calls)
161+
assert all(c["candidate_k"] >= c["top_k"] for c in calls) # floored at the hop's top_k
162+
163+
128164
def test_agentic_config_requires_llm_model():
129165
from nemo_retriever.query.agentic import AgenticRetrievalConfig
130166

0 commit comments

Comments
 (0)