mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
390f3e3417
* feat: run isolated paired candidate and baseline skill evaluations Build the first complete paired skill-evaluation path: stage an immutable candidate, run matched candidate and baseline trials in clean environments, execute deterministic outcome graders, and produce a case-level comparison report. - eval_runner/sandbox.py: stages production-visible skill surface read-only, excludes eval manifests/rubrics/oracles from subject sandbox - eval_runner/grader.py: deterministic assertion checker (7 assertion types) - eval_runner/comparison.py: paired comparison report generation - eval_runner/paired.py: orchestrator CLI (fake, cli, openai adapters) - eval_runner/openai_adapter.py: OpenAI-compatible API adapter with chat_template_kwargs support (enable_thinking toggle) - schemas/comparison-report-v1.schema.json: report schema - .github/workflows/skill-eval.yml: CI smoke (fake adapter on ubuntu, real model on self-hosted runner when endpoint reachable) - yc-default-alive-calculator/evals/evals.json: initial 5-case eval manifest Verified against google_gemma-4-26B-A4B-it-IQ4_XS.gguf: 5/5 candidate improvements, 0 regressions. Closes #105 * ci: make paired-eval-model job non-blocking The self-hosted runner may not always be online. Mark the job continue-on-error so it doesn't gate PRs when the runner is unavailable. * ci: isolate model evals from pull requests * fix(raleigh): test arrivals against a daily route, not weekday-only The fixture only had a WEEK (Mon-Fri) service, so test_get_arrivals_for_stop returned 0 arrivals on weekends when _today_date() fell on Saturday/Sunday. Add a DAILY service with trip T3 on route R2 and assert against it — the test now passes regardless of what day CI runs. * ci: trigger checks on amended commit --------- Co-authored-by: magnus919 <magnus919>
135 lines
5.2 KiB
Python
135 lines
5.2 KiB
Python
"""Deterministic grader for eval assertions.
|
|
|
|
Checks machine-parseable assertions against AdapterOutput. Assertions follow
|
|
a convention-based format:
|
|
|
|
response_contains:<substring>
|
|
response_not_contains:<substring>
|
|
exit_status:<status>
|
|
artifact_exists:<filename>
|
|
environment_state:<key>=<value>
|
|
activation_evidence_contains:<substring>
|
|
tool_event_count_gte:<n>
|
|
|
|
Assertions that do not match a known pattern are reported as requiring manual
|
|
review and do not affect the pass/fail verdict.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
|
|
from .models import AdapterOutput, ExitStatus
|
|
|
|
|
|
class AssertionVerdict(str, Enum):
|
|
PASS = "pass"
|
|
FAIL = "fail"
|
|
MANUAL_REVIEW = "manual_review"
|
|
INFRA_ERROR = "infra_error"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AssertionResult:
|
|
assertion: str
|
|
verdict: AssertionVerdict
|
|
detail: str = ""
|
|
|
|
|
|
@dataclass
|
|
class GradeResult:
|
|
case_id: str
|
|
passed: bool
|
|
results: list[AssertionResult] = field(default_factory=list)
|
|
infra_error: bool = False
|
|
|
|
@property
|
|
def pass_count(self) -> int:
|
|
return sum(1 for r in self.results if r.verdict == AssertionVerdict.PASS)
|
|
|
|
@property
|
|
def fail_count(self) -> int:
|
|
return sum(1 for r in self.results if r.verdict == AssertionVerdict.FAIL)
|
|
|
|
@property
|
|
def manual_count(self) -> int:
|
|
return sum(1 for r in self.results if r.verdict == AssertionVerdict.MANUAL_REVIEW)
|
|
|
|
|
|
def _check_assertion(assertion: str, output: AdapterOutput) -> AssertionResult:
|
|
if ":" not in assertion:
|
|
return AssertionResult(assertion, AssertionVerdict.MANUAL_REVIEW, "no recognized pattern")
|
|
|
|
kind, _, value = assertion.partition(":")
|
|
kind = kind.strip().lower()
|
|
value = value.strip()
|
|
|
|
if kind == "response_contains":
|
|
if output.response and value in output.response:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"'{value}' not in response")
|
|
|
|
if kind == "response_not_contains":
|
|
if output.response is None or value not in output.response:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"'{value}' found in response")
|
|
|
|
if kind == "exit_status":
|
|
expected = value.lower()
|
|
actual = output.exit_status.value
|
|
if actual == expected:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"expected {expected}, got {actual}")
|
|
|
|
if kind == "artifact_exists":
|
|
if value in output.artifacts:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"'{value}' not in artifacts")
|
|
|
|
if kind == "environment_state":
|
|
if "=" not in value:
|
|
return AssertionResult(assertion, AssertionVerdict.MANUAL_REVIEW, "malformed key=value")
|
|
key, _, expected_val = value.partition("=")
|
|
if output.environment_state and key in output.environment_state:
|
|
actual_val = str(output.environment_state[key])
|
|
if actual_val == expected_val:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"{key}={actual_val}, expected {expected_val}")
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"key '{key}' not in environment_state")
|
|
|
|
if kind == "activation_evidence_contains":
|
|
if output.activation_evidence and value in output.activation_evidence:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"'{value}' not in activation_evidence")
|
|
|
|
if kind == "tool_event_count_gte":
|
|
try:
|
|
threshold = int(value)
|
|
except ValueError:
|
|
return AssertionResult(assertion, AssertionVerdict.MANUAL_REVIEW, "non-integer threshold")
|
|
if len(output.tool_events) >= threshold:
|
|
return AssertionResult(assertion, AssertionVerdict.PASS)
|
|
return AssertionResult(assertion, AssertionVerdict.FAIL, f"{len(output.tool_events)} < {threshold}")
|
|
|
|
return AssertionResult(assertion, AssertionVerdict.MANUAL_REVIEW, f"unknown assertion kind '{kind}'")
|
|
|
|
|
|
def grade_output(case_id: str, assertions: list[str], output: AdapterOutput) -> GradeResult:
|
|
"""Grade an adapter output against a list of assertions.
|
|
|
|
Infrastructure errors (non-completed exit status) are distinguished from
|
|
skill failures: all assertions are marked infra_error and the grade is
|
|
not-pass, but the infra_error flag is set.
|
|
"""
|
|
if output.exit_status != ExitStatus.COMPLETED:
|
|
results = [
|
|
AssertionResult(a, AssertionVerdict.INFRA_ERROR, f"exit_status={output.exit_status.value}")
|
|
for a in assertions
|
|
]
|
|
return GradeResult(case_id=case_id, passed=False, results=results, infra_error=True)
|
|
|
|
results = [_check_assertion(a, output) for a in assertions]
|
|
passed = all(r.verdict in (AssertionVerdict.PASS, AssertionVerdict.MANUAL_REVIEW) for r in results)
|
|
return GradeResult(case_id=case_id, passed=passed, results=results)
|