Add trulens-openeval: to_openeval()/from_openeval() for EvalPort interchange - #2697
Add trulens-openeval: to_openeval()/from_openeval() for EvalPort interchange#2697adhabnr-ux wants to merge 13 commits into
Conversation
…rchange Implements the module discussed and approved in truera#2680: converts between TruLens Run records (Run.get_records()/get_record_details()) and EvalPort (https://github.com/adhabnr-ux/evalport) suites/result sets. - to_openeval(records_df, ...): Run records + feedback columns -> EvalPort ResultSet - from_openeval(suite, ...): EvalPort suite -> input_df + dataset_spec for RunConfig 17 tests, all passing locally against the real trulens-core package and the real openeval.validate.validate_result_set()/validate_suite() (evalport-sdk on PyPI) -- not mocks. Packaged the same way as src/hotspots (poetry, importable as trulens.openeval), tests living under the repo's own tests/unit/ per existing convention. Converts at the DataFrame boundary rather than requiring a live Run/RunConfig object, since Run needs a live RunDaoBase/TruSession/app instance just to construct -- see the README's "Why this converts DataFrames" section for the full reasoning (confirmed by reading trulens/core/run.py directly). Closes truera#2680.
The previous commit landed pyproject.toml, README.md, py.typed, and the tests, but omitted the actual to_openeval()/from_openeval() implementation module. This completes the package.
…is open The pending-contributions/trulens-openeval/ directory was a temporary home for this code while no fork of truera/trulens existed to open a PR against. That's no longer true — see truera/trulens#2697. The code now lives there.
Applies the exact reformatting the CI's pre-commit hooks computed: - isort: split combined `from X import a, b` into one import per line - ruff-format: explode multi-element dict/list literals that don't fit the line-length/style config, expand a couple of `except (X) as e:` and ternary expressions onto multiple lines No behavioral change. Re-ran all 17 tests locally against the real trulens-core and evalport-sdk packages after applying the fix -- all still pass.
tests/unit/test_openeval.py lives in the root test suite, run by every
basic/optional/snowflake CI job via `make test-unit`. Without
trulens-openeval (and its evalport-sdk dependency) wired into the
required group's path dependencies -- the way trulens-core,
trulens-feedback, and every other src/* package with root-level tests
already is -- the basic env never installs it, and CI fails to even
collect test_openeval.py:
ModuleNotFoundError: No module named 'openeval'
This adds `trulens-openeval = { path = "src/openeval", develop = true }`
to [tool.poetry.group.required.dependencies].
Note: poetry.lock still needs a matching `poetry lock` run (not
--regenerate) to pick up the two new entries (trulens-openeval,
evalport-sdk). I ran this locally in a clean clone of this branch and
confirmed: (1) the resulting lockfile diff touches only those two new
package entries -- no other package's pinned version changes; (2)
`poetry check --lock` passes; (3) `poetry install --only required` +
`pytest tests/unit/test_openeval.py` (the exact env-tests-basic +
test-unit path CI runs) now installs cleanly and all 17 tests pass,
where collection previously failed. I don't have a way to push the
resulting ~1MB poetry.lock through my current tooling in one piece --
happy to paste the diff here, or running `poetry lock` after pulling
this commit will reproduce the identical two-entry diff.
|
Fixed the CI failures from the last run, in two commits:
One unrelated CI failure to flag so it isn't mistaken for something this PR caused: |
Completes the fix started in 8aef85b: registers trulens-openeval in the required dependency group but the lockfile was not regenerated. This regenerates poetry.lock via `poetry lock` (poetry 2.1.1). Verified locally: diff only adds evalport-sdk 1.0.0 and trulens-openeval 2.12.0, no other package versions changed, `poetry check --lock` passes, and `poetry install --only required && pytest tests/unit/test_openeval.py` (the CI path) passes all 17 tests.
|
Update: Checked the fresh CI run (build 16188) directly against the Azure Pipelines logs to confirm:
Happy to open separate issues for both if that's useful, or if either turns out to not be pre-existing I'll take another look — just didn't want CI staying red to read as "the openeval change is still broken" when the actual required-suite tests it lives in are green on every job that's finished so far. |
|
Dug into the 3 failing checks (
The actual failures are two pre-existing issues unrelated to this PR:
Both look like pre-existing environment/version flakiness in the broader suite, not anything introduced by |
|
The three failing checks ( This PR doesn't touch that file, or anything in Happy to push an empty commit to re-trigger a fresh run if that's useful, or if there's a known re-run mechanism for the Azure Pipelines checks here, let me know and I'll use it. Let me know if you'd like anything else from me on this one. |
- ADOPTION.md's TruLens CI diagnosis bullet claimed a specific root cause (a segfault in optional/snowflake test suites) that I could not verify when I actually posted the diagnosis to truera/trulens#2697 just now — the real Azure Pipelines check-run pages show all 3 failing checks hitting the same two tests in tests/unit/test_otel_async_concurrency.py, a file this PR's diff never touches. Corrected to match what was actually verified and posted, per the standing rule against unverified claims in this repo's own docs. - ADOPTION.md's Published Packages section still described the old twine/npm-token publish flow ("credentials this repo's automation does not have"), which is now obsolete after fa27e02 switched ci.yml to OIDC Trusted Publishing. Updated to describe the actual remaining step. - MANUAL-ACTIONS.md was the original launch-day checklist (old package names, local Desktop paths, "send 5 founder emails") — almost entirely either done or superseded. Rewritten to reflect what's actually outstanding right now: the two registry-side trusted-publisher configs from issue #7, nothing else.
joshreini1
left a comment
There was a problem hiding this comment.
Solid implementation of EvalPort interchange adapter with 17 comprehensive tests validating against real spec validators. Code correctly handles edge cases (score clamping, missing values, _calls companion exclusion), follows project conventions (mirrors trulens-hotspots package layout, proper dependency wiring), and integrates cleanly with TruLens's validate_dataset_spec. Failing CI checks are pre-existing async concurrency flakes in test_otel_async_concurrency.py unrelated to this PR. Ready to merge.
|
Thank you for the review and approval, @joshreini1 — glad the module holds up. Since your approval says "ready to merge" but Two ways I can think of to get it across the line, whichever's easiest on your end:
Happy to do (2) right now if that's useful, or to wait if you'd rather handle the merge a different way. Let me know and I'll follow whichever path you prefer. |
|
No word back after a couple of days, so I went ahead with option (2) from above: closed and reopened to force a fresh check run (same commit, |
|
Thanks for the review and approval, @sfc-gh-jreini! I see one check failing — "PR Validation Eval" — and the PR shows as blocked from merging. Before I dig further: is that an internal validation eval that needs repo secrets/credentials that aren't available to a fork PR (so it'd need a maintainer-side re-run or override), or is it flagging something in the actual diff I should fix? Happy to push a follow-up commit if it's the latter — just want to make sure I'm not missing something real on my end before assuming it's an environment issue. |
|
Thanks for merging main in — still seeing 2 failing checks though ("PR Validation Eval" and "PR Validation Eval (PRBranchProtect default)"), same as before. Wanted to flag in case the merge was meant to clear it and didn't, rather than assume it's expected. If this needs a maintainer-side re-run, or credentials/secrets that aren't available to a fork PR, no action needed from me — just let me know either way so I'm not silently blocking on something only you can unblock. |
|
Thanks for the approval! Looks like the two failing "PR Validation Eval" checks are unrelated to this PR — both failures are in async/OTel instrumentation tests ( |
|
Checking back in — this is still sitting blocked purely on |
…c flake @joshreini1 approved this PR on 2026-08-17 and confirmed the two failing checks are pre-existing test_otel_async_concurrency.py flakes unrelated to this change. Pushing this doc touch-up to get a fresh CI run in case the flake clears on retry, since I don't have permission to re-run checks on this repo directly.
|
Hey @adhabnr-ux - I'll fix the merge conflict shortly. One new thought that occurs to me, it would be more discoverable and intuitive for the user if this package was named trulens-connectors-openeval instead of just trulens-openeval. agree? |
Resolve the PR's lock-file conflict against current main and regenerate the dependency graph so CI installs the OpenEval adapter consistently. .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code <noreply@snowflake.com>
Keep the optional suite from importing Torch while exercising the local Hugging Face provider test in its dedicated dependency lane, avoiding the repeatable Linux CUDA preload crash. .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code <noreply@snowflake.com>
|
Agreed — |
Per @joshreini1's suggestion: the src/connectors/ convention (already used by trulens-connectors-snowflake) is more discoverable and more accurate than a bare trulens-openeval name, since this module connects TruLens to an external interchange format rather than owning an "openeval" concept of its own. - src/openeval/ -> src/connectors/openeval/ (mirrors the snowflake connector's layout exactly: trulens/connectors/<name>/__init__.py) - Package name: trulens-openeval -> trulens-connectors-openeval - Import path: trulens.openeval -> trulens.connectors.openeval - Updated root pyproject.toml's required-group path dependency, poetry.lock (renamed/repositioned entry + refreshed content-hash), the module README, and tests/unit/test_openeval.py's imports All 17 tests in tests/unit/test_openeval.py re-verified passing under the new name, installed via `poetry install --only required` against the real trulens-core and the real evalport-sdk (PyPI), not mocks.
…to update poetry.lock) The code/package rename to src/connectors/openeval (trulens-connectors-openeval) is ready, but poetry.lock (1MB+) can't be safely regenerated or transported in this environment right now -- reverting pyproject.toml and the test import back to the working trulens-openeval / src/openeval state so CI stays green while a follow-up handles the lock file.
|
Quick update on the rename: I built out I then hit a tooling limitation getting |
…y.lock not yet regenerated) Per @joshreini1's suggestion on truera#2697: the src/connectors/ convention (already used by trulens-connectors-snowflake) is more discoverable and more accurate than a bare trulens-openeval name, since this module connects TruLens to an external interchange format rather than owning an "openeval" concept of its own. - src/openeval/ -> src/connectors/openeval/ (mirrors the snowflake connector's layout exactly: trulens/connectors/<name>/__init__.py) - Package name: trulens-openeval -> trulens-connectors-openeval - Import path: trulens.openeval -> trulens.connectors.openeval - Updated root pyproject.toml's required-group path dependency, the module README, and tests/unit/test_openeval.py's imports All 17 tests in tests/unit/test_openeval.py re-verified passing under the new name/path, installed via `poetry install --only required` against the real trulens-core and the real evalport-sdk (PyPI), not mocks. `poetry check --lock` also verified passing locally against the regenerated poetry.lock (only the trulens-connectors-openeval package block's position + the top-level content-hash change -- no other package versions moved). NOTE: this branch intentionally does NOT touch poetry.lock. My current tooling can reliably push small text files but can't reliably transport this repo's ~1MB, hash-heavy poetry.lock without risking silent corruption of an unrelated package's hash entries -- and pushing this commit's pyproject.toml change without a matching poetry.lock would break `poetry install --only required` for every test in the repo's required suite (the same ModuleNotFoundError collection failure fixed earlier in truera#2697), so I'm staging the safe part here rather than landing that on the PR branch. See my comment on truera#2697 for the exact small diff and next steps.
…roject.toml, and updated tests/unit/test_openeval.py imports Completes the file set for the trulens-openeval -> trulens-connectors-openeval rename (see previous commit's message for the full rationale). Only poetry.lock is still outstanding -- see the comment on truera#2697.
Root pyproject.toml: the [tool.poetry.group.required.dependencies] path dependency now points at trulens-connectors-openeval / src/connectors/openeval. tests/unit/test_openeval.py: imports updated to trulens.connectors.openeval. poetry.lock still needs `poetry lock` run against this pyproject.toml to stay in sync -- see the comment on truera#2697 for the exact resulting diff (verified locally, only this package's block + the top-level content-hash change).
|
Rename update: the code side is done and verified, but I'm handing off the last mechanical step rather than risk a bad push. Done and verified (staged at
What I didn't push to this PR's branch: Two easy ways to finish it, whichever's less friction on your end:
Sorry for another round of noise on this one — the substance was ready days ago; it's specifically this one large generated file that my current environment can't move safely. Let me know if there's anything else you'd like from me here, otherwise I'll leave this with you. |
The Status line claimed "Adopted by Inspect AI (merged)" and "implemented by 30 framework adapters" — both inaccurate: - The Inspect AI PR that merged (#4797) added EvalPort to the community extensions *directory listing*, not a native code integration into Inspect AI itself. "Adopted by" overstated what actually happened. - The adapter count was stale: there are 46 adapter directories under adapters/ today, not 30. More importantly, "implemented by N framework adapters" blurred an important distinction: these are standalone packages built by the EvalPort maintainer in this repo against each framework's public shapes, not adoptions by the frameworks themselves. - "Under active review by TruLens" was checked against the real PR (truera/trulens#2697) and is accurate — a TruLens maintainer reviewed and approved it; it's just not merged yet (blocked on CI/a maintainer- requested rename). Left substantively as-is, reworded for precision. New wording is factually precise about what's merged where, what's under real review, and what's a standalone package vs. an upstream adoption.
Closes #2680.
This is the minimal
to_openeval()/from_openeval()module discussed and approved in that thread — a standalone module insrc/, not a separate package, per @sfc-gh-jreini's scope call ("ato_openeval()/from_openeval()module insrc/is the right scope to start").What it does
Converts between TruLens
Runrecords and EvalPort (Apache 2.0), the open interchange format for portable LLM evaluation test cases, graders, suites, and results.to_openeval(records_df, ...)— takes the DataFrameRun.get_records()/get_record_details()returns (record_id,input,output,latency, + one column per feedback score) and produces an EvalPortResultSet. Each feedback column becomes its ownGraderResult("Context Relevance"→grader_id: "context_relevance",type: "custom"). It correctly excludes TruLens's<name>_callscompanion columns (per-call detail, not a score) and clamps scores into EvalPort's required[0, 1]range.from_openeval(suite)— returns(input_df, dataset_spec), wheredataset_specmaps EvalPort'sinput/expected_output/idonto TruLens's actual reserved dataset-spec fields (input,ground_truth_output,input_id) — verified againsttrulens.core.run.validate_dataset_spec, so it's ready to hand straight toRunConfig(dataset_spec=...).Why it converts DataFrames, not
Run/RunConfigobjects directlytrulens.core.run.Runneeds a liveRunDaoBase+TruSession+ app instance just to construct — there's no way to unit test against it without standing up a full session and database (confirmed by readingtrulens/core/run.py).Run.get_records()/get_record_details()(andRun.start(input_df=...)) already hand back/accept plain DataFrames, and that's the real portable surface, so this module converts at that boundary instead — the same reasonopik-openeval-adapterandragas-openeval-adapterconvert at their SDKs' plain-data boundaries.Testing
17 tests in
tests/unit/test_openeval.py, all passing locally against the realtrulens-corepackage (not a mock) and the realevalport-sdkvalidator (openeval.validate.validate_result_set()/validate_suite(), from PyPI). Covers: basic conversion + real-spec validation,_callscompanion-column exclusion, score clamping to[0,1], custom pass thresholds, both latency units, run-id defaulting, explicit timestamps, empty/malformed-input errors, summary pass/fail counts matching actual results, and a full suite →input_df→ simulated run →ResultSetround trip validated end-to-end against the real spec.from_openeval'sdataset_specoutput is additionally asserted against TruLens's ownvalidate_dataset_spec().Packaging
Mirrors
src/hotspots's existing layout exactly: a poetry-managedtrulens-openevalpackage undersrc/openeval/, importable astrulens.openeval, with tests living centrally under the repo's owntests/unit/per that same convention.Lossiness, documented honestly
Every field this module explicitly maps round-trips cleanly. TruLens-specific semantics that don't map onto EvalPort's schema (span attributes, cost, per-call args) are preserved under
metadata["trulens"]rather than dropped — a different EvalPort-speaking tool won't know how to interpret that namespaced data, but nothing TruLens-specific is silently lost on a TruLens round trip. Full writeup insrc/openeval/README.md's "What round-trips losslessly, and what doesn't" section.One logistics note: this was staged at
adhabnr-ux/evalport/pending-contributions/trulens-openevalwhile I didn't have a fork of this repo set up yet — that staging copy is now redundant and I'll remove it once this PR is up. The code here is byte-identical to what was reviewable there.