Skip to content

Export evaluation run reports (JSON, HTML, Markdown) and Copy as Markdown - #621

Merged
TonsOfFun merged 3 commits into
mainfrom
feat/eval-report-export
Oct 21, 2026
Merged

TonsOfFun merged 3 commits into
mainfrom
feat/eval-report-export

Conversation

@TonsOfFun

@TonsOfFun TonsOfFun commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

A complete evaluation run can now leave the dashboard in three formats, and any piece of its report can be copied as Markdown for an issue or a pull request.

JSON export that round-trips through the existing import

  • GET <mount>/api/evaluation_reports/:run_id: authenticated with the ingest key (IngestAuthentication) and scoped to the tenant exactly like POST. It returns the version-1 envelope the POST takes. 404 for another tenant's run, 422 for a run that isn't complete or an unknown format.
  • How :run_id resolves:
    • Imported run: the run_id it was published under.
    • Run the dashboard executed: the stable dashboard-run-<id>-<created_at YYYYMMDDhhmmss UTC>. The timestamp keeps two installs' run Active Agent generate with stream #12 from colliding on a third install.
  • ActiveAgent::Evals::Publisher.envelope builds the envelope for both the publisher and the export (ActionAgent::EvaluationReportExport), so the hash layout lives in one place.
  • The report is EvaluationRun#to_report(estimate: false) with the recorded run-level judge usage, so the same run always exports the same envelope. The DB run id is left out of the metadata.
  • EvaluationReportImport treats a run's own export as an identical retry (200, duplicate: true). It covers both cases:
    • an imported run, where the export's digest differs from the stored digest of the original payload;
    • a dashboard run, found by its exported run_id, so posting it back creates nothing.
      Different content under the run_id is still a 409. The receipt's run_id is the exported one.
  • POST /api/evaluation_reports itself is unchanged.

Dashboard run report

  • …/runs/:run_id/report.json serves the same envelope to a dashboard session.
  • report.md serves the Markdown. ?part=verdict|fix_items[&model=]|scenario&scenario= serves one piece of it.
  • ?download=1 on any format sends Content-Disposition: attachment with a file name like <evaluation-slug>-run-<n>.html|json|md.

