Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions nemo_retriever/src/nemo_retriever/harness/portal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@

from nemo_retriever.harness import history
from nemo_retriever.harness.config import VALID_EVALUATION_MODES
from nemo_retriever.harness.contracts import MODE_TO_RUN_MODE

# Execution modes accepted by the current `retriever harness run --mode` surface
# (local, batch, service). Kept in sync with the harness contract so the portal
# never dispatches a job the runner cannot translate into a valid CLI invocation.
VALID_RUN_MODES = tuple(MODE_TO_RUN_MODE)
DEFAULT_RUN_MODE = "local"

mimetypes.add_type("text/javascript", ".jsx")

Expand Down Expand Up @@ -232,6 +239,9 @@ class TriggerRequest(BaseModel):
git_commit: str | None = None
nsys_profile: bool = False
graph_id: int | None = None
# Execution mode for the harness `--mode` flag: "local", "batch", or
# "service". ``run_mode`` is kept as a legacy alias for older clients.
mode: str | None = None
run_mode: str | None = None
service_url: str | None = None
service_max_concurrency: int | None = None
Expand Down Expand Up @@ -1521,6 +1531,54 @@ async def get_yaml_config():
return {"datasets": {}, "presets": {}, "active": {}}


@app.get("/api/harness-info")
async def get_harness_info():
"""Expose the current harness run contract so the UI stays in sync.

Reports the execution modes accepted by ``retriever harness run --mode``
and the code-owned benchmark/runset registry. The registry import is
intentionally lightweight (no ingest/query modules) so this endpoint stays
cheap to call from the trigger UI.
"""
modes = [
{"value": mode, "ingest_run_mode": MODE_TO_RUN_MODE[mode]}
for mode in sorted(MODE_TO_RUN_MODE)
]
benchmarks: list[dict[str, Any]] = []
runsets: list[dict[str, Any]] = []
try:
from nemo_retriever.harness.benchmark_registry import list_benchmarks, list_runsets

for spec in list_benchmarks():
benchmarks.append(
{
"name": spec.name,
"dataset": spec.dataset,
"tags": list(spec.tags),
"description": spec.description,
}
)
for runset in list_runsets():
runsets.append(
{
"name": runset.name,
"runs": list(runset.runs),
"tags": list(runset.tags),
"description": runset.description,
}
)
except Exception as exc: # pragma: no cover - registry import is best-effort
logger.warning("Failed to load harness benchmark registry: %s", exc)

return {
"default_mode": DEFAULT_RUN_MODE,
"modes": modes,
"evaluation_modes": sorted(VALID_EVALUATION_MODES),
"benchmarks": benchmarks,
"runsets": runsets,
}


# ---------------------------------------------------------------------------
# Managed Dataset CRUD
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -2228,8 +2286,18 @@ async def trigger_run(req: TriggerRequest):
req.git_commit,
)

if req.run_mode == "service":
merged_overrides["run_mode"] = "service"
# Resolve the execution mode against the harness contract. ``mode`` is the
# canonical field; ``run_mode`` is accepted as a legacy alias. Anything the
# current harness cannot map to ``retriever harness run --mode`` is rejected
# up front so a job never reaches a runner in an unrunnable state.
run_mode = (req.mode or req.run_mode or DEFAULT_RUN_MODE).strip().lower()
if run_mode not in MODE_TO_RUN_MODE:
raise HTTPException(
status_code=422,
detail=f"mode must be one of {sorted(MODE_TO_RUN_MODE)}, got {run_mode!r}",
)
merged_overrides["run_mode"] = run_mode
if run_mode == "service":
if req.service_url:
merged_overrides["service_url"] = req.service_url
if req.service_max_concurrency:
Expand Down
44 changes: 28 additions & 16 deletions nemo_retriever/src/nemo_retriever/harness/portal/mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
category="Jobs",
description=(
"Trigger a benchmark run on a dataset with an optional preset. "
"Returns the job ID and status. Use list_datasets and list_presets "
"first to discover valid names."
"Optionally set the execution mode ('local', 'batch', or 'service'; "
"defaults to 'local'). Returns the job ID and status. Use list_datasets "
"and list_presets first to discover valid names."
),
tags=["write", "jobs"],
)
Expand All @@ -37,32 +38,43 @@ def trigger_benchmark_run(
preset: str | None = None,
runner_id: int | None = None,
tags: list[str] | None = None,
mode: str | None = None,
) -> dict[str, Any]:
"""Trigger a benchmark run."""
from nemo_retriever.harness.contracts import MODE_TO_RUN_MODE
from nemo_retriever.harness.portal.app import (
DEFAULT_RUN_MODE,
_resolve_dataset_config,
_resolve_git_override,
_resolve_preset_overrides,
)

dataset_path, dataset_overrides = _resolve_dataset_config(dataset)
run_mode = (mode or DEFAULT_RUN_MODE).strip().lower()
if run_mode not in MODE_TO_RUN_MODE:
raise ValueError(f"mode must be one of {sorted(MODE_TO_RUN_MODE)}, got {run_mode!r}")

dataset_path, dataset_overrides, dataset_meta = _resolve_dataset_config(dataset)
preset_overrides = _resolve_preset_overrides(preset)
merged_overrides = {**(dataset_overrides or {}), **preset_overrides}
merged_overrides["run_mode"] = run_mode
pinned_sha, pinned_ref = _resolve_git_override(None, None)

job = history.create_job(
{
"trigger_source": "mcp",
"dataset": dataset,
"dataset_path": dataset_path,
"dataset_overrides": merged_overrides if merged_overrides else None,
"preset": preset,
"assigned_runner_id": runner_id,
"git_commit": pinned_sha,
"git_ref": pinned_ref,
"tags": tags or ["mcp-triggered"],
}
)
job_data: dict[str, Any] = {
"trigger_source": "mcp",
"dataset": dataset,
"dataset_path": dataset_path,
"dataset_overrides": merged_overrides if merged_overrides else None,
"preset": preset,
"assigned_runner_id": runner_id,
"git_commit": pinned_sha,
"git_ref": pinned_ref,
"tags": tags or ["mcp-triggered"],
}
if dataset_meta:
job_data["dataset_id"] = dataset_meta["dataset_id"]
job_data["dataset_config_hash"] = dataset_meta["dataset_config_hash"]

job = history.create_job(job_data)
return {"job_id": job["id"], "status": "pending"}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function DatasetFormModal({ dataset, onClose, onSaved }) {
beir_dataset_name: dataset?.beir_dataset_name || "",
beir_split: dataset?.beir_split || "test",
beir_query_language: dataset?.beir_query_language || "",
beir_doc_id_field: dataset?.beir_doc_id_field || "pdf_basename",
beir_doc_id_field: dataset?.beir_doc_id_field || "pdf_page",
beir_ks: (dataset?.beir_ks || [1,3,5,10]).join(", "),
embed_model_name: dataset?.embed_model_name || "",
embed_modality: dataset?.embed_modality || "text",
Expand Down Expand Up @@ -288,10 +288,14 @@ function DatasetFormModal({ dataset, onClose, onSaved }) {
<div>
<label style={labelStyle}>Input Type</label>
<select className="select" style={{width:'100%'}} value={form.input_type} onChange={e=>set('input_type',e.target.value)}>
<option value="auto">auto</option>
<option value="pdf">pdf</option>
<option value="doc">doc</option>
<option value="txt">txt</option>
<option value="html">html</option>
<option value="image">image</option>
<option value="text">text</option>
<option value="audio">audio</option>
<option value="video">video</option>
</select>
</div>
<div>
Expand Down Expand Up @@ -362,6 +366,7 @@ function DatasetFormModal({ dataset, onClose, onSaved }) {
<select className="select" style={{width:'100%'}} value={form.beir_doc_id_field} onChange={e=>set('beir_doc_id_field',e.target.value)}>
<option value="pdf_basename">pdf_basename</option>
<option value="pdf_page">pdf_page</option>
<option value="pdf_page_modality">pdf_page_modality</option>
<option value="source_id">source_id</option>
<option value="path">path</option>
</select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function RunnersView({ runners, loading, onRefresh, githubRepoUrl }) {
<tr><td colSpan="12" style={{textAlign:'center',padding:'60px',color:'var(--nv-text-muted)'}}>
<div style={{marginBottom:'8px',fontSize:'15px'}}>No runners registered</div>
<div style={{fontSize:'12px',color:'var(--nv-text-dim)'}}>
Register a runner manually or use <code className="mono" style={{background:'rgba(255,255,255,0.06)',padding:'2px 6px',borderRadius:'4px'}}>retriever harness runner start --manager-url &lt;portal-url&gt;</code>
Register a runner with the button above, then point your runner agent at this portal so it can poll the runner work API for jobs.
</div>
</td></tr>
) : pg.pageData.map(r => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ function RunsView({ runs, datasets, loading, filterDataset, setFilterDataset, fi
<tr><td colSpan="13" style={{textAlign:'center',padding:'60px',color:'var(--nv-text-muted)'}}>
<div style={{marginBottom:'8px',fontSize:'15px'}}>No runs found</div>
<div style={{fontSize:'12px',color:'var(--nv-text-dim)'}}>
Trigger a run or use <code className="mono" style={{background:'rgba(255,255,255,0.06)',padding:'2px 6px',borderRadius:'4px'}}>retriever harness backfill</code> to import existing results.
Trigger a run from the portal, or run <code className="mono" style={{background:'rgba(255,255,255,0.06)',padding:'2px 6px',borderRadius:'4px'}}>retriever harness run &lt;benchmark&gt;</code> on a runner to record results here.
</div>
</td></tr>
) : pg.pageData.map(run => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ function TriggerModal({ onClose, onTriggered }) {
const [dataset, setDataset] = useState("");
const [preset, setPreset] = useState("");
const [pipelineMode, setPipelineMode] = useState("preset");
const [runMode, setRunMode] = useState("local");
const [modeOptions, setModeOptions] = useState(["local", "batch", "service"]);
const [graphId, setGraphId] = useState("");
const [runnerId, setRunnerId] = useState("");
const [submitting, setSubmitting] = useState(false);
Expand All @@ -30,6 +32,11 @@ function TriggerModal({ onClose, onTriggered }) {
if (cfg.datasets?.length) setDataset(cfg.datasets[0]);
if (cfg.presets?.length) setPreset(cfg.presets[0]);
});
fetch("/api/harness-info").then(r=>r.json()).then(info => {
const modes = (info.modes || []).map(m => m.value).filter(Boolean);
if (modes.length) setModeOptions(modes);
if (info.default_mode) setRunMode(info.default_mode);
}).catch(()=>{});
fetch("/api/runners").then(r=>r.json()).then(setRunners).catch(()=>{});
fetch("/api/graphs").then(r=>r.json()).then(list => {
const arr = Array.isArray(list) ? list : [];
Expand Down Expand Up @@ -61,12 +68,12 @@ function TriggerModal({ onClose, onTriggered }) {
preset: pipelineMode === "preset" ? (preset || null) : null,
runner_id: runnerId ? parseInt(runnerId, 10) : null,
nsys_profile: nsysProfile,
mode: runMode,
};
if (pipelineMode === "graph") {
payload.graph_id = parseInt(graphId, 10);
}
if (pipelineMode === "service") {
payload.run_mode = "service";
if (runMode === "service") {
payload.service_url = serviceUrl.trim();
payload.service_max_concurrency = serviceMaxConcurrency;
}
Expand All @@ -93,13 +100,20 @@ function TriggerModal({ onClose, onTriggered }) {

const labelStyle = {display:'block',fontSize:'12px',fontWeight:500,color:'var(--nv-text-muted)',marginBottom:'6px',textTransform:'uppercase',letterSpacing:'0.04em'};
const hintStyle = {fontSize:'11px',color:'var(--nv-text-dim)',marginTop:'4px',lineHeight:'1.5'};
const modeBtn = (id, label) => ({
const toggleBtn = (active) => ({
fontSize:'11px',padding:'5px 12px',flex:1,justifyContent:'center',textAlign:'center',
background: pipelineMode===id ? 'rgba(118,185,0,0.12)' : 'transparent',
color: pipelineMode===id ? 'var(--nv-green)' : 'var(--nv-text-dim)',
border: `1px solid ${pipelineMode===id ? 'rgba(118,185,0,0.3)' : 'var(--nv-border)'}`,
cursor:'pointer', borderRadius:'6px', fontWeight: pipelineMode===id ? 600 : 400,
background: active ? 'rgba(118,185,0,0.12)' : 'transparent',
color: active ? 'var(--nv-green)' : 'var(--nv-text-dim)',
border: `1px solid ${active ? 'rgba(118,185,0,0.3)' : 'var(--nv-border)'}`,
cursor:'pointer', borderRadius:'6px', fontWeight: active ? 600 : 400,
});
const modeBtn = (id) => toggleBtn(pipelineMode===id);
const runModeLabels = { local: "Local", batch: "Batch", service: "Service" };
const runModeHints = {
local: "In-process ingest/query on the runner. Best for smoke and small BEIR datasets.",
batch: "Ray-backed batch ingest. Use for large BEIR corpora (BO767, FinanceBench, Earnings, ViDoRe).",
service: "Runs against an already-deployed Retriever service. No GPU or Ray cluster needed on the runner.",
};

const selectedGraph = graphs.find(g => String(g.id) === graphId);

Expand All @@ -119,6 +133,22 @@ function TriggerModal({ onClose, onTriggered }) {
</select>
</div>

{/* Execution Mode Toggle — maps to `retriever harness run --mode` */}
<div>
<label style={labelStyle}>Execution Mode</label>
<div style={{display:'flex',gap:'6px'}}>
{modeOptions.map(m => (
<button key={m} type="button" onClick={()=>{
setRunMode(m);
if (m === "service" && pipelineMode === "graph") setPipelineMode("preset");
}} className="btn btn-sm" style={toggleBtn(runMode===m)}>
{runModeLabels[m] || m}
</button>
))}
</div>
<div style={hintStyle}>{runModeHints[runMode] || ""}</div>
</div>

{/* Pipeline Mode Toggle */}
<div>
<label style={labelStyle}>Pipeline</label>
Expand All @@ -127,18 +157,15 @@ function TriggerModal({ onClose, onTriggered }) {
Preset
</button>
<button type="button" onClick={()=>setPipelineMode("graph")} className="btn btn-sm"
style={modeBtn("graph")} disabled={graphs.length===0}>
style={modeBtn("graph")} disabled={graphs.length===0 || runMode==="service"}>
Graph Pipeline
</button>
<button type="button" onClick={()=>setPipelineMode("service")} className="btn btn-sm" style={modeBtn("service")}>
Service
</button>
</div>
{graphs.length === 0 && pipelineMode === "preset" && (
<div style={hintStyle}>No saved graphs available. Create one in the Designer view to enable graph pipeline runs.</div>
)}
{pipelineMode === "service" && (
<div style={hintStyle}>Uploads documents to a running retriever service and measures ingestion throughput. No GPU or Ray cluster needed on the runner.</div>
{runMode === "service" && (
<div style={hintStyle}>Graph pipelines are not available in service mode; runs use the deployed service's ingest/query APIs.</div>
)}
</div>

Expand Down Expand Up @@ -168,7 +195,7 @@ function TriggerModal({ onClose, onTriggered }) {
</div>
)}

{pipelineMode === "service" && (
{runMode === "service" && (
<div style={{display:'flex',flexDirection:'column',gap:'12px'}}>
<div>
<label style={labelStyle}>Service URL</label>
Expand Down Expand Up @@ -307,8 +334,8 @@ function TriggerModal({ onClose, onTriggered }) {
</div>
<div className="modal-foot">
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button type="submit" disabled={submitting||!dataset||(pipelineMode==='graph'&&!graphId)||(pipelineMode==='service'&&!serviceUrl.trim())} className="btn btn-primary" style={{flex:1,justifyContent:'center'}}>
{submitting ? <><span className="spinner" style={{marginRight:'8px'}}></span>Triggering…</> : (pipelineMode==='graph' ? 'Run Graph Pipeline' : pipelineMode==='service' ? 'Run Service Ingest' : 'Start Run')}
<button type="submit" disabled={submitting||!dataset||(pipelineMode==='graph'&&!graphId)||(runMode==='service'&&!serviceUrl.trim())} className="btn btn-primary" style={{flex:1,justifyContent:'center'}}>
{submitting ? <><span className="spinner" style={{marginRight:'8px'}}></span>Triggering…</> : (pipelineMode==='graph' ? 'Run Graph Pipeline' : runMode==='service' ? 'Run Service Ingest' : runMode==='batch' ? 'Start Batch Run' : 'Start Run')}
</button>
</div>
</form>
Expand Down
Loading