Skip to content

Dev/mahikaw/agentic enhancements#2274

Open
mahikaw wants to merge 6 commits into
mainfrom
dev/mahikaw/agentic-enhancements
Open

Dev/mahikaw/agentic enhancements#2274
mahikaw wants to merge 6 commits into
mainfrom
dev/mahikaw/agentic-enhancements

Conversation

@mahikaw

@mahikaw mahikaw commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Description

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

mahikaw added 4 commits June 26, 2026 17:06
The --agentic path wrapped the run in quiet_capture(), which discards stdout/
stderr on success, so a multi-step ReAct query produced no feedback. Mirror the
`pipeline run` logging setup (logging.basicConfig at INFO; defaults to stderr) so
the operators' existing per-query/step/tool-call logs surface, while stdout stays
clean JSON. Dense path unchanged.

Signed-off-by: Mahika Wason <mwason@nvidia.com>
Agentic retrieval reduced each hit to {doc_id,text,score} at the agent boundary
and returned bare doc_ids, unlike the dense path which returns full RetrievalHit.
Cache the full hit (keyed by doc_id) in _retrieve_for_agent, then re-hydrate the
selected doc_ids at output time so agentic_query_documents returns RetrievalHit
metadata (text/source/page_number/pdf_page/metadata/...) plus the agentic
annotations (doc_id/rank/result_source). Addresses review feedback (jperez999).

Signed-off-by: Mahika Wason <mwason@nvidia.com>
--agentic-llm-model is no longer required: it defaults to
nvidia/llama-3.3-nemotron-super-49b-v1.5 (also settable via the
NEMO_RETRIEVER_AGENTIC_LLM_MODEL env var), so --agentic works standalone.

Signed-off-by: Mahika Wason <mwason@nvidia.com>
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>
…nhancements

# Conflicts:
#	nemo_retriever/src/nemo_retriever/cli/query/options.py
@copy-pr-bot

copy-pr-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@mahikaw mahikaw force-pushed the dev/mahikaw/agentic-enhancements branch from dea4482 to fed8ce0 Compare June 30, 2026 23:46
@mahikaw mahikaw marked this pull request as ready for review July 9, 2026 19:16
@mahikaw mahikaw requested review from a team as code owners July 9, 2026 19:16
@mahikaw mahikaw requested a review from edknv July 9, 2026 19:16
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes three agentic enhancements: sets a default LLM model (nvidia/llama-3.3-nemotron-super-49b-v1.5) so --agentic works out-of-the-box without --agentic-llm-model; adds a NEMO_RETRIEVER_AGENTIC_LLM_MODEL env-var override; and re-hydrates full RetrievalHit metadata (text, source, page_number, …) into the agentic output so it matches what the dense path returns.

  • Retrieval knob forwarding: candidate_k, page_dedup, and content_types are now threaded through AgenticRetrievalConfig and applied on every ReAct retrieval hop, mirroring the dense path's options.
  • Hit cache: _hit_cache is introduced in AgenticRetriever to capture full hits at the retrieval boundary before the agent reduces them to doc_ids, then re-hydrate the output.
  • Logging change: quiet_capture() is removed for the agentic path and replaced with logging.basicConfig(force=True) so ReAct step progress is surfaced on stderr.

Confidence Score: 3/5

The agentic output enrichment logic is well-structured and test-covered, but a behavioural inconsistency in content_types handling between the dense and agentic paths could cause a silent runtime failure.

The content_types normalization is missing from the agentic workflow — the dense path converts Sequence[str] to a comma-joined string before calling Retriever.query, but the agentic path passes the raw list through. Any programmatic caller passing content_types=["text", "image"] will see different behaviour or a runtime failure on the agentic branch.

workflow.py (content_types normalization gap) and agentic.py (candidate_k falsy check, _hit_cache.clear without lock) deserve the most attention before merge.

Important Files Changed

