headroom/.github/workflows/eval.yml
Ashish 46d4378cf7
feat(evals): weekly HotpotQA answer-recall report on the prose path (#1188)
## Description

Follow-up to **#1187** (the offline fidelity gate). That gate is
hermetic and **structured-only** (JSON tool outputs via Rust
compressors) so it can block every PR with zero setup. This PR adds the
genuinely-uncovered piece: **prose answer-recall on a real dataset
(HotpotQA)** in the **model-allowed weekly job**, where compression
routes through Kompress (ModernBERT).

> **Stacked on #1187.** Until that merges, this PR's diff shows its
commit too; it reduces to just `c71cc0cb` once #1187 lands. Please
review/merge #1187 first.

Closes #

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- **`CompressionOnlyRunner.evaluate_dataset_recall(suite)`**: for each
QA case, compress the supporting `context` via the production routing
path (`ContentRouter`) and check the `ground_truth` answer survives
(`compute_information_recall`). Counts only **probeable** cases — answer
literally present in the context and non-trivial (skips `yes/no`,
too-short) — so the aggregate is meaningful rather than inflated by
un-measurable cases.
- **`.github/workflows/eval.yml`**: a non-blocking step in the existing
`weekly-suite` job (schedule/manual only) drives it with
`load_hotpotqa(n=50)`. Defensive: a dataset download or model failure
emits `:⚠️:` and `|| true`, never failing the job.
- **Hermetic unit test** (`tests/test_dataset_recall_runner.py`):
exercises the method with synthetic JSON-array contexts (SmartCrusher /
Rust — no model, no network), so it runs in the standard `[dev]` shard.

### Scope notes

- **Prose path only.** BFCL / tool-schema integrity is already covered
by the existing `evaluate_tool_schema_compaction` eval (which runs in
the PR smoke-test), so this targets the previously-uncovered prose
recall path. NQ is an easy further extension using the same method +
`load_natural_questions`.
- **Why weekly, not per-PR.** Real datasets need a network download +
the ModernBERT model. The `weekly-suite` job already installs `[all]`
and genuinely runs every Monday (verified: 5 consecutive successful
scheduled runs), so it's the correct home — keeping PR CI fast and
hermetic.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ HF_HUB_OFFLINE=1 python -m pytest tests/test_dataset_recall_runner.py -q
..                                                                       [100%]
2 passed in 0.20s
```

## Real Behavior Proof

- Environment: local checkout of `feat/weekly-dataset-recall`, `pip
install -e ".[dev]"`, `HF_HUB_OFFLINE=1` (proves the unit tests need no
model/network)
- Exact command / steps: `HF_HUB_OFFLINE=1 python -m pytest
tests/test_dataset_recall_runner.py -q` -> `6 passed in 0.36s`; coverage
JSON confirms the runner's per-case exception handler and both
`warm_kompress_model` outcomes are exercised
- Observed result: with a synthetic suite of 3 cases (one probeable
answer in an error row, one trivial `yes`, one absent answer),
`evaluate_dataset_recall` counts only the 1 probeable case (`passed=1`,
`accuracy_rate=1.0`, `benchmark="dataset_recall:synthetic"`); a
monkeypatched compressor crash records the error and counts the case
failed instead of aborting; the new weekly-suite YAML step parses via
`yaml.safe_load` and sits under the `schedule || workflow_dispatch`
guard
- Not tested: the live HotpotQA download + ModernBERT compression --
exercised only by the weekly job (or `workflow_dispatch`), by design

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- CHANGELOG/version intentionally untouched: repo uses
**release-please**.
- The weekly job can be triggered on demand via **workflow_dispatch** to
see the HotpotQA recall numbers without waiting for Monday.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-15 21:40:55 +00:00

178 lines
8 KiB
YAML

name: Evaluation Suite
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday 6am UTC
workflow_dispatch: # Manual trigger
pull_request:
paths:
- 'headroom/transforms/**'
- 'headroom/evals/**'
- 'headroom/compress.py'
jobs:
# Fast smoke test on PRs touching compression code (~$0.05, ~2 min)
smoke-test:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-eval-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-eval-
# `pip install -e .` invokes maturin (declared in pyproject.toml's
# build-system) which calls cargo to compile the Rust extension.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Install dependencies (builds Rust extension via maturin)
run: |
pip install -e ".[all]"
python -c "from headroom._core import SmartCrusher; print('headroom._core OK:', SmartCrusher)"
- name: Run CCR round-trip (zero cost)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
cases = runner.generate_ccr_test_cases(n=50)
result = runner.evaluate_ccr_lossless(cases)
print(f'CCR Round-trip: {result.passed_cases}/{result.total_cases} passed')
assert result.passed, f'CCR failures: {result.errors}'
"
- name: Run tool schema compaction integrity eval (zero cost)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
result = runner.evaluate_tool_schema_compaction()
print(f'Tool schema compaction: {result.passed_cases}/{result.total_cases} passed, {result.total_tokens_saved} annotation tokens stripped')
assert result.passed, f'Schema compaction failures: {result.errors}'
"
# OPENAI_API_KEY is intentionally not set in the public OSS repo
# (the secret list is empty). The CCR round-trip step above is the
# mandatory gate; this step only runs when an operator has wired
# OPENAI_API_KEY as a repo secret (e.g. on a downstream fork). When
# missing, emit a loud GitHub `::warning::` annotation so the skip
# is visible in the run summary — never a silent pass.
- name: Run built-in tool output eval (skipped when OPENAI_API_KEY unset)
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "::warning title=Smoke eval skipped::OPENAI_API_KEY is not configured for this repo; only the CCR round-trip gate ran. Wire the secret to enable the live OpenAI eval."
exit 0
fi
python -m headroom.evals quick -n 8 --provider openai --model gpt-4o-mini
# Full Tier 1 suite, weekly or manual (~$3-5, ~30-45 min)
weekly-suite:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-eval-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-eval-
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Install dependencies (builds Rust extension via maturin)
run: |
pip install -e ".[all]"
python -c "from headroom._core import SmartCrusher; print('headroom._core OK')"
- name: Run Tier 1 evaluation suite
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "::warning title=Weekly eval skipped::OPENAI_API_KEY is not configured for this repo; skipping the live Tier 1 suite."
mkdir -p eval_results
printf '%s\n\n%s\n' \
'# Weekly Evaluation Skipped' \
'OPENAI_API_KEY is not configured for this repository, so the live Tier 1 evaluation suite was skipped.' \
> eval_results/skipped.md
exit 0
fi
python -m headroom.evals suite --tier 1 --ci -o eval_results/
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Recall-based fidelity report on the production routing path. Zero cost
# (synthetic structured cases -> Rust compressors; no model, no API, no
# secrets). Non-blocking: surfaces recall trends weekly without gating.
# The blocking per-PR fidelity gate lives in
# tests/test_compression_fidelity_regression.py (runs in the [dev] shard).
- name: Information-retention recall report (zero cost, non-blocking)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
cases = runner.generate_info_retention_cases(n=50)
result = runner.evaluate_information_retention(cases)
print(f'Information retention: {result.passed_cases}/{result.total_cases} cases >=0.9 recall, avg compression {result.avg_compression_ratio:.1%}')
if not result.passed:
print(f'::warning title=Fidelity recall::{result.failed_cases} case(s) fell below 0.9 recall: {result.errors[:3]}')
"
# Real-dataset recall on the prose path (HotpotQA): does the ground-truth
# answer survive compressing the supporting context? Uses the production
# routing path, so prose flows through Kompress (ModernBERT) — allowed here
# because the weekly job installs [all]. Non-blocking and defensive: a
# dataset download or model failure warns rather than fails the job.
- name: Dataset recall report — HotpotQA (model-allowed, non-blocking)
run: |
python -c "
try:
from headroom.transforms.kompress_compressor import warm_kompress_model
from headroom.evals.datasets import load_hotpotqa
from headroom.evals.runners.compression_only import CompressionOnlyRunner
# Block until the Kompress model is loaded; otherwise prose passes
# through uncompressed and the recall number is meaningless.
warmed = warm_kompress_model()
suite = load_hotpotqa(n=50)
result = CompressionOnlyRunner().evaluate_dataset_recall(suite)
print(f'HotpotQA answer recall: {result.passed_cases}/{result.total_cases} probeable cases >=0.9, avg compression {result.avg_compression_ratio:.1%} (model_warmed={warmed})')
if result.avg_compression_ratio < 0.01:
print('::warning title=Dataset recall::compression did not engage (~0%); recall is not a meaningful fidelity signal — check Kompress model availability')
elif result.failed_cases:
print(f'::warning title=Dataset recall::{result.failed_cases} HotpotQA case(s) lost the answer under compression')
except Exception as e:
print(f'::warning title=Dataset recall::skipped (dataset/model unavailable): {e}')
" || true
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: eval-results-${{ github.run_number }}
path: eval_results/