Add EvalPortReport: export EvaluationReport as an EvalPort ResultSet - #369
Add EvalPortReport: export EvaluationReport as an EvalPort ResultSet#369adhabnr-ux wants to merge 12 commits into
Conversation
Adds openagent_eval/reports/evalport.py, a ReportGenerator implementation that converts a completed EvaluationReport into an EvalPort (https://github.com/adhabnr-ux/evalport) ResultSet -- the open, tool-agnostic JSON result format shared across DeepEval, Promptfoo, Inspect AI, AutoGen, CrewAI, Ragas, LangSmith, Braintrust, MLflow, Opik, and TruLens. Implements the design agreed in Discussion OpenAgentHQ#296 with @Himanshu-kumar: - Pass/fail is derived via an optional evalport_thresholds (metric name -> threshold) mapping, defaulting to 0.5 for any metric not listed, since OpenAgent Eval's metrics are bare [0.0, 1.0] scores with no native pass/fail concept. Every derived result is flagged transparently via metadata.openeval_derived_pass = true. - test_case_id reads the dataset item's optional `id` field (preserved into EvaluationResult.metadata["id"] by Pipeline._evaluate_item), falling back to a positional f"{run_id}_item_{i}". - Strictly one-directional (EvaluationReport -> ResultSet); no from_openeval, since OpenAgent Eval's own dataset loading already has its own established shape. - metrics -> GraderResult, answer -> actual_output, metadata["latency_ms"] -> duration_ms, run metadata/summary -> the ResultSet's own top-level fields. Also: - Represents each PipelineResult.errors entry as its own failed Result (EvalPort's schema has no concept of an item that was never evaluated). - Preserves question/ground_truth/contexts under metadata.openagent_eval, since EvalPort's Result schema has no dedicated fields for them. - Adds the optional `evalport` extra (evalport-sdk) for validating output against openeval.validate.validate_result_set. - Exports EvalPortReport from openagent_eval.reports, alongside the other built-in generators. - Adds a full test suite (tests/unit/test_reports/test_evalport_report.py) that validates every generated ResultSet against the real openeval.validate.validate_result_set -- not a mock or hand-rolled schema check. - Documents the new format in docs/reports-output-formats.md and adds a CHANGELOG entry. This is a standalone, directly-importable ReportGenerator rather than one wired into the CLI's --output flag or config.models.OutputFormat, keeping this first version scoped to exactly what was asked for. Wiring it into --output evalport is a natural, separately-scoped follow-up once this conversion itself has been reviewed against real `oaeval run` output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
The previous commit introduced whitespace drift in the pre-existing Terminal Report sample-output box-drawing borders (unrelated to the new EvalPort section) during a copy/paste. Re-push via base64 encoding to guarantee byte-for-byte fidelity with the local, verified source. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
…d (retry) The previous base64-encoded fix (100a282) still left one box-drawing border line one character short. This commit's content was extracted directly from the local git blob (0cc6f46) via `git cat-file -p`, base64-encoded with `base64 -w0`, and roundtrip sanity-checked (decode + git hash-object match) before pushing, to eliminate manual-retype risk for the Unicode box-drawing art. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
Content uploaded as a real file via GitHub's web upload flow (not typed into a tool call), verified byte-identical to the local source (git hash-object 0cc6f46) before upload, to eliminate the transcription risk that caused two prior fix attempts (100a282, b52d47e) to still leave single-character drift in dense Unicode box-drawing runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
|
(Correcting a typo in the PR description above — meant to tag @himanshu231204, not "himanshu-kumar". Apologies for the mix-up.) |
Critical Issues — Block Merge1. Double-counting of failed items corrupts the ResultSetFile: The adapter iterates In
So every failed item appears in both lists. The adapter outputs it twice: once as a zeroed-metrics result, once as a synthetic error entry. For a 10-item run with 3 failures, the ResultSet has 13 entries, The tests mask this because Fix options:
Either way: add a test whose fixture mirrors the real pipeline (a failed item present in both 2. Wrong GitHub username in module docstringFile: The design attribution reads Important Issues — Should Fix Before Merge3.
|
| Gap | Criticality |
|---|---|
run_id from report.metadata["run_id"] branch never executed |
High |
suite_id fallback to "openagent_eval_run" (no dataset path) untested |
High |
answer is None → no actual_output key path untested |
High |
started_at / completed_at override params never passed in any test |
High |
default_threshold as an independent parameter untested |
Medium |
_result_metadata with no question / ground_truth / contexts (returns {}) untested |
Medium |
Positional test_case_id fallback for successful results ({run_id}_item_{i}) untested |
Medium |
Error entry test_case_id from item.metadata.id path untested |
Medium |
Suggestions
import jsoninsidegenerate()— move to module level to match other generators in this package_now_iso()strips sub-second precision without documentation;json_report.pypreserves microseconds via.isoformat(), creating an inconsistencyround(latency_ms)will raiseTypeErrorfor non-numeric values injected via datasetmetadataspread — wrap intry/except (TypeError, ValueError)indent < 0silently produces compact output (same asJSONReport) — either document or add a guard
Strengths
- Validating against the real
openeval.validate.validate_result_set()rather than hand-rolling schema checks is the right call - The
openeval_derived_passtransparency flag is principled and useful for downstream consumers _clamp_unitdefensive clamping is correctly scoped and documentedTYPE_CHECKINGimports are used correctly — no runtime import issuesgenerate_to_filepath normalization exactly matchesJSONReport's conventionto_result_set()exposing the unserialised dict is a useful addition- 34 tests with real SDK validation is solid foundation
Recommended Action
Not ready to merge. The double-counting bug (#1) is a data integrity failure that produces silently-wrong ResultSets against every real oaeval run, the justifying comment is factually inverted, and the tests only avoid it because the fixture diverges from real pipeline output. Fixing #1 and #3 together (same loop region), correcting the attribution (#2), addressing the metadata/timestamp gaps (#4, #5), adding threshold validation (#6), and covering the high-criticality test gaps should all land together before this is merged. Happy to iterate quickly once those are addressed.
…eview) @himanshu231204's review of PR OpenAgentHQ#369 identified that evaluation_report_to_result_set() built two Result entries for every pipeline failure: one from the zeroed EvaluationResult that Pipeline._evaluate_item's exception boundary already places in PipelineResult.results, and a second synthetic one from walking PipelineResult.errors. This silently corrupted summary.total and summary.pass_rate on any run with failures. Fix: - pipeline.py: the exception boundary's returned EvaluationResult now also spreads item.get("metadata", {}) and records error_type, so a failed item preserves its dataset id/custom metadata exactly like a successful item does (previously only the success path did this). - evalport.py: failures are now sourced from PipelineResult.results alone via metadata["failed"]; the old loop over PipelineResult.errors is removed entirely. This also sidesteps a real ordering hazard -- errors is appended to from inside each item's own coroutine, so under the parallel executor its order reflects completion time, not dataset position, making it unsafe to zip against results by index. - Also fixes, per the same review: wrong contributor handle in the module docstring (@Himanshu-kumar -> @himanshu231204), unverifiable package citations removed, range validation added for default_threshold/evalport_thresholds, started_at/completed_at now exposed on EvalPortReport.__init__, silent OPENEVAL_VERSION fallback now logs a warning, module-level `import json` instead of inline, round(latency_ms) guarded against non-numeric input, and a leaked `"title": null` key is now omitted when the report has no title. - test_evalport_report.py: adds a `realistic_pipeline_result` fixture shaped exactly like real Pipeline.execute() output (each failure appearing in both results and errors, with errors deliberately out of dataset order) plus a TestFailedItemRepresentation class with 6 regression tests for the double-counting fix, and coverage for every other point above. - docs/reports-output-formats.md + CHANGELOG.md updated to match. Verified locally: 1190 passed, 6 skipped (full suite, up from the PR's original 1177 -- 13 new tests added), ruff check clean, ruff format clean, ruff format --check clean. mypy on the two changed files surfaces one pre-existing pipeline.py:386 sum() arg-type note that predates this change (confirmed via `git stash` + mypy on the unmodified file) and is unrelated to it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
…docs Second half of the previous pipeline.py commit -- the exporter side of the fix for @himanshu231204's PR OpenAgentHQ#369 review (point 1, "Block Merge"): evaluation_report_to_result_set() no longer walks PipelineResult.errors to build a second failed Result for every item already represented as a zeroed, metadata["failed"]=True EvaluationResult in PipelineResult.results. Failures are now sourced from results alone. Also addresses the review's other points: wrong contributor handle fixed (@Himanshu-kumar -> @himanshu231204), unverifiable package citations removed, default_threshold/evalport_thresholds range validation added, started_at/completed_at exposed on EvalPortReport.__init__, OPENEVAL_VERSION import-fallback now logs a warning instead of failing silently, `import json` moved to module level, round(latency_ms) guarded against non-numeric input, and a leaked `"title": null` key is now omitted when absent. test_evalport_report.py adds a realistic_pipeline_result fixture shaped exactly like real Pipeline.execute() output (each failure appearing in both results and errors, errors deliberately out of dataset order) and a TestFailedItemRepresentation class with 6 regression tests, plus coverage for every point above. docs/reports-output-formats.md and CHANGELOG.md updated to match. Verified locally: 1190 passed, 6 skipped (full suite), ruff check clean, ruff format --check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
Third piece of the PR OpenAgentHQ#369 review fix (see the two prior commits on this branch): docs/reports-output-formats.md's "Mapping" bullet and "Sample Output" JSON for the EvalPort Report section described the old, double-counting behavior (a separate failed Result built from PipelineResult.errors, with a "run_id_error_N"-style test_case_id). Both are updated to describe and demonstrate the corrected behavior (failures sourced from PipelineResult.results alone, positional test_case_id falls back to "{run_id}_item_{i}"). CHANGELOG.md gets a Fixed entry under [Unreleased] alongside the existing EvalPortReport Added entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
…s.md The previous commit on this branch (f2d9c8e) pushed this file's full content through a hand-typed API call and silently corrupted the Terminal Report sample's Unicode box-drawing characters -- the exact same failure mode this branch's own d4cd959 ("Fix box-drawing corruption ... (final)") already diagnosed and fixed once by avoiding retyped content entirely. This restores the file from that known-good blob (0cc6f46) and reapplies only the two intended semantic edits (the EvalPort Report "Mapping" bullet and "Sample Output" JSON, both describing the double-counting fix from the two prior commits) via a Python string replace against the clean base -- not by retyping the file -- so nothing outside those two hunks changes. This time the content is sent base64-encoded rather than as a raw UTF-8 string, specifically to eliminate the corruption vector: base64 is plain ASCII, so it cannot suffer the same lookalike-character transcription error that hit the raw-UTF-8 push. Verified locally before sending: `base64 -d | git hash-object --stdin` reproduces the exact intended blob sha (f167b9a). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F
|
Thanks for the thorough review, @himanshu231204 — and sorry for misattributing it to 1. Double-counting (Block Merge) — fixed. You were right: As part of this I also patched 2. Wrong contributor handle — fixed, 3–8 (unverifiable citations, missing threshold validation,
Also fixed while in there (not in your numbered list, but adjacent bugs the same investigation surfaced):
Test coverage: rewrote Verification: full suite — 1190 passed, 6 skipped (up from this PR's original 1177; the 13 new tests account for the difference),
One process note for anyone reading this thread later: my first two attempts at pushing the docs update corrupted the Unicode box-drawing characters in the Terminal Report sample (same failure mode All commits are on |
Implements the
EvaluationReport -> EvalPort ResultSetexporter design from Discussion #296, per @Himanshu-kumar's design decisions there and his go-ahead to open this as a draft:What this adds
EvalPortReport(openagent_eval/reports/evalport.py), aReportGeneratorsubclass that exports a completedEvaluationReportas an EvalPortResultSet— an open, tool-agnostic JSON format for evaluation results already used by DeepEval, Promptfoo, Inspect AI, AutoGen, CrewAI, and others. This lets any EvalPort-speaking dashboard, comparison tool, or CI gate consume OpenAgent Eval's results without a bespoke integration.Per the Discussion #296 design:
evalport_thresholds: an optionaldict[str, float]mapping metric name → pass threshold, defaulting to0.5for any metric not listed (DEFAULT_PASS_THRESHOLD). Since OpenAgent Eval's metrics are bare[0.0, 1.0]scores with no native pass/fail concept, the derivedpassedboolean on everyGraderResult/Resultis flagged viametadata.openeval_derived_pass: true, so a downstream consumer can always distinguish an inferred pass/fail from a tool-native one.test_case_id: reads the dataset item's optionalidfield (viaEvaluationResult.metadata["id"], which is howPipeline._evaluate_item()already surfaces per-item metadata) when present, falling back to a positionalf"{run_id}_item_{i}"otherwise.EvaluationReport -> ResultSetonly. There is nofrom_openeval— OpenAgent Eval's own dataset loading already has an established shape this adapter intentionally doesn't replace.metrics -> GraderResult(one per metric,grader_id= metric name,type="custom"),answer -> actual_output,metadata["latency_ms"] -> duration_ms(rounded to the nearest ms), and run metadata (engine, version, title, config) ->ResultSet.metadata.openagent_eval.question/ground_truth/contextsare preserved undermetadata.openagent_evalper-result, since EvalPort'sResultschema has no dedicated fields for them (confirmed against the realevalport-sdkdataclasses — earlier drafts of this PR mistakenly assumedResult.input/expected_outputandGraderResult.paramsexisted; they don't, and this was corrected before anything shipped).PipelineResult.errors— items that failed before anEvaluationResultwas even constructed): each becomes its own failedResultwitherrorpopulated and emptygrader_results, since EvalPort's schema has no concept of an item that was never evaluated. This wasn't explicitly specified in the discussion but felt like the right way to avoid silently dropping failed items from the exported set.Why it's not wired into
--outputyetUnlike Terminal/Markdown/HTML/JSON, this generator isn't in
get_report_generator()'s--outputdispatch. Producing a validResultSetrequires apassedboolean per grader, and OpenAgent Eval's metrics have no single threshold that's obviously correct for every metric — so invoking it needs the caller to make (or explicitly accept the default for) that judgment call. It's fully usable directly from the SDK today; docs on that are included.Testing
tests/unit/test_reports/test_evalport_report.py, validating every generatedResultSetagainst the realopeneval.validate.validate_result_set()(viapytest.importorskip("openeval.validate")), not just asserting on OpenAgent Eval's own output shape.dev,evalport,corpus,pdf,datasets,providers), zero regressions.ruff check/ruff format --check: clean.mypy --ignore-missing-imports openagent_eval/reports/evalport.py: clean.docs/reports-output-formats.md, new "5. EvalPort Report" section) include real, actually-executed sample output, validated against the SDK validator before being embedded — not fabricated.All of the above was run against the exact commit this PR proposes (verified via a git worktree checkout of this branch's tip, not just the working copy used during development).
New optional
evalportextra added topyproject.toml(pip install openagent-eval[evalport]) forevalport-sdk, used for validation and by the docs example — not required to importopenagent_eval.reports.evalportitself, which degrades gracefully to a bundledOPENEVAL_VERSIONfallback if the SDK isn't installed.Ready for review against real
oaeval runoutput whenever convenient — happy to iterate on anything that doesn't match real-world usage.🤖 Generated with Claude Code
https://claude.ai/code/session_01RtocdH3tKifGdkiCZxxV3F