feat: mmbench — agent-vs-agent multimodal eval system - #103
feat: mmbench — agent-vs-agent multimodal eval system#103nwaughachukwuma wants to merge 44 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the benchmark suite by reorganizing files into a more structured directory layout, updating script names, and improving path resolution logic for data and results directories. Specifically, it standardizes the calculation of DATA_DIR across several shell scripts and updates Python helper scripts to use absolute path resolution. A suggestion was made to further simplify the DATA_DIR construction in bench_universal.sh to avoid relative path segments.
| DATA_DIR="${SCRIPT_DIR}/data" | ||
| BENCH_DIR="${DATA_DIR}/universal-bench" | ||
| RESULTS_DIR="${SCRIPT_DIR}/universal_cli/run_results" | ||
| DATA_DIR="$(cd "${SCRIPT_DIR}/../data" && pwd 2>/dev/null || echo "${SCRIPT_DIR}/../data")" |
There was a problem hiding this comment.
The logic for determining DATA_DIR is robust for absolute paths, but it relies on cd succeeding to return a canonical path. If the directory does not yet exist, it falls back to a path containing ... While functional, consider using a more direct absolute path construction if the goal is to avoid relative path segments in the variable.
| DATA_DIR="$(cd "${SCRIPT_DIR}/../data" && pwd 2>/dev/null || echo "${SCRIPT_DIR}/../data")" | |
| DATA_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)/data" |
…, grader, and preflight
…t reference now points to hf
…onent - Updated index.html to load new JavaScript and CSS assets. - Removed SessionDetail import and related routing logic from App.svelte. - Enhanced CellDetail to toggle session details and fetch session data on demand. - Added expandable session detail rows in CellDetail with loading state and case breakdown. - Deleted the SessionDetail.svelte file as its functionality is now integrated into CellDetail.
…arameters - Updated index.html to load the new JavaScript bundle. - Modified App.svelte to parse and pass the open query parameter to CellDetail. - Enhanced CellDetail.svelte to manage open sessions based on the URL, ensuring the state is consistent with the URL. - Refactored toggle function to update the URL when sessions are opened or closed.
- Introduced a new API function `fetchTranscript` to retrieve transcripts based on session and case. - Updated `CellDetail.svelte` to include a dialog for viewing transcripts, with options for "With mm" and "Without mm". - Enhanced the UI to display loading states and handle cases where transcripts are not available. - Updated styles and structure for better readability and user experience.
- Updated the frontend to include a new ArtifactView component for displaying various artifact types (images, videos, audio, PDFs, and text). - Enhanced the CellDetail page to fetch and display artifacts related to a session, case, and arm. - Introduced new API functions to fetch case specifications and artifacts. - Modified the Assistant class to support streaming output from agents, allowing live output to be displayed in the terminal. - Implemented artifact persistence in the run process, saving agent-generated files for later review. - Updated the command-line interface to include a flag for enabling streaming output during runs.
- Updated the frontend to include a new "Log" tab in CellDetail.svelte, allowing users to view the mm invocation log. - Introduced a new function to format and display the mm log in a user-friendly manner. - Modified the AssistantResult class to include a full mm invocation log, capturing every command executed. - Enhanced the database schema to store the mm log for each case result. - Updated the primer documentation to reflect the new capabilities and usage of the mm command. - Ensured that the mm log is persisted and retrieved correctly during the orchestration of cases.
- Updated the frontend to reflect changes in log parsing and display. - Renamed transcript to stderr in various components to clarify its purpose. - Enhanced the logging mechanism in the assistant to capture exit codes and durations. - Adjusted database schema and data models to accommodate the new stderr field. - Improved user interface messages for clarity regarding results and logs.
…tting and readability in Leaderboard component
| codex) codex --version </dev/null >/dev/null 2>&1 ;; | ||
| gemini) gemini -p 'hi' </dev/null >/dev/null 2>&1 ;; | ||
| openclaw) openclaw -p 'hi' </dev/null >/dev/null 2>&1 ;; | ||
| opencode) opencode -prompt 'hi' </dev/null >/dev/null 2>&1 ;; |
There was a problem hiding this comment.
🔴 Opencode assistant probe uses wrong flag format, so it always fails and is silently skipped
The opencode reachability probe uses a single-dash flag (opencode -prompt 'hi' at benchmarks/bench_universal/bench_universal_tiny_metadata.sh:233) instead of the correct double-dash --prompt, while the same script's actual benchmark commands use --prompt (benchmarks/bench_universal/bench_universal_tiny_metadata.sh:279), so the probe always fails and opencode is incorrectly skipped from every metadata benchmark run.
Impact: Opencode is silently excluded from all metadata benchmark results even when it is properly installed and authenticated.
Inconsistency between probe and usage within the same script and across peer scripts
The check_assistant function at line 233 uses opencode -prompt 'hi' (single dash). In contrast:
- The same file's
assistant_cmdat line 279 usesopencode --prompt '${prompt}'(double dash) - The sister script
bench_universal_tiny_extractions.sh:235correctly usesopencode --prompt 'hi'(double dash) - The mmbench harness
mmbench/harness/assistants.py:55also uses the double-dash form
Since -prompt is not a valid flag (the CLI expects --prompt), the probe returns a non-zero exit code, and the probe_assistants loop at line 253-258 marks opencode as "unreachable (skipping)".
| opencode) opencode -prompt 'hi' </dev/null >/dev/null 2>&1 ;; | |
| opencode) opencode --prompt 'hi' </dev/null >/dev/null 2>&1 ;; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| local name="$1" | ||
| case "${name}" in | ||
| claude) claude -p 'hi' </dev/null >/dev/null 2>&1 ;; | ||
| codex) codex --version </dev/null >/dev/null 2>&1 ;; |
There was a problem hiding this comment.
🔴 Codex assistant probe only checks installation, not authentication, so unauthenticated runs proceed and fail
The codex reachability probe checks codex --version (benchmarks/bench_universal/bench_universal_tiny_metadata.sh:230) instead of running an actual query like codex -q 'hi', so the probe passes for any installed-but-unauthenticated codex, and the benchmark runs will then fail on every actual codex -q '...' command.
Impact: An unauthenticated codex is accepted during preflight and then fails on every benchmark task, wasting benchmark time and producing error results.
Inconsistency with all other scripts that probe codex
Every other script uses codex -q 'hi' to probe codex reachability:
bench_universal_tiny_extractions.sh:232:codex -q 'hi'bench_universal.sh:132:codex -q 'hi'helpers/lib_common.sh:144:codex -q 'hi'
The metadata script's own assistant_cmd at bench_universal_tiny_metadata.sh:276 generates commands with codex -q, confirming the probe is testing a different code path than actual usage.
| codex) codex --version </dev/null >/dev/null 2>&1 ;; | |
| codex) codex -q 'hi' </dev/null >/dev/null 2>&1 ;; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| vendor,date,total | ||
| CPB Software (Germany) GmbH,2024-03-01,453.53 | ||
| Google,2019-09-24,4647.68 |
There was a problem hiding this comment.
🚩 Unreferenced invoices.csv committed to repo root
A new file invoices.csv was added at the repository root with two sample invoice rows. No code in the diff or the broader repo references this file (confirmed via grep). It appears to be a test artifact or example data that was accidentally committed. While not a code bug, it adds clutter to the repo root and may confuse contributors.
Was this helpful? React with 👍 or 👎 to provide feedback.
| profile_model: "${PROFILE_MODEL}" | ||
| data_dir: "${BENCH_DIR}" | ||
| file_count: $(find "${BENCH_DIR}" -type f ! -name '.DS_Store' | wc -l | tr -d ' ') | ||
| total_size_bytes: $(find "${BENCH_DIR}" -type f ! -name '.DS_Store' -exec stat -f '%z' {} + 2>/dev/null | awk '{s+=$1}END{print s+0}' || find "${BENCH_DIR}" -type f ! -name '.DS_Store' -exec stat -c '%s' {} + 2>/dev/null | awk '{s+=$1}END{print s+0}') |
There was a problem hiding this comment.
🚩 stat -f '%z' fallback for total_size_bytes is Linux-incompatible
Both bench_universal_tiny_metadata.sh:582 and bench_universal_tiny_extractions.sh:613 compute total_size_bytes using stat -f '%z' (macOS) with a fallback to stat -c... (Linux). However, the diff shows the Linux fallback as truncated (-exec stat -c...). If the fallback is incomplete or malformed, the YAML header's total_size_bytes field will be wrong on Linux. Since this is cosmetic metadata in the YAML header (not used for grading or comparison), it's non-critical, but may produce invalid YAML on Linux.
Was this helpful? React with 👍 or 👎 to provide feedback.
…ounterparts using pi harness
Summary
Introduces
mmbench/, a self-contained agent-vs-agent benchmark system that measures whether giving an AI agent themmCLI makes it more capable and faster at real-world multimodal directory tasks.The core thesis is simple: run every agent twice per task — once with only its native tools (without_mm), once with
mmon PATH plus a one-page primer (with_mm) — and score both on correctness and speed. The headline numbers are lift (correctness delta) and speedup (wall-clock ratio).Table of contents
1. Overview
mmbench/adds ~9,000 lines across 52 files. The directory is structured as:2. Data source and datasets
Dataset: a frozen, nested, multimodal file fixture (
mmbench-agent/) with ~180 files (images, video, audio, PDFs, docs) spread across subfolders, pluscases.jsonl(the 20 case definitions). Hosted on Hugging Face Hub:The harness auto-downloads on first run via
ensure_dataset()→huggingface_hub.snapshot_download()intommbench/data/(gitignored). Subsequent runs reuse the local copy. HF auth (hf auth login) is required.The fixture is purpose-built from three reproducible sources. It includes a 15-page paper (deep PDF QA), invoices in PDF and image form, a video only readable with mm, OCR targets (one container photo among 150+), floor-plan classification sets, and structured extraction targets.
3. Cases, graders, sandboxes, and artifacts
Cases (
harness/cases.py)20 declarative cases in
cases.jsonl(data, not code). Each is a frozenEvalCasedataclass:Composition: ~10 retrieval, ~6 artifact, ~4 organization. Every
mmcommand (find,peek,wc,sql,grep,cat) is primary in ≥3 cases. Every modality is load-bearing in ≥3 cases. 14 hard / 6 medium.Grader (
harness/grader.py)Correctness is a 50/50 blend of two signals:
Deterministic checks (
CHECK_SPECS): partial-credit, weighted checks run against three surfaces:names_file,contains_number,contains_text— matched against the agent's final text output.path_exists,path_absent— matched against the sandbox's final directory state.artifact_exists,artifact_contains,artifact_row_count— matched against a file the agent wrote, parsed and compared to ground truth.LLM judge: a single 0-5 score from the judge model against the case's
judge_objective, grounded in itsground_truth. Retried up to 3 times; on persistent failure the run is voided (rows deleted) so no run mixes judged and checks-only cells.Additional signals:
task_completion(binary: agent produced output and didn't timeout),failure_mode(timeout|tool_error).Sandboxes (
harness/sandbox.py)Every
(assistant, profile, case, arm, run_index)runs in its own disposable copy of the dataset:--keep-sandboxesretains them).MM_DATA_DIRandMM_CACHE_DIR, so neither the DB nor the cache leaks across runs.Artifacts
Agent-written files (named by
artifact_*checks) are copied out of the sandbox before disposal intommbench/data/_artifacts/<session>/<case>/<arm>/for dashboard review. The FastAPI app serves them via/api/artifact-filewith path-traversal protection; the SvelteArtifactViewcomponent renders them inline (image, video, audio, PDF, or text) with download links.4. Harness, SQLite store, token usage tracking, and mm shims
Orchestrator (
harness/run.py)The unit of work is an (assistant, profile) cell. The orchestrator takes the cartesian product of
--assistantsand--profiles:Each cell is one session; its inner loop:
for case → for arm in [without_mm, with_mm] → run → grade → persist. Sessions accumulate for trend analysis. Supports--resume(reuse latest session, skip completed cells),--stream(tee agent stdout live), and--runs N(variance control; dashboard shows mean±std).SQLite store (
harness/store.py)Three-table schema modeling the result hierarchy:
sessions(session_id PK, assistant, profile_name, base_url, model, started_at, ended_at, status) → runs(run_id PK, session_id FK, run_index, started_at, ended_at, elapsed_s) → case_results(run_id FK, session_id FK, case_id, arm, correctness, checkpoint_score, judge_score, speed_s, task_completion, mm_used, mm_commands_used_json, failure_mode, final_output, stderr, mm_log, token_total, token_usage_json, mm_token_total, mm_token_usage_json, PK(run_id, case_id, arm))Indexed on
session_id,case_id,arm,(assistant, profile_name),started_at. Supportsvoid_run()to atomically discard a run when the judge fails mid-run.Token usage tracking
Two layers of token tracking per case result:
Agent token usage (
token_total,token_usage_json): the agent CLI's own LLM consumption (input, output, cache read/write, cost). Parsed from each agent's JSON output format byAgentOutputParser(see section 6).mm token usage (
mm_token_total,mm_token_usage_json): mm's own LLM token spend (prompt + completion acrossmm cat -m accuratecalls). Read post-run from mm's run-isolated SQLite cache (MM_DATA_DIR/mm.db→extractions.metadata.verbose_suffix), so it adds zero runtime overhead:mm PATH shims (
_mm_shim)The core isolation mechanism. A context manager creates a temp bin dir whose
mmentry is:mm, passes stdout/stderr/exit code through, and appends<exit_code>\t<duration_s>\t<args>to$MMBENCH_MM_LOG. mm-grounding is read from this log — reliable, agent-agnostic, no transcript parsing."mm: command not found"), so the agent genuinely has no mm.The shim dir is prepended to
PATH. Additionally,MM_DATA_DIRandMM_CACHE_DIRare set to per-run temp paths so mm's global DB and cache never leak across runs or sessions.5. Assistants, profiles, LLM judge, and custom judge setup
Assistants (
harness/assistants.py)Eight agent CLIs in the registry, each with its autonomy flag and output format:
Each
Assistant.run()builds the prompt (with_mm prepends the primer), creates the PATH shim environment, invokes the agent subprocess (buffered or streaming), reads the mm log, reads mm's token usage from the run's isolated DB, and parses the agent's structured output.Profiles (
harness/profiles.py)Two types of mm backend specs:
mm profile list(e.g.gateway). Resolved at runtime from the user's mm config.--profile.model/--profile.base-url/--profile.api-keymaterialize a throwawaymm.tomlin a temp dir, wired viaMM_CONFIG_DIR. The user's global config is untouched.Profiles only affect the with_mm arm (they set mm's backend); the without_mm arm never touches mm.
LLM judge and custom judge setup
Default judge:
google/gemini-3.1-flash-litevia OpenRouter (https://openrouter.ai/api/v1), key fromMMBENCH_JUDGE_API_KEYorOPENROUTER_API_KEY.Override per-run with all three flags:
Or via environment:
MMBENCH_JUDGE_MODEL,MMBENCH_JUDGE_BASE_URL,MMBENCH_JUDGE_API_KEY.The judge is a zero-temperature, structured-output call:
Retried up to 3 times. On persistent failure,
JudgeErroris raised and the orchestrator voids the run (deletes its rows) and halts — no run ever mixes judged and checks-only cells. Fix the judge endpoint and--resume.6. Primer, without_mm vs with_mm, preflights, and agent_output
Primer (
harness/primer.md)A one-page reference (141 lines) given only to the with_mm arm. It documents:
find/sql/grep; read binary →cat -m accurate; metadata →peek; budget →wc.The same task prompt goes to both arms verbatim; the only variable is mm availability.
without_mm vs with_mm
mmon PATHMM_PROFILE=<profile>MM_DATA_DIR+MM_CACHE_DIRmm_commands_usedPreflights (
harness/preflight.py)Run before any session. Four checks, in order:
cat <file>and echo a sentinel).No silent fallbacks. A failure aborts with a precise reason.
--checkruns only preflight and exits (for CI/setup validation).--skip-preflightbypasses (not advised).agent_output (
harness/agent_output.py)Normalizes each agent CLI's stdout into
AgentOutput(final_output, token_usage):result(text) +usage.{input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens}. Total recomputed to include cache.item.completedevents (text) +turn.completed(usage).response(text) +stats.models.*.tokens.{input, candidates, total, cached}.assistantevents withcontent[].text+usage.textevents (text) +step_finish(per-step tokens with cache).meta.finalAssistantVisibleText(text) +meta.agentMeta.usage.hermes sessions export.text_endevents (text) +message.usage.7. FastAPI app and Svelte frontend
FastAPI app (
app/app.py,app/db.py)Serves the prebuilt SPA and a JSON API at
http://localhost:9095:GET /api/leaderboardwithout_mm/with_mmarm stats,lift,speedup,n_sessions,n_runs.GET /api/sessionsGET /api/case-breakdownGET /api/cell?assistant=&profile=GET /api/session/{id}GET /api/transcript?session=&case=GET /api/case-spec?case=cases.jsonl.GET /api/artifacts?session=&case=&arm=GET /api/artifact-file?...db.pycontains the read-side aggregation queries. Hero metric:_lift_speedup(without_stats, with_stats). Pass threshold: correctness ≥ 60.Svelte frontend (
frontend/)Built with Svelte 5 (runes), Tailwind CSS, Chart.js, svelte-multiselect. Hash-based routing (
#/→ Leaderboard,#/cell/<assistant>/<profile>→ CellDetail).Leaderboard page (
Leaderboard.svelte):svelte-multiselectchips for assistants and mm profiles. Selections persisted in localStorage.CellDetail page (
CellDetail.svelte):ArtifactView: images, video, audio, PDF, text with download), Case spec (prompt + full case definition JSON).Components:
Chart.svelte: reactive Chart.js wrapper (auto-creates or updates on data change).InfoTip.svelte: click-toggle tooltip for column header explanations.ArtifactView.svelte: auto-detects file type by extension and renders inline (img, video, audio, PDF iframe, or text pre-block) with download link.8. How to try it
Prerequisites: an agent CLI installed and authed, an mm profile for the with_mm arm,
MMBENCH_JUDGE_API_KEY(orOPENROUTER_API_KEY) for the judge, andhf auth loginfor the private dataset.Other changes
fastapi>=0.110,uvicorn>=0.29,huggingface-hub>=0.25to[dev]dependencies.python/mm/config.py(update_config_keybackend assignment).[tool.ty.analysis].allowed-unresolved-importsfor optional extras (mlx,faster_whisper, etc.).Link to Devin session: https://app.devin.ai/sessions/0c4184b797aa4b6ca6699a179dad9300
Requested by: @nwaughachukwuma