airayven7 commited on
Commit
ecee177
Β·
verified Β·
1 Parent(s): bfa15f4

Sync from GitHub 97ec386

Browse files
Files changed (6) hide show
  1. README.md +10 -5
  2. app.py +20 -5
  3. core/constants.py +11 -0
  4. frontend/index.html +20 -0
  5. pipelines/agent_ask.py +36 -20
  6. pipelines/mock_ask.py +3 -2
README.md CHANGED
@@ -48,8 +48,10 @@ Space (no external endpoints).
48
  chapters plus a per-request fuzzy shortlist of fine parse headings
49
  (`app/core/sections.py`).
50
 
51
- **Retrieval is fused** β€” the search tool needs both indexes of a manual, so a
52
- manual must be indexed both ways:
 
 
53
 
54
  - **Visual** β€” every page is embedded as an image with
55
  [Nemotron ColEmbed v2](https://huggingface.co/nvidia/nemotron-colembed-vl-4b-v2)
@@ -58,13 +60,16 @@ manual must be indexed both ways:
58
  disk via numpy memmap) to shortlist candidate pages β€” the top page is shown.
59
  (Late-interaction visual ranking beat a 1B text rerank of the shortlist in the
60
  eval, 0.84 vs 0.68 hit@1, so the search tool takes ColEmbed's top page.)
 
61
  - **Parsed** β€” pages are parsed with
62
  [Nemotron Parse v1.2](https://huggingface.co/nvidia/NVIDIA-Nemotron-Parse-v1.2),
63
  figures and tables are described by MiniCPM-V, and heading-based section
64
  chunks are embedded with
65
- [Llama Nemotron Embed VL 1B v2](https://huggingface.co/nvidia/llama-nemotron-embed-vl-1b-v2)
66
- (used by the offline answer eval). On the Space the parsed pages supply the
67
- **whole-page text** the agent reasons over (`app/core/page_context.py`).
 
 
68
 
69
  ## Indexing (offline only)
70
 
 
48
  chapters plus a per-request fuzzy shortlist of fine parse headings
49
  (`app/core/sections.py`).
50
 
51
+ **Two indexes, one picked per turn** β€” the **Search index** setting chooses
52
+ which one the search tool ranks against (default **parsed**). A manual must be
53
+ indexed both ways regardless: the parsed index also supplies the page text the
54
+ agent reads and circles on.
55
 
56
  - **Visual** β€” every page is embedded as an image with
57
  [Nemotron ColEmbed v2](https://huggingface.co/nvidia/nemotron-colembed-vl-4b-v2)
 
60
  disk via numpy memmap) to shortlist candidate pages β€” the top page is shown.
61
  (Late-interaction visual ranking beat a 1B text rerank of the shortlist in the
62
  eval, 0.84 vs 0.68 hit@1, so the search tool takes ColEmbed's top page.)
63
+ Strongest on diagrams.
64
  - **Parsed** β€” pages are parsed with
65
  [Nemotron Parse v1.2](https://huggingface.co/nvidia/NVIDIA-Nemotron-Parse-v1.2),
66
  figures and tables are described by MiniCPM-V, and heading-based section
67
  chunks are embedded with
68
+ [Llama Nemotron Embed VL 1B v2](https://huggingface.co/nvidia/llama-nemotron-embed-vl-1b-v2).
69
+ The search tool scores the query against those chunks by dense cosine and
70
+ votes their parent pages β€” strongest on spec/table lookups, the default. The
71
+ parsed pages also supply the **whole-page text** the agent reasons over
72
+ (`app/core/page_context.py`).
73
 
74
  ## Indexing (offline only)
75
 
app.py CHANGED
@@ -61,6 +61,7 @@ from huggingface_hub import HfApi, snapshot_download
61
  from core.constants import (
62
  AGENT_MODELS,
63
  DEFAULT_AGENT_MODEL,
 
64
  DEFAULT_TOP_K,
65
  GROUND_ENABLE_THINKING,
66
  LIBRARY_DATASET_ID,
@@ -70,6 +71,7 @@ from core.constants import (
70
  PARSED_SUBDIR,
71
  PREINDEXED_DIR,
72
  RENDER_DPI,
 
73
  VISUAL_SUBDIR,
74
  )
75
  from core.pdf import pdf_outline, render_page_png
@@ -120,6 +122,8 @@ def _build_libraries():
120
  VISUAL_STORE, PARSED_STORE, PIPELINE = _build_libraries()
121
  # Valid agent-brain keys, for validating the per-request `agent_model`.
122
  _AGENT_MODEL_KEYS = {m["key"] for m in AGENT_MODELS}
 
 
123
  # method -> store, for the picker / pdf lookups. In mock both keys map to the
124
  # one MockStore, so every mock manual reads as indexed under both methods.
125
  _METHOD_STORES = {"visual": VISUAL_STORE, "parsed": PARSED_STORE}
@@ -268,6 +272,7 @@ def api_find(
268
  history: list = None,
269
  think: bool = GROUND_ENABLE_THINKING,
270
  agent_model: str = DEFAULT_AGENT_MODEL,
 
271
  vram_log: bool = False,
272
  session_id: str = "",
273
  ) -> dict: # the per-yield type: Server.api infers outputs from this annotation
@@ -305,16 +310,18 @@ def api_find(
305
  # the log reflects what actually ran).
306
  if agent_model not in _AGENT_MODEL_KEYS:
307
  agent_model = DEFAULT_AGENT_MODEL
 
 
308
  log.info(
309
- "find: manual=%s k=%s think=%s model=%s viewer=%s hist=%d q=%r",
310
- manual, k, bool(think), agent_model, viewer, len(history or []),
311
- request[:200],
312
  )
313
  try:
314
  events = PIPELINE.run_find(
315
  VISUAL_STORE, PARSED_STORE, request, [manual], int(k),
316
- viewer, history, bool(think), agent_model, bool(vram_log),
317
- str(session_id or ""),
318
  )
319
  for ev in events:
320
  if ev.get("type") == "tool_result" and "gallery" in ev:
@@ -365,6 +372,14 @@ def index():
365
  json.dumps([{"key": m["key"], "label": m["label"]} for m in AGENT_MODELS]),
366
  )
367
  .replace("__AGENT_MODEL__", DEFAULT_AGENT_MODEL)
 
 
 
 
 
 
 
 
368
  # Cache-bust key for /page images: changing the render DPI changes the
369
  # served page size, so it must change the URL too β€” otherwise a browser
370
  # could keep an old-resolution page (cached up to a day) under the same
 
61
  from core.constants import (
62
  AGENT_MODELS,
63
  DEFAULT_AGENT_MODEL,
64
+ DEFAULT_RETRIEVAL_MODE,
65
  DEFAULT_TOP_K,
66
  GROUND_ENABLE_THINKING,
67
  LIBRARY_DATASET_ID,
 
71
  PARSED_SUBDIR,
72
  PREINDEXED_DIR,
73
  RENDER_DPI,
74
+ RETRIEVAL_MODES,
75
  VISUAL_SUBDIR,
76
  )
77
  from core.pdf import pdf_outline, render_page_png
 
122
  VISUAL_STORE, PARSED_STORE, PIPELINE = _build_libraries()
123
  # Valid agent-brain keys, for validating the per-request `agent_model`.
124
  _AGENT_MODEL_KEYS = {m["key"] for m in AGENT_MODELS}
125
+ # Valid search-index keys, for validating the per-request `retrieval_mode`.
126
+ _RETRIEVAL_MODE_KEYS = {m["key"] for m in RETRIEVAL_MODES}
127
  # method -> store, for the picker / pdf lookups. In mock both keys map to the
128
  # one MockStore, so every mock manual reads as indexed under both methods.
129
  _METHOD_STORES = {"visual": VISUAL_STORE, "parsed": PARSED_STORE}
 
272
  history: list = None,
273
  think: bool = GROUND_ENABLE_THINKING,
274
  agent_model: str = DEFAULT_AGENT_MODEL,
275
+ retrieval_mode: str = DEFAULT_RETRIEVAL_MODE,
276
  vram_log: bool = False,
277
  session_id: str = "",
278
  ) -> dict: # the per-yield type: Server.api infers outputs from this annotation
 
310
  # the log reflects what actually ran).
311
  if agent_model not in _AGENT_MODEL_KEYS:
312
  agent_model = DEFAULT_AGENT_MODEL
313
+ if retrieval_mode not in _RETRIEVAL_MODE_KEYS:
314
+ retrieval_mode = DEFAULT_RETRIEVAL_MODE
315
  log.info(
316
+ "find: manual=%s k=%s think=%s model=%s search=%s viewer=%s hist=%d q=%r",
317
+ manual, k, bool(think), agent_model, retrieval_mode, viewer,
318
+ len(history or []), request[:200],
319
  )
320
  try:
321
  events = PIPELINE.run_find(
322
  VISUAL_STORE, PARSED_STORE, request, [manual], int(k),
323
+ viewer, history, bool(think), agent_model, retrieval_mode,
324
+ bool(vram_log), str(session_id or ""),
325
  )
326
  for ev in events:
327
  if ev.get("type") == "tool_result" and "gallery" in ev:
 
372
  json.dumps([{"key": m["key"], "label": m["label"]} for m in AGENT_MODELS]),
373
  )
374
  .replace("__AGENT_MODEL__", DEFAULT_AGENT_MODEL)
375
+ # Search-index picker: which index the search tool ranks against
376
+ # (visual / parsed) and the default, so the settings dropdown needs no
377
+ # extra round-trip on load.
378
+ .replace(
379
+ "__RETRIEVAL_MODES_JSON__",
380
+ json.dumps([{"key": m["key"], "label": m["label"]} for m in RETRIEVAL_MODES]),
381
+ )
382
+ .replace("__RETRIEVAL_MODE__", DEFAULT_RETRIEVAL_MODE)
383
  # Cache-bust key for /page images: changing the render DPI changes the
384
  # served page size, so it must change the URL too β€” otherwise a browser
385
  # could keep an old-resolution page (cached up to a day) under the same
core/constants.py CHANGED
@@ -171,6 +171,17 @@ AGENT_MAX_NEW_TOKENS = 128
171
  AGENT_MAX_STEPS = 6
172
  # ColEmbed shortlist size the search tool retrieves (the eval default).
173
  AGENT_SEARCH_CANDIDATES = 5
 
 
 
 
 
 
 
 
 
 
 
174
  # Past turns of conversation fed back as memory (resolve "the other one", "go
175
  # back"); the live turn carries the full current page text (no table of contents).
176
  AGENT_HISTORY_TURNS = 6
 
171
  AGENT_MAX_STEPS = 6
172
  # ColEmbed shortlist size the search tool retrieves (the eval default).
173
  AGENT_SEARCH_CANDIDATES = 5
174
+ # Which index the search tool ranks against. Both retrievers return the same
175
+ # (doc, page, score) shape, so the agent loop is identical either way:
176
+ # visual β€” ColEmbed late-interaction MaxSim over page-image embeddings
177
+ # parsed β€” Nemotron dense cosine over parsed section/figure/table chunks
178
+ # Exposed as a UI setting (settings panel); the parsed index wins on spec/table
179
+ # lookups, so it is the default for now.
180
+ RETRIEVAL_MODES = [
181
+ {"key": "parsed", "label": "Parsed (text)"},
182
+ {"key": "visual", "label": "Visual (ColEmbed)"},
183
+ ]
184
+ DEFAULT_RETRIEVAL_MODE = RETRIEVAL_MODES[0]["key"]
185
  # Past turns of conversation fed back as memory (resolve "the other one", "go
186
  # back"); the live turn carries the full current page text (no table of contents).
187
  AGENT_HISTORY_TURNS = 6
frontend/index.html CHANGED
@@ -355,6 +355,21 @@
355
  <p class="mt-1 text-xs text-brand-400">The model that finds pages and points. Switching loads it fresh β€” the first turn after a change is slower.</p>
356
  </div>
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  <!-- think: let the VLM reason before committing to a circle box -->
359
  <div class="mt-5 flex items-start justify-between gap-3">
360
  <div>
@@ -442,6 +457,10 @@ function repairGuy() {
442
  // the previous from VRAM server-side (load-on-switch), so the FIRST turn
443
  // after a change is slower while the new model loads.
444
  agentModels:__AGENT_MODELS_JSON__, agentModel:'__AGENT_MODEL__',
 
 
 
 
445
  // Diagnostics: when on, the server logs a GPU-memory snapshot at each step of
446
  // the turn (resident models, brain evict/load, the grounding spike). Sent as
447
  // `vram_log`; off by default since it's only for debugging the VRAM budget.
@@ -728,6 +747,7 @@ function repairGuy() {
728
  try{
729
  const job = this.client.submit('/find', {
730
  request:q, manual:this.manual, k:this.k, think:this.think, agent_model:this.agentModel,
 
731
  session_id:this.sessionId,
732
  page: this.viewDoc ? this.viewPage : 0, section: this.sectionTitle(this.viewPage),
733
  pages: this.spreadPages, // every page currently on screen β€” the agent may circle on any
 
355
  <p class="mt-1 text-xs text-brand-400">The model that finds pages and points. Switching loads it fresh β€” the first turn after a change is slower.</p>
356
  </div>
357
 
358
+ <!-- search index: which index the search tool ranks the query against -->
359
+ <div class="mt-5">
360
+ <label class="block text-xs font-semibold uppercase tracking-wide text-brand-500/80 mb-1.5">Search index</label>
361
+ <div class="relative">
362
+ <select x-model="retrievalMode"
363
+ class="w-full appearance-none rounded-xl border border-brand-200 bg-brand-50/50 px-3.5 py-2.5 pr-9 text-sm font-medium text-navy focus:border-brand-400 focus:ring-2 focus:ring-brand-100 outline-none transition">
364
+ <template x-for="m in retrievalModes" :key="m.key">
365
+ <option :value="m.key" x-text="m.label"></option>
366
+ </template>
367
+ </select>
368
+ <i data-lucide="chevron-down" class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-brand-400"></i>
369
+ </div>
370
+ <p class="mt-1 text-xs text-brand-400">What the search tool ranks against. Parsed (text) wins on specs and tables; Visual (ColEmbed) on diagrams.</p>
371
+ </div>
372
+
373
  <!-- think: let the VLM reason before committing to a circle box -->
374
  <div class="mt-5 flex items-start justify-between gap-3">
375
  <div>
 
457
  // the previous from VRAM server-side (load-on-switch), so the FIRST turn
458
  // after a change is slower while the new model loads.
459
  agentModels:__AGENT_MODELS_JSON__, agentModel:'__AGENT_MODEL__',
460
+ // The search-index picker: which index the search tool ranks against
461
+ // (visual / parsed), sent as `retrieval_mode` each turn. Parsed wins on
462
+ // spec/table lookups, visual on diagrams; initialized from the server default.
463
+ retrievalModes:__RETRIEVAL_MODES_JSON__, retrievalMode:'__RETRIEVAL_MODE__',
464
  // Diagnostics: when on, the server logs a GPU-memory snapshot at each step of
465
  // the turn (resident models, brain evict/load, the grounding spike). Sent as
466
  // `vram_log`; off by default since it's only for debugging the VRAM budget.
 
747
  try{
748
  const job = this.client.submit('/find', {
749
  request:q, manual:this.manual, k:this.k, think:this.think, agent_model:this.agentModel,
750
+ retrieval_mode:this.retrievalMode,
751
  session_id:this.sessionId,
752
  page: this.viewDoc ? this.viewPage : 0, section: this.sectionTitle(this.viewPage),
753
  pages: this.spreadPages, // every page currently on screen β€” the agent may circle on any
pipelines/agent_ask.py CHANGED
@@ -9,15 +9,18 @@ Flow (one @spaces.GPU call, streamed as events):
9
  figures/tables as their descriptions). No table of contents is injected. Then
10
  loop, up to AGENT_MAX_STEPS:
11
  decide β†’ ONE tool:
12
- search(query) ColEmbed (visual) top-N β†’ show the best page; its
13
- text is fed back so the agent can then circle on it (continues)
14
  circle(target) ground the target on the CURRENT page (VLM) and
15
  circle it (terminal)
16
  done(message) nothing to do / not in the manual (terminal)
17
 
18
- Retrieval is VISUAL: search uses ColEmbed page embeddings (visual store). The
19
- parsed store still supplies the page text the agent reads and circles on, so a
20
- manual must be indexed both ways.
 
 
 
21
 
22
  History is used only to resolve references, never to restate answers. Each turn
23
  is otherwise grounded in the viewer state the client sends (current page +
@@ -45,6 +48,7 @@ from core import tracing
45
  from core.constants import (
46
  AGENT_HISTORY_TURNS,
47
  AGENT_MAX_STEPS,
 
48
  FIND_GPU_DURATION,
49
  )
50
  from core.page_context import index_pages, page_to_text
@@ -52,6 +56,7 @@ from core.pdf import page_count, render_page
52
  from core.vram import log_vram, reset_peak, set_enabled
53
  from models import minicpm, minicpm_agent
54
  from models.colembed import maxsim_search
 
55
 
56
  log = logging.getLogger("repairguy.agent")
57
 
@@ -91,15 +96,18 @@ def agent_events(
91
  history: list | None = None,
92
  ground_thinking: bool | None = None,
93
  agent_model: str | None = None,
 
94
  vram_log: bool = False,
95
  session_id: str | None = None,
96
  ):
97
  """Yield the events of one agent turn (see module docstring). The agent gets
98
- no table of contents β€” it works from search (visual retrieval) and the
99
- current page's text. ground_thinking toggles MiniCPM-V's
100
- reasoning for the circle grounding (None β†’ server default); agent_model picks
101
- which brain drives the loop (None β†’ default), loaded on switch inside this
102
- GPU window. vram_log enables the per-turn VRAM probe (UI setting)."""
 
 
103
  # Apply the UI's VRAM-logging toggle for this turn. Done here (inside the GPU
104
  # worker) rather than in the parent so a reused ZeroGPU worker always honors
105
  # the current request's setting. When off, every log_vram/reset_peak below is
@@ -269,14 +277,20 @@ def agent_events(
269
  [(p, round(s, 3)) for _, p, s in hits])
270
  else:
271
  yield {"type": "status", "text": f"Searching for β€œ{query}”…"}
272
- # k (the viewer's slider) is the shortlist size; ColEmbed's top
273
- # page is the one shown. A 1B text rerank measured WORSE than raw
274
- # ColEmbed top-1 (0.68 vs 0.84 hit@1) β€” visual late interaction
275
- # already ranks these (figure-heavy) pages better than re-judging
276
- # from page text.
 
 
 
277
  with tracing.retriever("search", input=query,
278
- metadata={"retriever": "colembed", "k": int(top_k)}) as rsp:
279
- hits = maxsim_search(query, visual_store, doc_ids, top_k)
 
 
 
280
  if rsp is not None:
281
  rsp.update(output=[{"page": p, "score": round(float(s), 4)}
282
  for _, p, s in hits])
@@ -375,12 +389,14 @@ class AgentPipeline:
375
  history: list | None = None,
376
  ground_thinking: bool | None = None,
377
  agent_model: str | None = None,
 
378
  vram_log: bool = False,
379
  session_id: str | None = None,
380
  ):
381
  """One streamed agent turn (the event generator of agent_events).
382
- vram_log forwards the UI's VRAM-logging toggle to the probe; session_id
383
- groups a page session's turns together in Langfuse."""
 
384
  request = (request or "").strip()
385
  if not request:
386
  raise ValueError("Tell me what to find.")
@@ -391,5 +407,5 @@ class AgentPipeline:
391
  return agent_events(
392
  request, visual_store, parsed_store, doc_ids or list(names),
393
  int(top_k), names, viewer, history, ground_thinking,
394
- agent_model, vram_log, session_id,
395
  )
 
9
  figures/tables as their descriptions). No table of contents is injected. Then
10
  loop, up to AGENT_MAX_STEPS:
11
  decide β†’ ONE tool:
12
+ search(query) retrieve top-N β†’ show the best page; its text is fed
13
+ back so the agent can then circle on it (continues)
14
  circle(target) ground the target on the CURRENT page (VLM) and
15
  circle it (terminal)
16
  done(message) nothing to do / not in the manual (terminal)
17
 
18
+ The search tool ranks against whichever index `retrieval_mode` picks (a UI
19
+ setting): "visual" β€” ColEmbed late interaction over page-image embeddings;
20
+ "parsed" β€” Nemotron dense cosine over parsed chunks. Both retrievers return the
21
+ same (doc, page, score) shape, so the loop is identical either way. The parsed
22
+ store ALSO supplies the page text the agent reads and circles on, so a manual
23
+ must be indexed both ways regardless of mode.
24
 
25
  History is used only to resolve references, never to restate answers. Each turn
26
  is otherwise grounded in the viewer state the client sends (current page +
 
48
  from core.constants import (
49
  AGENT_HISTORY_TURNS,
50
  AGENT_MAX_STEPS,
51
+ DEFAULT_RETRIEVAL_MODE,
52
  FIND_GPU_DURATION,
53
  )
54
  from core.page_context import index_pages, page_to_text
 
56
  from core.vram import log_vram, reset_peak, set_enabled
57
  from models import minicpm, minicpm_agent
58
  from models.colembed import maxsim_search
59
+ from pipelines.parsed_ask import retrieve_pages
60
 
61
  log = logging.getLogger("repairguy.agent")
62
 
 
96
  history: list | None = None,
97
  ground_thinking: bool | None = None,
98
  agent_model: str | None = None,
99
+ retrieval_mode: str | None = None,
100
  vram_log: bool = False,
101
  session_id: str | None = None,
102
  ):
103
  """Yield the events of one agent turn (see module docstring). The agent gets
104
+ no table of contents β€” it works from search and the current page's text.
105
+ retrieval_mode picks the search index ("visual" | "parsed", None β†’ default).
106
+ ground_thinking toggles MiniCPM-V's reasoning for the circle grounding (None β†’
107
+ server default); agent_model picks which brain drives the loop (None β†’
108
+ default), loaded on switch inside this GPU window. vram_log enables the
109
+ per-turn VRAM probe (UI setting)."""
110
+ retrieval_mode = retrieval_mode if retrieval_mode in ("visual", "parsed") else DEFAULT_RETRIEVAL_MODE
111
  # Apply the UI's VRAM-logging toggle for this turn. Done here (inside the GPU
112
  # worker) rather than in the parent so a reused ZeroGPU worker always honors
113
  # the current request's setting. When off, every log_vram/reset_peak below is
 
277
  [(p, round(s, 3)) for _, p, s in hits])
278
  else:
279
  yield {"type": "status", "text": f"Searching for β€œ{query}”…"}
280
+ # k (the viewer's slider) is the shortlist size; the top page is
281
+ # the one shown. Two indexes can answer the query (UI setting):
282
+ # visual β€” ColEmbed late interaction over page images. A 1B text
283
+ # rerank measured WORSE than raw ColEmbed top-1 (0.68 vs 0.84
284
+ # hit@1), so the search tool takes ColEmbed's top page as-is.
285
+ # parsed β€” Nemotron dense cosine over chunks β†’ parent pages
286
+ # (wins on spec/table lookups). Same (doc, page, score) shape.
287
+ retriever_name = "colembed" if retrieval_mode == "visual" else "nemotron-embed"
288
  with tracing.retriever("search", input=query,
289
+ metadata={"retriever": retriever_name, "k": int(top_k)}) as rsp:
290
+ if retrieval_mode == "visual":
291
+ hits = maxsim_search(query, visual_store, doc_ids, top_k)
292
+ else:
293
+ hits = retrieve_pages(query, parsed_store, doc_ids, top_k)
294
  if rsp is not None:
295
  rsp.update(output=[{"page": p, "score": round(float(s), 4)}
296
  for _, p, s in hits])
 
389
  history: list | None = None,
390
  ground_thinking: bool | None = None,
391
  agent_model: str | None = None,
392
+ retrieval_mode: str | None = None,
393
  vram_log: bool = False,
394
  session_id: str | None = None,
395
  ):
396
  """One streamed agent turn (the event generator of agent_events).
397
+ retrieval_mode picks the search index ("visual" | "parsed"); vram_log
398
+ forwards the UI's VRAM-logging toggle to the probe; session_id groups a
399
+ page session's turns together in Langfuse."""
400
  request = (request or "").strip()
401
  if not request:
402
  raise ValueError("Tell me what to find.")
 
407
  return agent_events(
408
  request, visual_store, parsed_store, doc_ids or list(names),
409
  int(top_k), names, viewer, history, ground_thinking,
410
+ agent_model, retrieval_mode, vram_log, session_id,
411
  )
pipelines/mock_ask.py CHANGED
@@ -143,13 +143,14 @@ class MockAskPipeline:
143
  history: list | None = None,
144
  ground_thinking: bool | None = None,
145
  agent_model: str | None = None,
 
146
  vram_log: bool = False,
147
  session_id: str | None = None,
148
  ):
149
  """Yield the same event sequence as pipelines/agent_ask.py, with a
150
  keyword-driven stand-in for the agent's tool choice. Both stores are the
151
- one MockStore; history, ground_thinking, agent_model, vram_log and
152
- session_id are ignored. MOCK_DELAY (seconds) paces events."""
153
  request = (request or "").strip()
154
  if not request:
155
  raise ValueError("Tell me what to find.")
 
143
  history: list | None = None,
144
  ground_thinking: bool | None = None,
145
  agent_model: str | None = None,
146
+ retrieval_mode: str | None = None,
147
  vram_log: bool = False,
148
  session_id: str | None = None,
149
  ):
150
  """Yield the same event sequence as pipelines/agent_ask.py, with a
151
  keyword-driven stand-in for the agent's tool choice. Both stores are the
152
+ one MockStore; history, ground_thinking, agent_model, retrieval_mode,
153
+ vram_log and session_id are ignored. MOCK_DELAY (seconds) paces events."""
154
  request = (request or "").strip()
155
  if not request:
156
  raise ValueError("Tell me what to find.")