Run trigger-accuracy + body-alignment evaluation. Args: skill_dir: `` /output/skills/ `` model: LiteLLM model string from KB config eval_set: pre-generated prompts; if None, generate fresh count: how many should-trigger + should-not prompts to generate
(
skill_dir: Path,
*,
model: str,
eval_set: list[EvalPrompt] | None = None,
count: int = EVAL_DEFAULT_COUNT,
)
| 387 | |
| 388 | |
| 389 | async def run_eval( |
| 390 | skill_dir: Path, |
| 391 | *, |
| 392 | model: str, |
| 393 | eval_set: list[EvalPrompt] | None = None, |
| 394 | count: int = EVAL_DEFAULT_COUNT, |
| 395 | ) -> EvalResult: |
| 396 | """Run trigger-accuracy + body-alignment evaluation. |
| 397 | |
| 398 | Args: |
| 399 | skill_dir: ``<kb>/output/skills/<name>`` |
| 400 | model: LiteLLM model string from KB config |
| 401 | eval_set: pre-generated prompts; if None, generate fresh |
| 402 | count: how many should-trigger + should-not prompts to generate |
| 403 | """ |
| 404 | if eval_set is None: |
| 405 | eval_set = await generate_eval_set(skill_dir, model=model, count=count) |
| 406 | |
| 407 | desc = _read_description(skill_dir) |
| 408 | content = _skill_content_block(skill_dir) |
| 409 | result = EvalResult(prompts=eval_set) |
| 410 | |
| 411 | # Run grading concurrently. Each prompt is independent — graders read |
| 412 | # the same `desc`/`content` strings and produce results that are then |
| 413 | # appended to `result` in eval_set order below, so concurrent |
| 414 | # execution is correctness-preserving. A semaphore caps simultaneous |
| 415 | # LLM calls to avoid hitting provider rate limits. |
| 416 | sem = asyncio.Semaphore(EVAL_CONCURRENCY) |
| 417 | |
| 418 | async def _trigger(p: EvalPrompt) -> Literal["trigger", "no-trigger"]: |
| 419 | async with sem: |
| 420 | return await grade_one(desc, p.question, model=model) |
| 421 | |
| 422 | async def _coverage( |
| 423 | p: EvalPrompt, |
| 424 | ) -> tuple[Literal["supported", "unsupported", "ambiguous"], str]: |
| 425 | async with sem: |
| 426 | return await grade_coverage(content, p.question, model=model) |
| 427 | |
| 428 | trigger_tasks = [_trigger(p) for p in eval_set] |
| 429 | # Body alignment only meaningful on questions the skill claims to |
| 430 | # handle — for should-not questions the body is correctly empty of |
| 431 | # relevant material. |
| 432 | coverage_prompts = [p for p in eval_set if p.expected == "trigger"] |
| 433 | coverage_tasks = [_coverage(p) for p in coverage_prompts] |
| 434 | |
| 435 | # return_exceptions=True so one failed grader doesn't discard the |
| 436 | # other ~29 successful gradings. Errored prompts are surfaced |
| 437 | # separately on the result and excluded from rate denominators. |
| 438 | trigger_results, coverage_results = await asyncio.gather( |
| 439 | asyncio.gather(*trigger_tasks, return_exceptions=True), |
| 440 | asyncio.gather(*coverage_tasks, return_exceptions=True), |
| 441 | ) |
| 442 | |
| 443 | # Walk inputs in original order so `result.*` lists are deterministic |
| 444 | # even though the gather() above completed out of order. |
| 445 | for prompt, graded in zip(eval_set, trigger_results): |
| 446 | if isinstance(graded, BaseException): |