Skip to content

Commit e592510

Browse files
committed
Added routing layer in agentic RAG
1 parent 026db1a commit e592510

14 files changed

Lines changed: 869 additions & 33 deletions

File tree

deploy/compose/.env

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,39 @@ export OCR_MS_GPU_ID=0
101101
# ==========================
102102

103103
export PROMPT_CONFIG_FILE=${PWD}/src/nvidia_rag/rag_server/prompt.yaml
104+
105+
# ==========================
106+
# Agentic RAG
107+
# ==========================
108+
109+
# Post-synthesis verification pass (re-checks the answer and back-fills with
110+
# follow-up retrieval). Enabled to improve groundedness on queries where the
111+
# context was retrieved but the synthesis refused/under-answered.
112+
export AGENTIC_VERIFICATION_ENABLED=false
113+
114+
# --- Per-role reasoning config (active = DEFAULT all-on for latency probe) ---
115+
# Default = thinking=true, budget=0 (unbounded), low_effort=false on ALL roles = max reasoning.
116+
export AGENTIC_PLANNER_LLM_ENABLE_THINKING=true
117+
export AGENTIC_PLANNER_LLM_REASONING_BUDGET=0
118+
export AGENTIC_PLANNER_LLM_LOW_EFFORT=false
119+
export AGENTIC_SYNTHESIS_LLM_ENABLE_THINKING=true
120+
export AGENTIC_SYNTHESIS_LLM_REASONING_BUDGET=0
121+
export AGENTIC_SYNTHESIS_LLM_LOW_EFFORT=false
122+
export AGENTIC_SEED_GEN_LLM_ENABLE_THINKING=true
123+
export AGENTIC_SEED_GEN_LLM_REASONING_BUDGET=0
124+
export AGENTIC_SEED_GEN_LLM_LOW_EFFORT=false
125+
export AGENTIC_TASK_LLM_ENABLE_THINKING=true
126+
export AGENTIC_TASK_LLM_REASONING_BUDGET=0
127+
export AGENTIC_TASK_LLM_LOW_EFFORT=false
128+
129+
# --- Per-query reasoning routing (classifier picks thinking on/off per query) ---
130+
# When enabled, a lightweight classifier labels each query simple/complex and
131+
# overrides the per-role ENABLE_THINKING above for that request only (uncertain
132+
# -> thinking-ON for safety). ROLES limits which roles the router controls.
133+
export AGENTIC_QUERY_ROUTING_ENABLED=false
134+
export AGENTIC_QUERY_ROUTING_ROLES=planner,task,seed_gen,synthesis
135+
136+
# --- Cycle 3: non-reasoning latency lever (fewer scope-discovery rounds) ---
137+
# Default 2; reduced to 1 to cut sequential planner round-trips. Recall is saturated
138+
# (recall@10=0.94) so a 2nd scope round adds little retrieval coverage.
139+
export AGENTIC_PLANNER_MAX_SCOPE_ROUNDS=1

deploy/compose/docker-compose-rag-server.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ services:
9797
AGENTIC_SYNTHESIS_LLM_REASONING_BUDGET: ${AGENTIC_SYNTHESIS_LLM_REASONING_BUDGET:-0}
9898
AGENTIC_SYNTHESIS_LLM_LOW_EFFORT: ${AGENTIC_SYNTHESIS_LLM_LOW_EFFORT:-false}
9999

100+
# Per-query reasoning routing (off by default). When enabled, a lightweight
101+
# classifier labels each query simple/complex and overrides the per-role
102+
# ENABLE_THINKING for that request only (uncertain -> thinking-ON for safety).
103+
AGENTIC_QUERY_ROUTING_ENABLED: ${AGENTIC_QUERY_ROUTING_ENABLED:-false}
104+
AGENTIC_QUERY_ROUTING_ROLES: ${AGENTIC_QUERY_ROUTING_ROLES:-planner,task,seed_gen,synthesis}
105+
100106
##===Query Rewriter Model specific configurations===
101107
APP_QUERYREWRITER_MODELNAME: ${APP_QUERYREWRITER_MODELNAME:-"nvidia/nemotron-3-super-120b-a12b"}
102108
# url on which query rewriter model is hosted. If "", Nvidia hosted API is used
@@ -261,10 +267,15 @@ services:
261267

262268
# Verification pass (disabled by default)
263269
AGENTIC_VERIFICATION_ENABLED: ${AGENTIC_VERIFICATION_ENABLED:-false}
270+
AGENTIC_VERIFICATION_MAX_TASKS: ${AGENTIC_VERIFICATION_MAX_TASKS:-3}
264271

265272
# Context window budget for retrieved chunks
266273
AGENTIC_CONTEXT_MAX_TOKENS: ${AGENTIC_CONTEXT_MAX_TOKENS:-100000}
267274

275+
# Planner decomposition budget (surfaced for tuning; defaults match code)
276+
AGENTIC_PLANNER_MAX_TASKS: ${AGENTIC_PLANNER_MAX_TASKS:-5}
277+
AGENTIC_PLANNER_MAX_SCOPE_ROUNDS: ${AGENTIC_PLANNER_MAX_SCOPE_ROUNDS:-2}
278+
268279
ports:
269280
- "8081:8081"
270281
expose:

deploy/helm/nvidia-blueprint-rag/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ envVars:
292292
AGENTIC_SYNTHESIS_LLM_REASONING_BUDGET: "0"
293293
AGENTIC_SYNTHESIS_LLM_LOW_EFFORT: "false"
294294

295+
# Per-query reasoning routing (off by default). When enabled, a lightweight
296+
# classifier labels each query simple/complex and overrides the per-role
297+
# ENABLE_THINKING for that request only (uncertain -> thinking-ON for safety).
298+
AGENTIC_QUERY_ROUTING_ENABLED: "false"
299+
AGENTIC_QUERY_ROUTING_ROLES: "planner,task,seed_gen,synthesis"
300+
295301
NEMO_GUARDRAILS_URL: "nemo-guardrails:7331"
296302

297303
# enable iterative query decomposition

frontend/src/hooks/__tests__/useChatStream.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,46 @@ describe('useChatStream — agentic streaming path (PR #512)', () => {
448448
]);
449449
expect(last.reasoning_steps?.[0].status).toBe('done');
450450
});
451+
452+
it('renders the query_routing stage (start+end) as one completed step', async () => {
453+
// Backend emits a query_routing stage_start/stage_end pair as the first
454+
// two chunks when per-query reasoning routing is enabled. No frontend
455+
// code changes are needed — it flows through the generic stage handling.
456+
const { last } = await drain([
457+
{
458+
choices: [
459+
{
460+
delta: {
461+
event_type: 'stage_start',
462+
stage: 'query_routing',
463+
reasoning_content:
464+
'Analyzing the question to choose a reasoning strategy…',
465+
},
466+
},
467+
],
468+
},
469+
{
470+
choices: [
471+
{
472+
delta: {
473+
event_type: 'stage_end',
474+
stage: 'query_routing',
475+
reasoning_content:
476+
'Classified as a multi-step question — enabling step-by-step reasoning.',
477+
},
478+
},
479+
],
480+
},
481+
{ choices: [{ delta: {}, finish_reason: 'stop' }] },
482+
]);
483+
const step = last.reasoning_steps?.[0];
484+
expect(step?.stage).toBe('query_routing');
485+
expect(step?.status).toBe('done');
486+
expect(step?.label).toBe(
487+
'Analyzing the question to choose a reasoning strategy…'
488+
);
489+
expect(step?.summary).toBe(
490+
'Classified as a multi-step question — enabling step-by-step reasoning.'
491+
);
492+
});
451493
});

frontend/src/hooks/__tests__/useCitationUtils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('useCitationUtils', () => {
3434
const { result } = renderHook(() => useCitationUtils());
3535
expect(result.current.formatStage('initial_retrieval')).toBe('Initial retrieval');
3636
expect(result.current.formatStage('verify_execute')).toBe('Verify execute');
37+
expect(result.current.formatStage('query_routing')).toBe('Query routing');
3738
});
3839

3940
it('humanises kebab-case stage identifiers', () => {

src/nvidia_rag/rag_server/agentic_rag/agentic_rag.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,16 @@ def _resolve_role_llm(
259259
model_or_endpoint_overridden = (
260260
overrides.model is not None or overrides.llm_endpoint is not None
261261
)
262-
cache_key = "__shared__" if model_or_endpoint_overridden else role_name
262+
# When the query router supplies per-role thinking overrides, every role
263+
# may resolve to a different enable_thinking, so they cannot share one
264+
# client — force a per-role cache key even if model/endpoint is also
265+
# overridden (each role still uses the overridden model/endpoint, just
266+
# with its own thinking flag).
267+
role_thinking_overridden = bool(overrides.enable_thinking_by_role)
268+
if model_or_endpoint_overridden and not role_thinking_overridden:
269+
cache_key = "__shared__"
270+
else:
271+
cache_key = role_name
263272

264273
cache = overrides._built_llms
265274
if cache_key in cache:
@@ -356,10 +365,17 @@ def _resolve_gen(field_name: str, override_val: Any) -> Any:
356365
temperature = _resolve_gen("temperature", overrides.temperature)
357366
top_p = _resolve_gen("top_p", overrides.top_p)
358367
max_tokens = _resolve_gen("max_tokens", overrides.max_tokens)
359-
# Thinking params are never overridden by the per-request API; always
360-
# resolved from the per-role agentic config so that AGENTIC_*_LLM_ENABLE_THINKING
361-
# is respected even when temperature/top_p/max_tokens cause this override path.
362-
enable_thinking = _resolve_gen("enable_thinking", None)
368+
# enable_thinking may be overridden per-role by the query router; when
369+
# the router didn't decide for this role (or routing is off) the value
370+
# is None and we fall back to the per-role AGENTIC_*_LLM_ENABLE_THINKING
371+
# config. reasoning_budget / low_effort are never overridden per-request
372+
# and always resolve from config.
373+
role_thinking_override = (
374+
overrides.enable_thinking_by_role.get(role_name)
375+
if overrides.enable_thinking_by_role
376+
else None
377+
)
378+
enable_thinking = _resolve_gen("enable_thinking", role_thinking_override)
363379
reasoning_budget = _resolve_gen("reasoning_budget", None)
364380
low_effort = _resolve_gen("low_effort", None)
365381

src/nvidia_rag/rag_server/agentic_rag/builder.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,24 @@ class AgenticLLMOverrides:
104104
temperature: float | None = None
105105
top_p: float | None = None
106106
max_tokens: int | None = None
107+
# Per-role enable_thinking override set by the query router (see
108+
# query_router.build_thinking_overrides). Maps role name (planner / task /
109+
# seed_gen / synthesis) → desired thinking bool for this request only.
110+
# A role absent from the dict keeps its server-wide AGENTIC_*_LLM_ENABLE_THINKING
111+
# default. None means "router inactive — no thinking override".
112+
enable_thinking_by_role: dict[str, bool] | None = None
107113
# Per-request cache: maps cache key → built LLM client. Key is "__shared__"
108114
# when model/endpoint is overridden (all 4 roles collapse to one client);
109115
# otherwise it's the role name (planner / task / seed_gen / synthesis) so
110116
# each role gets its own client with that role's configured model/endpoint
111117
# but the request-provided generation params applied.
112118
# Mutable; not part of equality / hashing.
113-
_built_llms: dict[str, Any] = field(
114-
default_factory=dict, repr=False, compare=False
115-
)
119+
_built_llms: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
116120

117121
def has_any_override(self) -> bool:
118122
"""True if at least one non-None override is set."""
123+
if self.enable_thinking_by_role:
124+
return True
119125
return any(
120126
v is not None
121127
for v in (
@@ -319,9 +325,7 @@ def _make_role_llm(
319325
temperature = _resolve_role_generation_param(
320326
role_cfg, fallback_cfg, rag_config, "temperature"
321327
)
322-
top_p = _resolve_role_generation_param(
323-
role_cfg, fallback_cfg, rag_config, "top_p"
324-
)
328+
top_p = _resolve_role_generation_param(role_cfg, fallback_cfg, rag_config, "top_p")
325329
max_tokens = _resolve_role_generation_param(
326330
role_cfg, fallback_cfg, rag_config, "max_tokens"
327331
)

0 commit comments

Comments
 (0)