Sync from GitHub 97ec386
Browse files- README.md +10 -5
- app.py +20 -5
- core/constants.py +11 -0
- frontend/index.html +20 -0
- pipelines/agent_ask.py +36 -20
- 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 |
-
**
|
| 52 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 67 |
-
|
|
|
|
|
|
|
| 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,
|
| 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,
|
| 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)
|
| 13 |
-
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 99 |
-
|
| 100 |
-
reasoning for the circle grounding (None β
|
| 101 |
-
which brain drives the loop (None β
|
| 102 |
-
GPU window. vram_log enables the
|
|
|
|
|
|
|
| 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;
|
| 273 |
-
#
|
| 274 |
-
# ColEmbed
|
| 275 |
-
#
|
| 276 |
-
#
|
|
|
|
|
|
|
|
|
|
| 277 |
with tracing.retriever("search", input=query,
|
| 278 |
-
metadata={"retriever":
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 383 |
-
|
|
|
|
| 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,
|
| 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.")
|