Filename Overview
nemo_retriever/src/nemo_retriever/query/workflow.py Passes content_types directly from QueryRetrievalOptions to AgenticRetrievalConfig without the sequence-to-string normalization that the non-agentic resolve_query_plan path applies; enriches agentic output with re-hydrated hit metadata.
nemo_retriever/src/nemo_retriever/query/agentic.py Adds candidate_k/page_dedup/content_types forwarding to per-hop retrieval and introduces _hit_cache for metadata re-hydration; the cache clear is not lock-protected and candidate_k=0 is silently ignored.
nemo_retriever/src/nemo_retriever/cli/query/app.py Removes quiet_capture() around agentic retrieval and adds logging.basicConfig(force=True) to surface ReAct progress; force=True aggressively replaces root handlers and the missing explicit stream= is a latent test fragility.
nemo_retriever/src/nemo_retriever/query/options.py Adds DEFAULT_AGENTIC_LLM_MODEL constant and changes llm_model default in QueryAgenticOptions from None to the 49B Nemotron model; clean change.
nemo_retriever/src/nemo_retriever/cli/query/options.py Adds NEMO_RETRIEVER_AGENTIC_LLM_MODEL envvar support and updated help text for --agentic-llm-model; straightforward and correct.
nemo_retriever/tests/test_agentic_eval.py Adds tests for hit re-hydration (hit column assertions) and for retrieval knob forwarding (candidate_k/page_dedup/content_types); FakeRetriever extended with query_calls tracking; good coverage of happy path.
nemo_retriever/tests/test_root_query_cli.py Replaces the 'requires --agentic-llm-model' test with a test asserting the default 49B model is used when the flag is omitted; correct for the new behavior.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
nemo_retriever/src/nemo_retriever/query/workflow.py:168-170
**`content_types` sequence not normalized for agentic path**

The non-agentic path in `resolve_query_plan()` explicitly converts a `Sequence[str]` to a comma-joined string before passing it to `Retriever.query`. The agentic path passes `request.retrieval.content_types` raw to `AgenticRetrievalConfig`, which then forwards it directly to `self._retriever.query(content_types=...)`. If a caller passes `content_types=["text", "image"]` programmatically, the agentic path will hand a `list` to `Retriever.query` while the dense path would produce `"text,image"`, meaning the same option behaves differently across the two paths and may fail at runtime in the agentic case.

### Issue 2 of 4
nemo_retriever/src/nemo_retriever/query/agentic.py:286
**`candidate_k=0` silently treated as `None`**

`if self._cfg.candidate_k else None` is a falsy check. `candidate_k=0` is falsy in Python, so it would be silently ignored and treated as `None` rather than triggering the floor calculation. An integer check is safer here and makes the intent explicit.

```suggestion
        candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k is not None else None
```

### Issue 3 of 4
nemo_retriever/src/nemo_retriever/query/agentic.py:215
**`_hit_cache.clear()` called without holding `self._lock`**

`self._hit_cache` is shared across concurrent `_retrieve_for_agent` workers (which hold `self._lock` when writing). If `retrieve()` is ever called on the same `AgenticRetriever` instance from two threads simultaneously, the `clear()` here races with `setdefault()` calls from workers of the first invocation. The current usage pattern (new instance per call in `agentic_query_documents`) is safe, but the absence of a lock on `clear()` leaves the class undocumented as "not safe for concurrent `retrieve()` calls" — a footgun for future users who hold onto an `AgenticRetriever` and call it concurrently.

### Issue 4 of 4
nemo_retriever/src/nemo_retriever/cli/query/app.py:251-255
**`logging.basicConfig(force=True)` without explicit `stream` parameter**

`force=True` replaces all existing handlers on the root logger. When this CLI runs under Typer's `CliRunner` (which defaults to `mix_stderr=True`), log lines emitted to stderr appear in `result.output` and break any `json.loads(result.output)` assertions. Passing `stream=sys.stderr` explicitly makes the intent clear and is safer if `sys.stderr` is swapped by the test runner.

```suggestion
            logging.basicConfig(
                level=logging.INFO,
                format="%(asctime)s %(levelname)s %(name)s: %(message)s",
                stream=sys.stderr,
                force=True,
            )
```

Reviews (1): Last reviewed commit: "Reuse agentic LLM default in CLI help" | Re-trigger Greptile

Comment on lines +168 to +170
"candidate_k": request.retrieval.candidate_k,
"page_dedup": bool(request.retrieval.page_dedup),
"content_types": request.retrieval.content_types,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 content_types sequence not normalized for agentic path

