mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## 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>
178 lines
8 KiB
YAML
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/
|