huggingface / huggingface/upskill
Interop question: optional EvalPort export/import adapter for upskill's eval results?
- 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