The non-agentic path in resolve_query_plan() explicitly converts a Sequence[str] to a comma-joined string before passing it to Retriever.query. The agentic path passes request.retrieval.content_types raw to AgenticRetrievalConfig, which then forwards it directly to self._retriever.query(content_types=...). If a caller passes content_types=["text", "image"] programmatically, the agentic path will hand a list to Retriever.query while the dense path would produce "text,image", meaning the same option behaves differently across the two paths and may fail at runtime in the agentic case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/query/workflow.py
Line: 168-170

Comment:
**`content_types` sequence not normalized for agentic path**

The non-agentic path in `resolve_query_plan()` explicitly converts a `Sequence[str]` to a comma-joined string before passing it to `Retriever.query`. The agentic path passes `request.retrieval.content_types` raw to `AgenticRetrievalConfig`, which then forwards it directly to `self._retriever.query(content_types=...)`. If a caller passes `content_types=["text", "image"]` programmatically, the agentic path will hand a `list` to `Retriever.query` while the dense path would produce `"text,image"`, meaning the same option behaves differently across the two paths and may fail at runtime in the agentic case.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


# candidate_k must be >= the hop's top_k, which grows as the agent paginates,
# so floor it at top_k when the caller requested a wider pool.
candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 candidate_k=0 silently treated as None

if self._cfg.candidate_k else None is a falsy check. candidate_k=0 is falsy in Python, so it would be silently ignored and treated as None rather than triggering the floor calculation. An integer check is safer here and makes the intent explicit.

Suggested change
candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k else None
candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k is not None else None
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/query/agentic.py
Line: 286

Comment:
**`candidate_k=0` silently treated as `None`**

`if self._cfg.candidate_k else None` is a falsy check. `candidate_k=0` is falsy in Python, so it would be silently ignored and treated as `None` rather than triggering the floor calculation. An integer check is safer here and makes the intent explicit.

```suggestion
        candidate_k = max(int(self._cfg.candidate_k), int(top_k)) if self._cfg.candidate_k is not None else None
```

How can I resolve this? If you propose a fix, please make it concise.


if len(query_ids) != len(query_texts):
raise ValueError("query_ids and query_texts must have the same length.")
self._hit_cache.clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _hit_cache.clear() called without holding self._lock

self._hit_cache is shared across concurrent _retrieve_for_agent workers (which hold self._lock when writing). If retrieve() is ever called on the same AgenticRetriever instance from two threads simultaneously, the clear() here races with setdefault() calls from workers of the first invocation. The current usage pattern (new instance per call in agentic_query_documents) is safe, but the absence of a lock on clear() leaves the class undocumented as "not safe for concurrent retrieve() calls" — a footgun for future users who hold onto an AgenticRetriever and call it concurrently.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/query/agentic.py
Line: 215

Comment:
**`_hit_cache.clear()` called without holding `self._lock`**

`self._hit_cache` is shared across concurrent `_retrieve_for_agent` workers (which hold `self._lock` when writing). If `retrieve()` is ever called on the same `AgenticRetriever` instance from two threads simultaneously, the `clear()` here races with `setdefault()` calls from workers of the first invocation. The current usage pattern (new instance per call in `agentic_query_documents`) is safe, but the absence of a lock on `clear()` leaves the class undocumented as "not safe for concurrent `retrieve()` calls" — a footgun for future users who hold onto an `AgenticRetriever` and call it concurrently.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +251 to +255
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
force=True,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 logging.basicConfig(force=True) without explicit stream parameter

force=True replaces all existing handlers on the root logger. When this CLI runs under Typer's CliRunner (which defaults to mix_stderr=True), log lines emitted to stderr appear in result.output and break any json.loads(result.output) assertions. Passing stream=sys.stderr explicitly makes the intent clear and is safer if sys.stderr is swapped by the test runner.

Suggested change
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
force=True,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
stream=sys.stderr,
force=True,
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/cli/query/app.py
Line: 251-255

Comment:
**`logging.basicConfig(force=True)` without explicit `stream` parameter**

`force=True` replaces all existing handlers on the root logger. When this CLI runs under Typer's `CliRunner` (which defaults to `mix_stderr=True`), log lines emitted to stderr appear in `result.output` and break any `json.loads(result.output)` assertions. Passing `stream=sys.stderr` explicitly makes the intent clear and is safer if `sys.stderr` is swapped by the test runner.

```suggestion
            logging.basicConfig(
                level=logging.INFO,
                format="%(asctime)s %(levelname)s %(name)s: %(message)s",
                stream=sys.stderr,
                force=True,
            )
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant