huggingface / huggingface/upskill

Interop question: optional EvalPort export/import adapter for upskill's eval results?

Open
#37 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
746
Forks
91
PR merge metrics
No merged PRs in 30d

Description

## Summary

UPskill's `src/upskill/models.py` already has its own `TestCase`, `TestResult`, `ConversationStats`, and `EvalResults` (skill-vs-baseline lift) — a full eval-results stack, purpose-built for grading Claude Code / Codex / Open Code agent skills. I maintain [EvalPort](https://github.com/adhabnr-ux/evalport), an open interchange spec (Apache 2.0) for the same kind of object — portable test cases, graders, and results — with a Python/TS SDK (`evalport-sdk`) and 32 real framework adapters, including one for Hugging Face's own [`evaluate`](https://github.com/adhabnr-ux/evalport/tree/main/adapters/huggingface-evaluate-openeval-adapter) and one for `lighteval` (the library behind the HF Open LLM Leaderboard).

I'd like to propose a small, optional `to_openeval()` / `from_openeval()` adapter for `upskill` — not a change to `upskill`'s own models, just a converter package (mirroring the shape of the 32 adapters already in that repo) so `upskill` eval results could be exported into a format other eval tools can read, and results from EvalPort-speaking tools could be imported as `upskill` test cases. Opening this as a discussion, not a PR — wanted to check whether this is something the maintainers would want before writing code.

## The overlap, concretely

I want to be upfront that this is naming coincidence more than anything: `upskill` already has a class literally called `TestCase`, distinct from EvalPort's own `TestCase`. They're not the same shape, but they map cleanly:

| `upskill` (`src/upskill/models.py`) | EvalPort (`spec/schemas`) | Notes |
|---|---|---|
| `TestCase.input` (str) | `TestCase.input` | Direct match |
| `TestCase.expected: ExpectedSpec \| None` (`.contains: list[str]`) | `Grader` of type `contains` | `expected.contains` → a `contains` grader |
| `TestCase.verifiers: list[VerifierSpec]` | `Grader[]` | Each `VerifierSpec.type` (e.g. `"contains"`) maps to an EvalPort grader `type`; `VerifierSpec.cmd`/`.text` map toward `code`/`custom` graders |
| `TestResult.success` (bool) | `Result.passed` (bool) | Direct match |
| `TestResult.output` (str \| None) | `Result.actual_output` | Direct match |
| `TestResult.stats: ConversationStats` (`.total_tokens`, `.turns`, `.tool_calls`, …) | `Result.metadata` (free-form dict) | Would flow into `metadata` since EvalPort's core `Result` has no first-class agent-trace fields yet |
| `EvalResults.with_skill_results` / `.baseline_results` (two `list[TestResult]`) | `ResultSet.results: list[Result]` | Would need a `metadata.variant: "with_skill" \| "baseline"` tag per result, since EvalPort's `ResultSet` today models one run, not a paired comparison |
| `EvalResults.skill_lift`, `.token_savings` (computed properties) | *(no direct equivalent)* | Would live in `ResultSet.metadata` as derived summary stats, not part of the core spec |

The honest gap: `EvalResults` is a *comparison* object (skill vs. baseline, same test suite, two conditions) and `ConversationStats` carries real agent-trace detail (tool-call counts, per-tool error maps, timing). EvalPort's core `Result`/`ResultSet` don't model a paired A/B comparison or agent-trace stats natively — those would land in `metadata` rather than becoming new required fields in the shared spec. So this is more "upskill results become portable to whatever reads EvalPort" than "upskill adopts EvalPort's shape internally."

## Sketch (illustrative, not proposing to merge this as-is)

```python
# adapters/upskill-openeval-adapter/upskill_openeval_adapter/convert.py
from upskill.models import EvalResults, TestResult

def to_openeval(results: EvalResults) -> dict:
"""Convert upskill EvalResults into an EvalPort ResultSet dict."""
def _result(tr: TestResult, variant: str) -> dict:
return {
"id": tr.test_case.input[:64],
"passed": tr.success,
"actual_output": tr.output,
"metadata": {
"variant": variant,
"tokens_total": tr.stats.total_tokens,
"turns": tr.stats.turns,
"tool_calls": tr.stats.tool_calls,
"error": tr.error,
},
}

return {
"version": "1.0.0",
"id": f"{results.skill_name}-{results.model}",
"results": (
[_result(r, "with_skill") for r in results.with_skill_results]
+ [_result(r, "baseline") for r in results.baseline_results]
),
"metadata": {
"skill_name": results.skill_name,
"model": results.model,
"skill_lift": results.skill_lift,
"token_savings": results.token_savings,
"is_beneficial": results.is_beneficial,
},
}
```

Comparable, real precedent for this exact shape of adapter: [`huggingface-evaluate-openeval-adapter`](https://github.com/adhabnr-ux/evalport/tree/main/adapters/huggingface-evaluate-openeval-adapter) and [`lighteval-openeval-adapter`](https://github.com/adhabnr-ux/evalport/tree/main/adapters/lighteval-openeval-adapter) — both standalone packages with `to_openeval()`/`from_openeval()`, tested against EvalPort's real validator, no changes to the source project's own models.

## What I'm actually asking

Not proposing a PR yet. Three questions:

1. Is an optional, standalone conversion adapter (living in the `evalport` repo, or as a small `upskill`-side extra — whichever you'd prefer) something the maintainers would find useful, or is `upskill`'s eval-results format intentionally self-contained and not meant to interoperate?
2. If useful, would you rather this live entirely on the EvalPort side (an adapter package importing `upskill.models`, versioned independently, no changes needed here) or as a small opt-in export inside `upskill` itself (e.g. `upskill runs --export openeval`)?
3. Anything about `ConversationStats` or the skill-vs-baseline comparison in `EvalResults` you'd want represented as first-class EvalPort spec fields rather than `metadata`, if this got real use?

Happy to build whichever direction makes sense, or to drop this entirely if it's not a fit — genuinely asking, not assuming.

— Sahi, independent contributor (not affiliated with Hugging Face)

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading src/upskill/models.py, especially TestCase, TestResult, ConversationStats, and EvalResults, then compare those models with the EvalPort schemas and the referenced adapter examples. This issue is seeking maintainer direction rather than requesting a defined implementation; the work is done only once the adapter location, supported conversions, and representation of comparison and trace metadata are agreed.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.