UI

  • The run report view has Download HTML / JSON / Markdown and Copy as Markdown next to Open standalone. The iframe gets allow="clipboard-write".
  • The suite's run header gets the same downloads plus a copy button.
  • There are Copy as Markdown buttons on:
    • the Models panel (verdict and comparison);
    • What to fix (follows the model filter);
    • each fix card (its Markdown comes with show_run's fix_items[].markdown);
    • each scenario drill-down.
  • The bundle is rebuilt.

Markdown (ActiveAgent::Evals::ReportMarkdown)

  • Report#to_markdown now has the HTML page's content in GitHub-flavored Markdown:
    • the verdict's rationale;
    • What to fix from fix_items, which replaces the old Recommendations list;
    • each scenario in a <details><summary>… block with a per-model table (status, score, tools, fault, cost) and the answers quoted.
  • Table cells escape | and fold line breaks. Text in summaries and answers escapes </>.
  • The pieces are public methods: verdict_markdown, fix_items_markdown(model:), fix_item_markdown(item) and scenario_markdown(key). The CLI, the endpoints, the dashboard buttons and the standalone HTML page all use these methods.
  • The standalone HTML page keeps its Markdown in hidden textareas. One small inline script copies it, falling back to execCommand("copy") when the Clipboard API is refused. Nothing is loaded from outside the page.

Rake

  • action_agent:evaluations:export RUN=<run_id> | EVALUATION=<id> RUN=<id> FORMAT=json|html|md [FILE=path] writes to standard output when no FILE is given.
  • action_agent:evaluations:import FILE=path [ACCOUNT_ID=…] goes through EvaluationReportImport and prints Stored: … or Already stored (identical retry): ….

Tests

  • New actionagent/test/evaluation_report_export_test.rb:
    • export → re-POST is an identical retry, for imported and dashboard runs;
    • moving a run to a fresh install and exporting it there gives the same envelope;
    • a modified export is a 409;
    • run_id values with dots work;
    • the API-key format=html page is the same page run_report renders, with absolute fix links, text/html and no theme;
    • download=1 on json/html/md, and an unknown format is a 422;
    • HTML and Markdown exports are covered by auth (401) and tenant isolation (404);
    • 404/422 cases;
    • ingest-key auth;
    • tenant isolation, for imported and dashboard runs;
    • .json/.md/download=1 headers and file names, and the Markdown pieces;
    • fix_items[].markdown;
    • the rake export (json/md/html) and import (stored, then identical retry).
  • test/evals/report_test.rb:
    • the <details> blocks, pipe/newline escaping and per-model What to fix;
    • each piece matching the whole report;
    • the HTML copy stores matching the Ruby Markdown;
    • one inline script and no external assets.
  • test/evals/publisher_test.rb: Publisher.envelope is what #call sends.
  • Frontend: reportExport.test.mjs (URLs, ClipboardItem path and fallback) and reportExportViews.test.mjs (the rendered buttons and links).

Results:

  • Follow-up commit (format=html, download=1): engine suite 1794 runs, 0 failures, 0 errors; test/evals 245 runs, 0 failures.
  • bin/test (gem + engine): 4056 runs, 0 failures, 5 errors. All 5 are RubyLLMIntegrationTest, which calls OpenAI live whenever OPENAI_API_KEY is set, and the local placeholder key gets a 401. The same 5 fail on a clean origin/main checkout with the same .env.test. CI names that key OPEN_AI_API_KEY, so the file skips there.
  • bin/lint: no offenses.
  • npm test: 698 pass. npm run build was rerun and its output committed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KhpgqyUrmSDK8cLDSheH3W

TonsOfFun and others added 3 commits October 21, 2026 15:15
…y piece as Markdown

A complete run comes back out as the version-1 envelope the collector at
<mount>/api/evaluation_reports takes, so it moves to another install with
one POST, and posting it back is an identical retry:

- GET <mount>/api/evaluation_reports/:run_id (ingest key, tenant-scoped like
  the POST). An imported run exports under its published run_id, a dashboard
  run under dashboard-run-<id>-<created_at>. EvaluationReportImport accepts a
  run's own export as identical to it.
- ActiveAgent::Evals::Publisher.envelope builds the envelope for both the
  publisher and the export (EvaluationReportExport).
- The dashboard's run report adds report.json and report.md (one piece with
  ?part=verdict|fix_items|scenario), and ?download=1 on any format.
- rake action_agent:evaluations:export / :import.

Report#to_markdown now carries the HTML page's content in GitHub-flavored
Markdown (ReportMarkdown): the verdict, What to fix from fix_items, and each
scenario's answers in a <details> block, with table cells escaped. Its
pieces are public and back "Copy as Markdown" in the dashboard and in the
standalone HTML page, which gains one inline script for it. Downloads sit
next to "Open standalone" and on the suite's run header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhpgqyUrmSDK8cLDSheH3W
GET <mount>/api/evaluation_reports/:run_id?format=html returns the
self-contained page run_report renders, with no theme (it follows the
viewer) and its fix actions linking into the dashboard by absolute path, so
a CLI can fetch a run's report with the ingest key. download=1 sends any
format as an attachment named like the dashboard's downloads; an unknown
format is a 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhpgqyUrmSDK8cLDSheH3W
…t key

The collector takes reports from an install with no ingest key, a mount
a host keeps off the network. A read of a stored run is different: its
prompts, answers and judge text would be open under a guessable run_id
to anyone who reaches the mount. GET /api/evaluation_reports/:run_id
now answers 401 on a single-tenant install until ActionAgent.ingest_api_key
is set; a multi-tenant install authenticates the tenant's key already.

The HTML and Markdown formats also refuse an incomplete run (422) as the
envelope did, instead of rendering a partial page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVSMckjfmJy2QWSb4wRrGh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants