-
Notifications
You must be signed in to change notification settings - Fork 54
feat: add skill-level evaluators for skill-equipped agents #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sangminwoo
wants to merge
25
commits into
strands-agents:main
Choose a base branch
from
sangminwoo:skill-evaluators
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
6786c56
feat: add skill-level evaluators for skill-equipped agents
sangminwoo bc5b95c
fix: extend skill extraction to Google ADK shapes and harden dedup
sangminwoo e45f38c
fix: build a fresh judge per skill in the selection evaluator
sangminwoo da92a71
fix: register the skill evaluators so experiment files can load them
sangminwoo b24f1d8
fix: an empty step list is a valid skill-adherence judgment, not a retry
sangminwoo b3cc96e
fix: a refused skill load is not a skill body
sangminwoo c95f933
fix: an abstention with no skills on offer is not a selection decision
sangminwoo 047ebe8
fix: a shell command only counts as a skill read when the verb owns t…
sangminwoo 9ed61dd
Record failed skill loads and stop mispairing skill bodies
sangminwoo fa43c03
Make one not-applicable convention every score reader agrees on
sangminwoo f715ae3
Recognize an AgentSkills refusal as a failed load, not just a missing…
sangminwoo 1ab54ab
Split extractors/skills into models/adapters/extractor, carry refusal…
sangminwoo d8e3092
Format the new refusal-message test
sangminwoo 61f240c
docs: add Args/Returns to the three public skill functions
sangminwoo afd378a
Add SkillLoadEvent, the per-attempt layer the evaluators can read
sangminwoo 5ebd8b3
Split the harness adapters into one module per harness
sangminwoo 089d7cf
Test each adapter against a fixture captured from its harness
sangminwoo c14d88a
fix: judge only invoked skills, not the decision to invoke none
sangminwoo 2e000cb
fix: do not label an unjudged case with the score mapping's worst ver…
sangminwoo b57c45a
fix: a malformed match must not hide a second reading of the same call
sangminwoo 789d2ff
docs: say why a case with no rows counts toward the mean
sangminwoo a58d7f4
fix: count a case that failed to judge, and stop losing short skill b…
sangminwoo 932fab5
Merge upstream main into skill-evaluators
sangminwoo 28948b6
fix: an unrecorded skill catalog is not an empty one
sangminwoo 0c56611
Merge upstream main into skill-evaluators
sangminwoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/strands_evals/evaluators/deterministic/skill_invoked.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from ...extractors.skills import extract_skill_load_events | ||
| from ...types.evaluation import EvaluationData, EvaluationOutput, InputT, OutputT | ||
| from ..evaluator import Evaluator | ||
|
|
||
|
|
||
| class SkillInvoked(Evaluator[InputT, OutputT]): | ||
| """Checks if a specific skill was invoked in the trajectory.""" | ||
|
|
||
| def __init__(self, skill_name: str, name: str | None = None): | ||
| super().__init__(name=name) | ||
| self.skill_name = skill_name | ||
|
|
||
| def evaluate(self, evaluation_case: EvaluationData[InputT, OutputT]) -> list[EvaluationOutput]: | ||
| trajectory = evaluation_case.actual_trajectory | ||
| if trajectory is None: | ||
| return [EvaluationOutput(score=0.0, test_pass=False, reason="no trajectory provided")] | ||
|
|
||
| # Read the individual attempts rather than the per-skill summary, so this check applies its | ||
| # own definition of "invoked": a refused load does not count, because the agent never | ||
| # received the skill and an assertion that it was used is false. The summary folds a | ||
| # refusal and a later success into one loaded row, which is right for judging the choice | ||
| # and wrong for asserting the skill was in play. | ||
| attempts = [e for e in extract_skill_load_events(trajectory) if e.name == self.skill_name] | ||
| found = any(e.status == "loaded" for e in attempts) | ||
| refusal = next((e for e in attempts if e.status == "failed"), None) | ||
| if found: | ||
| reason = f"skill '{self.skill_name}' was invoked" | ||
| elif refusal is not None: | ||
| # Report what the harness said: a check that fails on a misspelled skill name and one | ||
| # that fails because nothing was mounted call for different fixes. | ||
| detail = f": {refusal.error}" if refusal.error else "" | ||
| attempted = f" ({len(attempts)} attempts)" if len(attempts) > 1 else "" | ||
| reason = f"skill '{self.skill_name}' was requested but the load failed{attempted}{detail}" | ||
| elif attempts: | ||
| # Every attempt was made and none has a recorded outcome, so the trajectory does not | ||
| # say whether the skill was received. Reported as not invoked, since the check asserts | ||
| # use and use was not observed, but named apart from never asking. | ||
| reason = f"skill '{self.skill_name}' was requested but the trajectory records no outcome" | ||
| else: | ||
| reason = f"skill '{self.skill_name}' was not invoked" | ||
| return [ | ||
| EvaluationOutput( | ||
| score=1.0 if found else 0.0, | ||
| test_pass=found, | ||
| reason=reason, | ||
| ) | ||
| ] | ||
|
|
||
| async def evaluate_async(self, evaluation_case: EvaluationData[InputT, OutputT]) -> list[EvaluationOutput]: | ||
| return self.evaluate(evaluation_case) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
src/strands_evals/evaluators/prompt_templates/skill_instruction_following/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from . import skill_instruction_following_v0 | ||
|
|
||
| VERSIONS = { | ||
| "v0": skill_instruction_following_v0, | ||
| } | ||
|
|
||
| DEFAULT_VERSION = "v0" | ||
|
|
||
|
|
||
| def get_template(version: str = DEFAULT_VERSION): | ||
| return VERSIONS[version] |
39 changes: 39 additions & 0 deletions
39
...evaluators/prompt_templates/skill_instruction_following/skill_instruction_following_v0.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| SYSTEM_PROMPT = """You are an objective judge evaluating whether an AI agent followed the \ | ||
| instructions of a skill it loaded. | ||
|
|
||
| A skill is an instruction file (SKILL.md) with prescribed steps. You are given: | ||
| - the SKILL.md body (the steps the agent was supposed to follow), | ||
| - the agent's run (its actions, tool calls, and outputs). | ||
|
|
||
| ## Evaluation Task | ||
| Identify the prescribed steps in the SKILL.md body, then, for EACH step, judge from the agent's | ||
| run whether the step was: | ||
| - "covered": the run clearly shows the agent carried out the step, | ||
| - "partial": the run shows incomplete or ambiguous adherence, | ||
| - "skipped": the run shows no evidence the step was carried out. | ||
|
|
||
| After the per-step judgments, give an overall five-point rating of how fully the agent | ||
| followed the skill's steps: | ||
| - "Fully Followed": essentially every step carried out; no meaningful gaps. | ||
| - "Mostly Followed": the great majority of steps carried out; only minor gaps. | ||
| - "Partially Followed": a mix of carried-out and skipped steps. | ||
| - "Minimally Followed": most steps skipped; only a few carried out. | ||
| - "Not Followed": the skill's steps were essentially ignored. | ||
|
|
||
| The overall rating must be consistent with your per-step statuses: it should track the share | ||
| of steps that were covered (a run with nearly all steps covered is "Fully Followed"; one with | ||
| nearly all skipped is "Not Followed"). | ||
|
|
||
| ## Guidelines | ||
| - Judge only against the steps this skill prescribes, not generic task quality. | ||
| - Ground each per-step judgment in specific evidence from the run. | ||
| - A plan, unexecuted code snippet, or claim that a step will be done is not evidence | ||
| that an executable step was carried out. Require an action or result in the trajectory. | ||
| - If the skill has no clearly enumerable steps, treat its core instructions as the steps. | ||
| - If the skill body prescribes nothing at all (it is reference material rather than instructions), | ||
| return an empty step list rather than inventing steps. | ||
|
sangminwoo marked this conversation as resolved.
|
||
|
|
||
| ## Output Format | ||
| Provide, for each step, its status (covered / partial / skipped) with a short evidence note; | ||
| brief overall reasoning; and the overall five-point rating. | ||
| """ | ||
11 changes: 11 additions & 0 deletions
11
src/strands_evals/evaluators/prompt_templates/skill_selection_accuracy/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from . import skill_selection_accuracy_v0 | ||
|
|
||
| VERSIONS = { | ||
| "v0": skill_selection_accuracy_v0, | ||
| } | ||
|
|
||
| DEFAULT_VERSION = "v0" | ||
|
|
||
|
|
||
| def get_template(version: str = DEFAULT_VERSION): | ||
| return VERSIONS[version] |
28 changes: 28 additions & 0 deletions
28
...evals/evaluators/prompt_templates/skill_selection_accuracy/skill_selection_accuracy_v0.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| SYSTEM_PROMPT = """You are an objective judge evaluating whether an AI agent made an \ | ||
| appropriate skill-selection decision for a task. | ||
|
|
||
| A skill is a reusable instruction file the agent may load to help with a task. At runtime the | ||
| agent is shown a list of available skills (each with a name and description) and decides which, | ||
| if any, to load. You are given: | ||
| - the task the agent was asked to do, | ||
| - the list of available skills (name + description), | ||
| - one skill the agent invoked, which is the decision under evaluation, | ||
| - the agent's run. | ||
|
|
||
| ## Evaluation Question | ||
| Judge only the one skill named as the decision under evaluation. | ||
| - "Yes" if that skill's description fits the task (it was a reasonable skill to load). | ||
| - "No" if that skill does not fit the task (an inappropriate pick). | ||
| - On a task that needs several skills, invoking any one skill that genuinely fits is | ||
| appropriate on its own; judge this skill on its own merits, not on whether the agent also | ||
| loaded the other skills it needed. | ||
|
|
||
| ## Guidelines | ||
| - Judge the selection decision, not how well the agent then executed the skill. | ||
| - Base the decision on the skill descriptions and the task, not on the outcome. | ||
|
|
||
| ## Output Format | ||
| First give brief step-by-step reasoning, then a single verdict: | ||
| - "Yes" if the decision was appropriate, | ||
| - "No" if it was not. | ||
| """ |
41 changes: 41 additions & 0 deletions
41
src/strands_evals/evaluators/prompt_templates/trajectory_prompt_template.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Rendering a trajectory into judge-prompt text.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| from ...types.trace import Session | ||
|
|
||
| # Cap on serialized trajectory size in judge prompts, ~150k tokens at 4 chars/token. | ||
| MAX_TRAJECTORY_CHARS = 600_000 | ||
|
|
||
|
|
||
| def serialize_trajectory(trajectory: Session | list[Any] | None, max_chars: int = MAX_TRAJECTORY_CHARS) -> str: | ||
| """Serialize a trajectory into stable JSON, for use in judge prompts. | ||
|
|
||
| The middle of a long run is dropped: a real trajectory can reach millions of tokens (one read | ||
| of a large artifact is enough), which overflows any judge context window. The head and tail are | ||
| kept because skills are loaded early and the outcome lands late. | ||
|
|
||
| Args: | ||
| trajectory: A `Session` or a raw message list, or None. | ||
| max_chars: Size ceiling for the returned text. Pass 0 to keep the whole trajectory. | ||
|
|
||
| Returns: | ||
| str: The serialized trajectory, "(no trajectory)" when None, with an inline note naming the | ||
| character count where the middle was omitted. | ||
| """ | ||
| if trajectory is None: | ||
| return "(no trajectory)" | ||
| if isinstance(trajectory, Session): | ||
| value: Any = trajectory.model_dump(mode="json") | ||
| else: | ||
| value = [item.model_dump(mode="json") if isinstance(item, BaseModel) else item for item in trajectory] | ||
| text = json.dumps(value, indent=2, default=str) | ||
| if max_chars <= 0 or len(text) <= max_chars: | ||
| return text | ||
| keep = max_chars // 2 | ||
| return f"{text[:keep]}\n\n... [{len(text) - 2 * keep} characters omitted] ...\n\n{text[-keep:]}" | ||
|
sangminwoo marked this conversation as resolved.
Outdated
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.