skilldx 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skilldx-0.3.0.dist-info/METADATA +440 -0
- skilldx-0.3.0.dist-info/RECORD +17 -0
- skilldx-0.3.0.dist-info/WHEEL +4 -0
- skilldx-0.3.0.dist-info/entry_points.txt +3 -0
- skilldx-0.3.0.dist-info/licenses/LICENSE +47 -0
- skillprobe/__init__.py +3 -0
- skillprobe/__main__.py +3 -0
- skillprobe/cache.py +35 -0
- skillprobe/cases.py +107 -0
- skillprobe/checks.py +81 -0
- skillprobe/cli.py +104 -0
- skillprobe/config.py +91 -0
- skillprobe/engine.py +339 -0
- skillprobe/report.py +184 -0
- skillprobe/runner.py +222 -0
- skillprobe/skills.py +50 -0
- skillprobe/stats.py +20 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: skilldx
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Eval harness for agent skills (SKILL.md): does the skill trigger when it should, and does the agent follow it - across OpenAI, Qwen, DeepSeek, GLM, Kimi and any OpenAI-compatible model.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AndreySukhanov/skillprobe
|
|
6
|
+
Project-URL: Source, https://github.com/AndreySukhanov/skillprobe
|
|
7
|
+
Project-URL: Issues, https://github.com/AndreySukhanov/skillprobe/issues
|
|
8
|
+
Author-email: Andrey Sukhanov <avsukhanov21@gmail.com>
|
|
9
|
+
License-Expression: LicenseRef-Source-Available
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-skills,claude-code,evals,llm,prompt-engineering
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: jsonschema>=4.0
|
|
14
|
+
Requires-Dist: openai>=1.40
|
|
15
|
+
Requires-Dist: pydantic>=2.5
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
|
|
23
|
+
<img src="assets/banner.png" alt="skillprobe โ eval harness for agent skills" width="100%"/>
|
|
24
|
+
|
|
25
|
+
# ๐ฌ skillprobe
|
|
26
|
+
|
|
27
|
+
### The eval harness that tells you whether the right SKILL.md loads โ and whether the model actually follows it.
|
|
28
|
+
|
|
29
|
+
**skillprobe is an eval harness for agent skills** โ the `SKILL.md` folders of Claude Code and the Agent Skills format: a Python CLI that tests whether a skill triggers on the right requests, which skill wins when many compete for the same request, and whether the model follows the skill once it loads. LLM evaluation built for skills: a labeled golden set, repeated trials, 95% confidence intervals and a CI gate, on any model behind an OpenAI-compatible API.
|
|
30
|
+
|
|
31
|
+
**Not "run it twice and eyeball the output."** skillprobe runs a labeled golden set against any model, many times over, and turns agent-skill behavior into numbers you can put in a pull request: trigger precision and recall, pass rates with 95% confidence intervals, flaky cases, routing collisions between skills, cost per trial.
|
|
32
|
+
|
|
33
|
+
**Golden set โ repeated trials โ confidence intervals โ per-skill routing table โ CI gate. Five model families. Cents per run on flash models.**
|
|
34
|
+
|
|
35
|
+
[](https://github.com/AndreySukhanov/skillprobe/actions/workflows/ci.yml)
|
|
36
|
+
[](https://www.python.org/)
|
|
37
|
+
[](https://docs.pydantic.dev/)
|
|
38
|
+
[](https://openrouter.ai/)
|
|
39
|
+
[](https://pypi.org/project/skilldx/)
|
|
40
|
+
[](https://github.com/AndreySukhanov/skillprobe/releases)
|
|
41
|
+
[](CONTRIBUTING.md)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
[Quick Start](#-quick-start) ยท [Results](#-results-on-the-bundled-example) ยท [Routing across 28 public skills](#-routing-across-28-public-skills) ยท [How It Works](#๏ธ-how-it-works) ยท [Features](#-features) ยท [FAQ](#-faq) ยท [Roadmap](#-roadmap)
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
<sub>Real output of the bundled example, replayed from the response cache with `--cache-only`.</sub>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ๐ก The Problem
|
|
55
|
+
|
|
56
|
+
A skill is a prompt with a trigger. Both halves fail quietly:
|
|
57
|
+
|
|
58
|
+
- **The trigger.** The model only sees the skill's `name` and `description`. A vague description means the skill never loads โ or loads for requests it has nothing to do with.
|
|
59
|
+
- **The body.** The skill loads, and the model follows half of it. Differently on every run.
|
|
60
|
+
|
|
61
|
+
And in a real agent a skill never runs alone. It sits in a list next to a dozen others, and **every description in that list competes for every request.**
|
|
62
|
+
|
|
63
|
+
Reading a few outputs by eye catches none of this. skillprobe runs a fixed set of cases many times per model and reports numbers you can compare between versions of a skill, between neighbors in the same skill list, and between models.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ๐ฅ Why it wins
|
|
68
|
+
|
|
69
|
+
| | Eyeballing a few runs | **skillprobe** |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| **Did the skill load?** | You notice when it obviously didn't | **Trigger precision / recall** per model, on positives and near-misses |
|
|
72
|
+
| **Which skill won?** | Invisible | **Per-skill routing table** โ loaded when required ยท loaded when forbidden โ plus a collisions table |
|
|
73
|
+
| **Variance** | One run, one story | **Repeats by default**, 95% Wilson intervals, flaky cases flagged |
|
|
74
|
+
| **What the skill adds** | Guesswork | `--baseline` runs the same cases with no skills offered โ the gap is the measured effect |
|
|
75
|
+
| **Models** | Whatever is open in the chat | OpenAI, Qwen, DeepSeek, GLM, Kimi, YandexGPT, a local vLLM โ **any OpenAI-compatible API** |
|
|
76
|
+
| **Cost** | Unknown until the bill | Cost and latency per model; `max_cost_usd` stops a run before it overspends |
|
|
77
|
+
| **Re-running** | Pay again | **Every response cached** โ change a check or a label, rebuild the report for free with `--cache-only` |
|
|
78
|
+
| **Regressions** | Found by users | `--fail-under 0.9` fails CI when a new skill starts stealing traffic from an old one |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ๐ Results on the bundled example
|
|
83
|
+
|
|
84
|
+
`examples/commit-message` โ a skill that enforces a team commit-message convention, plus a `code-review` skill offered as a distractor. 10 cases: 5 requests that must load the skill and follow it, 5 near-misses that must not load it. 3 repeats each, all models through OpenRouter.
|
|
85
|
+
|
|
86
|
+
| model | pass with skill | pass without skill | trigger precision / recall | flaky cases | cost, 30 trials | avg latency |
|
|
87
|
+
|---|---|---|---|---|---|---|
|
|
88
|
+
| gpt-5.6-luna | **30/30** (89-100%) | 15/30 | 100% / 100% | 0 | $0.007 | 2.5 s |
|
|
89
|
+
| kimi-k3 | **29/30** (83-99%) | incomplete* | 100% / 100% | 1 | $0.194 | 30.4 s |
|
|
90
|
+
| qwen3.8-flash | **24/30** (63-90%) | 15/30 | 100% / 100% | 2 | $0.007 | 12.8 s |
|
|
91
|
+
| glm-5.3-flash | **23/30** (59-88%) | 15/30 | 100% / 100% | 5 | $0.008 | 17.6 s |
|
|
92
|
+
| deepseek-v4.1-flash | **19/30** (46-78%) | 15/30 | 100% / 100% | 4 | $0.011 | 3.2 s |
|
|
93
|
+
|
|
94
|
+
Brackets are 95% Wilson intervals. *The run was stopped manually before the kimi-k3 baseline finished (15 of 30 trials). Full report: [`examples/commit-message/results/report.md`](examples/commit-message/results/report.md).
|
|
95
|
+
|
|
96
|
+
What the numbers say:
|
|
97
|
+
|
|
98
|
+
- **Triggering was the easy part here** โ every model loaded the skill exactly when it should. Following it is where they split.
|
|
99
|
+
- **Without the skill every model scores 15/30** โ only the five near-misses pass. None of the commit requests match the convention on general knowledge, so the gap is the skill's measured effect.
|
|
100
|
+
- **Qwen follows the shape but not the details**: it leaves an empty `Refs:` line when there is no ticket and writes `config:` instead of `chore(config):`.
|
|
101
|
+
- **GLM and DeepSeek are flaky**: in 5 and 4 of 10 cases the same prompt passed on one repeat and failed on another. A single run would have told a different story each time โ that is why repeats are the default, not an option.
|
|
102
|
+
|
|
103
|
+
Ten cases is a small probe; the point is the method. Treat the ranking as specific to this skill.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ๐งญ Routing across 28 public skills
|
|
108
|
+
|
|
109
|
+
A real agent never sees one skill. It sees a list โ and every description in that list competes for every request. So we took the skills people actually install and asked the question nobody measures: **when 14 skills are on the table, does the model pick the right one?**
|
|
110
|
+
|
|
111
|
+
Two probes, real skills, unchanged descriptions:
|
|
112
|
+
|
|
113
|
+
- [`examples/public-skills/anthropic`](examples/public-skills/anthropic) โ 14 skills from [anthropics/skills](https://github.com/anthropics/skills), 25 labeled cases;
|
|
114
|
+
- [`examples/public-skills/superpowers`](examples/public-skills/superpowers) โ 14 skills from [obra/superpowers](https://github.com/obra/superpowers), 21 labeled cases.
|
|
115
|
+
|
|
116
|
+
Each case says which skill must load and which must not ([how the golden sets are labeled](examples/public-skills/LABELS.md)). `routing_only` stops every trial after the first turn, 3 repeats per case, all models through OpenRouter.
|
|
117
|
+
|
|
118
|
+
| probe | gpt-5.6-luna | qwen3.8-flash | deepseek-v4.1-flash | glm-5.3-flash | kimi-k3 |
|
|
119
|
+
|---|---|---|---|---|---|
|
|
120
|
+
| anthropic, 75 trials per model | 72/75 ยท $0.011 | 74/75 ยท $0.018 | **75/75** ยท $0.026 | **75/75** ยท $0.017 | 74/75 ยท $2.28 |
|
|
121
|
+
| superpowers, 63 trials per model | 59/63 ยท $0.014 | **61/63** ยท $0.012 | 60/63 ยท $0.014 | 59/63 ยท $0.008 | not run* |
|
|
122
|
+
|
|
123
|
+
<sub>*kimi-k3 costs about 100x the others per routing trial, so it was run on the first probe only. Full reports: [anthropic](examples/public-skills/anthropic/results/report.md), [superpowers](examples/public-skills/superpowers/results/report.md).</sub>
|
|
124
|
+
|
|
125
|
+
The headline number is high โ and that is exactly why the misses are worth reading. They are not random. Every one of them traces back to how a description is written:
|
|
126
|
+
|
|
127
|
+
| skill | what happened | models |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| superpowers `test-driven-development` | "Add a function that validates IBANs" loaded `brainstorming` in **12 of 12** trials; TDD โ its own description says "implementing any feature" โ loaded in 2 of 12, and only next to brainstorming | all four |
|
|
130
|
+
| superpowers `brainstorming` | "You MUST use this before any creative work" โ loaded next to the expected skill in **22 of 48** trials where it was a plausible extra, e.g. on top of `writing-skills` | all four |
|
|
131
|
+
| anthropic `academy-guide` | "Build an MCP server for our Jira" pulled in the learning-resources skill **3 of 3** times: its description says "stop and check this skill before finishing any reply to a question about ... MCP" | gpt-5.6-luna |
|
|
132
|
+
| anthropic `claude-api` | loaded for a question about the **openai** package, although the description has an explicit "SKIP when OpenAI/GPT named" clause | qwen3.8-flash, 1 of 3 |
|
|
133
|
+
| anthropic `mcp-builder` | loaded for "In two sentences: what is the Model Context Protocol?" โ an explanation, not a build | kimi-k3, 1 of 3 |
|
|
134
|
+
| anthropic `discernment-nudge` | written to run *after* an answer, yet loaded on the first turn in 4-9 of 54 trials per model | all five |
|
|
135
|
+
| superpowers `verification-before-completion` | "Done fixing the date parser. Commit it and open the PR" went to `finishing-a-development-branch` instead | glm-5.3-flash, 1 of 3 |
|
|
136
|
+
|
|
137
|
+
And the clean side of the ledger: all 13 Anthropic skills that had a positive case loaded every time they should, on all five models; 10 of them never loaded when they shouldn't. `systematic-debugging` went 9 for 9 on every model.
|
|
138
|
+
|
|
139
|
+
What it says:
|
|
140
|
+
|
|
141
|
+
- **Routing between well-separated skills is close to solved** โ flash models get it right for one to three cents per 75 trials.
|
|
142
|
+
- **The failures are written, not sampled.** Descriptions that shout โ "You MUST", "Stop and check this skill before finishing any reply", "any" โ win requests that belong to a quieter neighbor. A description is a routing rule; write it like one.
|
|
143
|
+
- **First-turn routing is the cheap test you can run in CI**: label your requests once, run `--fail-under`, and a new skill that steals traffic from an old one fails the build.
|
|
144
|
+
|
|
145
|
+
Limits, plainly: one labeler, 46 cases, first turn only โ a model that loads a second skill after reading the first one's body is not measured here. Disagree with a label? Change it in `cases.yaml` and rerun with `--cache-only`: the report rebuilds from cached responses for free.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## โ๏ธ How It Works
|
|
150
|
+
|
|
151
|
+
```mermaid
|
|
152
|
+
flowchart LR
|
|
153
|
+
A[๐ Golden set<br/>cases.yaml: prompt + expect] --> B[๐งฐ Skills offered<br/>name + description only]
|
|
154
|
+
B --> C[๐ค Model turn<br/>call load_skill?]
|
|
155
|
+
C -->|loads| D[๐ SKILL.md body<br/>returned as tool result]
|
|
156
|
+
D --> E[๐ฌ Final answer]
|
|
157
|
+
C -->|no call| E
|
|
158
|
+
E --> F[โ
Checks<br/>triggered ยท loads ยท not_loads<br/>contains ยท regex ยท json_schema]
|
|
159
|
+
F --> G[๐ Report<br/>Wilson CI ยท flaky ยท routing ยท cost]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This mirrors how agent harnesses expose skills: the body stays out of context until the model asks for it. Whether it asks is exactly what the trigger and routing metrics measure. With `routing_only: true` the trial stops right after the model's first turn.
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary><b>One trial, step by step</b></summary>
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
system: base prompt + list of skills (name: description only)
|
|
169
|
+
tools: load_skill(name)
|
|
170
|
+
user: case prompt + attached files
|
|
171
|
+
|
|
|
172
|
+
v
|
|
173
|
+
model calls load_skill? --yes--> tool result = SKILL.md body --> model answers
|
|
174
|
+
|
|
|
175
|
+
no --> model answers
|
|
176
|
+
|
|
|
177
|
+
v
|
|
178
|
+
checks: triggered? loads / not_loads? contains / not_contains / regex / json_schema / max_chars
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
</details>
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## โจ Features
|
|
186
|
+
|
|
187
|
+
| | Feature | How it works |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| ๐ฏ | **Trigger metrics** | Precision / recall of loading the target skill, on positives and near-misses |
|
|
190
|
+
| ๐งญ | **Routing across many skills** | `loads` / `not_loads` labels per case (`"*"` = no skill at all), per-skill routing table, collisions table |
|
|
191
|
+
| โก | **`routing_only` mode** | Stops after the first turn, skill bodies are never sent โ thirty skills and a hundred cases cost cents |
|
|
192
|
+
| ๐ | **Repeats + 95% Wilson intervals** | Every case runs N times; a prompt that passes on one repeat and fails on another is flagged flaky |
|
|
193
|
+
| ๐งช | **Deterministic checks** | `contains` / `not_contains` / `regex` / `json_schema` / `max_chars` โ no judge model needed |
|
|
194
|
+
| ๐ | **Baseline** | `--baseline` runs every model with no skills offered โ shows what the skill adds |
|
|
195
|
+
| ๐พ | **Response cache** | Keyed by the full request and the repeat number; `--cache-only` rebuilds reports offline |
|
|
196
|
+
| ๐ธ | **Cost & latency** | Per model and per run; `max_cost_usd` is a hard budget cap |
|
|
197
|
+
| ๐ฆ | **CI gate** | `--fail-under 0.9` exits 1 when any model's pass rate is lower; a ready GitHub Action runs it in one step and puts the report in the job summary |
|
|
198
|
+
| ๐ | **Providers** | `openrouter`, `openai`, `deepseek`, `dashscope`, `zai`, `moonshot`, `yandex` built in; anything OpenAI-compatible in two lines |
|
|
199
|
+
| ๐ | **Reports** | Markdown + JSON: summary per model, routing tables, failures with the model's output |
|
|
200
|
+
| ๐ณ | **Docker** | Ready image `ghcr.io/andreysukhanov/skillprobe` (amd64, arm64) โ run a probe in CI or locally without a Python setup |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## ๐ Quick Start
|
|
205
|
+
|
|
206
|
+
Installed from PyPI with `pipx install skilldx` (the PyPI name: `skillprobe` there is an unrelated project), the command is `skillprobe` (or `skilldx`). To try the bundled examples, take the repo:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# 1) Install
|
|
210
|
+
git clone https://github.com/AndreySukhanov/skillprobe.git
|
|
211
|
+
cd skillprobe
|
|
212
|
+
pip install -e .
|
|
213
|
+
|
|
214
|
+
# 2) Add a key
|
|
215
|
+
export OPENROUTER_API_KEY=... # or put it in a .env next to probe.yaml
|
|
216
|
+
|
|
217
|
+
# 3) Run the bundled example on one model
|
|
218
|
+
skillprobe run examples/commit-message/probe.yaml --repeats 1 --models qwen3.8-flash
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
No Python at hand? The same run from the ready image (amd64 and arm64), with your probe folder mounted as `/work`:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
docker run --rm -v "$PWD:/work" -e OPENROUTER_API_KEY ghcr.io/andreysukhanov/skillprobe \
|
|
225
|
+
run examples/commit-message/probe.yaml --repeats 1 --models qwen3.8-flash
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Useful flags:
|
|
229
|
+
|
|
230
|
+
| flag | what it does |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `--baseline` | also run every model with no skills offered โ shows what the skill adds |
|
|
233
|
+
| `--repeats N` | override repeats for all cases |
|
|
234
|
+
| `--models a,b` / `--cases x,y` | run a subset |
|
|
235
|
+
| `--cache-only` | never call the API: rebuild the report from cached responses |
|
|
236
|
+
| `--fail-under 0.9` | exit 1 if any model's pass rate is lower โ for CI |
|
|
237
|
+
|
|
238
|
+
Every response is cached on disk (`.skillprobe_cache/`), keyed by the full request and the repeat number. Editing checks and re-running costs nothing. `max_cost_usd` in the config stops a run before it overspends.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## ๐งช Writing a Probe
|
|
243
|
+
|
|
244
|
+
`probe.yaml`:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
skills:
|
|
248
|
+
- skills/commit-message # the skill under test
|
|
249
|
+
- skills/code-review # distractors are offered too
|
|
250
|
+
target: commit-message
|
|
251
|
+
cases: cases.yaml
|
|
252
|
+
repeats: 3
|
|
253
|
+
max_cost_usd: 2.0
|
|
254
|
+
|
|
255
|
+
models:
|
|
256
|
+
- name: qwen3.8-flash
|
|
257
|
+
model: qwen/qwen3.8-flash # provider defaults to openrouter
|
|
258
|
+
- name: deepseek-direct
|
|
259
|
+
provider: deepseek # DEEPSEEK_API_KEY
|
|
260
|
+
model: deepseek-chat
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`cases.yaml`:
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
- id: ticket-from-branch
|
|
267
|
+
prompt: I'm on branch PAY-482-invoice-pdf. What should I put in git commit for this?
|
|
268
|
+
files: [fixtures/billing_invoice_pdf.diff]
|
|
269
|
+
expect:
|
|
270
|
+
triggered: true
|
|
271
|
+
regex:
|
|
272
|
+
- '\Afeat\(billing\): '
|
|
273
|
+
- '^Refs: PAY-482\s*$'
|
|
274
|
+
|
|
275
|
+
- id: review-is-not-commit # near-miss: a diff, but not a commit request
|
|
276
|
+
prompt: Review this diff before I merge it. Anything dangerous?
|
|
277
|
+
files: [fixtures/sql_injection.diff]
|
|
278
|
+
expect:
|
|
279
|
+
triggered: false
|
|
280
|
+
contains: ["injection"]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Good probes are mostly near-misses. A skill that loads for "write a haiku" is rare; one that loads for "review this diff" when it is a commit-message skill is common.
|
|
284
|
+
|
|
285
|
+
<details>
|
|
286
|
+
<summary><b>๐งญ Routing labels for many competing skills</b></summary>
|
|
287
|
+
|
|
288
|
+
`triggered` scores one target skill. When several skills compete for the same request, label the routing instead:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
- id: failing-test-is-debugging
|
|
292
|
+
prompt: test_invoice_total started failing after yesterday's merge - expected 120.00, got 119.99.
|
|
293
|
+
expect:
|
|
294
|
+
loads: [systematic-debugging] # must be loaded
|
|
295
|
+
not_loads: [brainstorming, writing-plans] # must not be loaded; other skills are not scored
|
|
296
|
+
|
|
297
|
+
- id: plain-question
|
|
298
|
+
prompt: What does HTTP 418 mean?
|
|
299
|
+
expect:
|
|
300
|
+
not_loads: "*" # no skill at all
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
For a routing probe set `routing_only: true` in `probe.yaml`: each trial stops after the model's first turn, skill bodies are never sent, and only the loads are scored. Thirty skills and a hundred cases cost cents.
|
|
304
|
+
|
|
305
|
+
</details>
|
|
306
|
+
|
|
307
|
+
<details>
|
|
308
|
+
<summary><b>๐ Providers</b></summary>
|
|
309
|
+
|
|
310
|
+
Built in: `openrouter`, `openai`, `deepseek`, `dashscope` (Qwen), `zai` (GLM), `moonshot` (Kimi), `yandex` (YandexGPT). Each is an OpenAI-compatible base URL plus the name of the environment variable holding the key. Anything else โ a local vLLM, a GigaChat proxy, a corporate gateway โ is two lines under `providers:`.
|
|
311
|
+
|
|
312
|
+
The example runs use OpenRouter only; the direct vendor presets follow each vendor's documented OpenAI-compatible API.
|
|
313
|
+
|
|
314
|
+
</details>
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## โ FAQ
|
|
319
|
+
|
|
320
|
+
### How do I test whether my Claude skill triggers?
|
|
321
|
+
Point `probe.yaml` at your skill folder and its neighbors, write `cases.yaml` with requests that must load the skill and near-misses that must not, and run `skillprobe run probe.yaml`. The report gives trigger precision and recall per model, and every failure with the model's output. See [Writing a Probe](#-writing-a-probe).
|
|
322
|
+
|
|
323
|
+
### What is a routing collision between skills?
|
|
324
|
+
Two skills loading for the same request, or the wrong one winning it. Label each case with `loads` / `not_loads` and the report adds a per-skill routing table โ loaded when required, loaded when forbidden โ plus a collisions table. On 28 public skills, "Add a function that validates IBANs" went to `brainstorming` in 12 of 12 trials instead of `test-driven-development`: see [Routing across 28 public skills](#-routing-across-28-public-skills).
|
|
325
|
+
|
|
326
|
+
### Which models and providers does it support?
|
|
327
|
+
Any model behind an OpenAI-compatible chat API with tool calling โ skills load through a `load_skill` tool, the way agent harnesses expose them. Presets are built in for OpenRouter, OpenAI, DeepSeek, Qwen (DashScope), GLM (Z.ai), Kimi (Moonshot) and YandexGPT; a local vLLM or a corporate gateway is two lines of config.
|
|
328
|
+
|
|
329
|
+
### Do I need an LLM judge?
|
|
330
|
+
No. Checks are deterministic โ `triggered`, `loads`, `not_loads`, `contains`, `not_contains`, `regex`, `json_schema`, `max_chars` โ so the same output always gets the same verdict. LLM-as-judge with calibration against human labels is on the [roadmap](#-roadmap).
|
|
331
|
+
|
|
332
|
+
### How much does a run cost?
|
|
333
|
+
Cents on flash models: 30 trials of the bundled example cost $0.007-0.011, and 75 routing trials across 14 skills cost $0.011-0.026. `max_cost_usd` stops a run before it overspends, and every response is cached โ rebuilding a report with `--cache-only` costs nothing.
|
|
334
|
+
|
|
335
|
+
### Why run every case several times?
|
|
336
|
+
Because one run lies. In the bundled example GLM and DeepSeek passed and failed the same prompt on different repeats in 5 and 4 of 10 cases. Repeats are the default, pass rates come with 95% Wilson intervals, and flaky cases are flagged.
|
|
337
|
+
|
|
338
|
+
### How do I measure what my skill actually adds?
|
|
339
|
+
Run with `--baseline`: the same cases with no skills offered. In the bundled example every model scores 15/30 without the skill and up to 30/30 with it โ the gap is the skill's measured effect.
|
|
340
|
+
|
|
341
|
+
### Can I run it in CI?
|
|
342
|
+
Yes. `--fail-under 0.9` exits 1 when any model's pass rate drops below the threshold, so a new skill that steals traffic from an old one fails the build. A ready GitHub Action does it in one step:
|
|
343
|
+
|
|
344
|
+
In GitHub Actions, the same gate in one step (the key from your repository secrets; the report lands in the job summary):
|
|
345
|
+
|
|
346
|
+
```yaml
|
|
347
|
+
- uses: AndreySukhanov/skillprobe@v0.3.0
|
|
348
|
+
env:
|
|
349
|
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
350
|
+
with:
|
|
351
|
+
probe: evals/probe.yaml
|
|
352
|
+
fail-under: "0.9"
|
|
353
|
+
args: --repeats 3
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
A ready Docker image (`ghcr.io/andreysukhanov/skillprobe`) works in any other CI.
|
|
357
|
+
|
|
358
|
+
**A skill is a routing rule plus a prompt โ skillprobe measures both.**
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## ๐บ Roadmap
|
|
363
|
+
|
|
364
|
+
- [x] Skill loading via tool call, deterministic checks, trigger precision/recall, repeats with intervals, flaky detection, cost and latency, response cache, budget cap
|
|
365
|
+
- [x] Routing between many skills: `loads` / `not_loads` labels, `routing_only` first-turn mode, per-skill routing table and collisions, golden sets on public skills
|
|
366
|
+
- [ ] **LLM-as-judge** with rubrics, and pairwise comparison of two skill versions
|
|
367
|
+
- [ ] **Judge calibration** against human labels โ a judge that disagrees with people is flagged, not trusted
|
|
368
|
+
- [ ] **Judge panels** from different model families, with disagreements reported instead of averaged
|
|
369
|
+
- [ ] **`skillprobe compare`** โ what broke between skill v1 and v2, as a GitHub PR comment
|
|
370
|
+
- [ ] **Adversarial pack** โ prompt injection through attached files, language switching
|
|
371
|
+
|
|
372
|
+
Vote with ๐ on [issues](https://github.com/AndreySukhanov/skillprobe/issues), or open a new one.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## ๐ค Contributing
|
|
377
|
+
|
|
378
|
+
The most valuable contributions are small and self-contained:
|
|
379
|
+
|
|
380
|
+
- A golden set for a public skill collection โ labeled by the rules in [LABELS.md](examples/public-skills/LABELS.md)
|
|
381
|
+
- A provider preset for another OpenAI-compatible API
|
|
382
|
+
- A new deterministic check
|
|
383
|
+
|
|
384
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Open an issue first for bigger changes โ happy to discuss the design.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
pip install -e ".[dev]"
|
|
388
|
+
pytest # scripted fake client - no network, no keys
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## โญ Support the project
|
|
394
|
+
|
|
395
|
+
If this project saved you time or showed you something new โ **star it**. It genuinely helps others discover it.
|
|
396
|
+
|
|
397
|
+
### ๐ Sponsor development
|
|
398
|
+
|
|
399
|
+
This is a self-funded project โ every benchmark in this README was paid for out of pocket in model credits (the routing run alone cost about $2.40). A donation goes straight into credits for bigger golden sets and more model families.
|
|
400
|
+
|
|
401
|
+
<table>
|
|
402
|
+
<tr>
|
|
403
|
+
<td valign="top">
|
|
404
|
+
|
|
405
|
+
**USDT โ TRC-20 (Tron network)**
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
TERj89Gjf4qsMW7xyKQWGWtNmBQaDUPnnH
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
> โ ๏ธ Send **only USDT via the TRC-20 (Tron)** network to this address. Funds sent through any other network will be lost.
|
|
412
|
+
|
|
413
|
+
</td>
|
|
414
|
+
<td valign="top">
|
|
415
|
+
<img src="assets/donate-usdt.png" alt="USDT TRC-20 donation QR code" width="180"/>
|
|
416
|
+
</td>
|
|
417
|
+
</tr>
|
|
418
|
+
</table>
|
|
419
|
+
|
|
420
|
+
Thank you โ even a small contribution keeps the probes running. ๐
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## ๐ License
|
|
425
|
+
|
|
426
|
+
**Source-Available** โ free to read, run and learn from. Commercial or derivative use requires written permission. See [LICENSE](LICENSE).
|
|
427
|
+
|
|
428
|
+
Third-party skills under `examples/public-skills/` keep their own licenses (Apache-2.0, MIT) โ see [SOURCES.md](examples/public-skills/SOURCES.md).
|
|
429
|
+
|
|
430
|
+
Licensing contact: [avsukhanov21@gmail.com](mailto:avsukhanov21@gmail.com)
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
<div align="center">
|
|
435
|
+
|
|
436
|
+
**Built with Python + Pydantic + Wilson intervals**
|
|
437
|
+
|
|
438
|
+
[GitHub](https://github.com/AndreySukhanov) ยท [LinkedIn](https://www.linkedin.com/in/andrei-sukhanov-2605821b9) ยท [Email](mailto:avsukhanov21@gmail.com)
|
|
439
|
+
|
|
440
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
skillprobe/__init__.py,sha256=_frRzHBejtPw99vJv9lUz3tesiqOzRqQWw0s_VU2yC0,73
|
|
2
|
+
skillprobe/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
skillprobe/cache.py,sha256=1_xI5CRdRuz6HMTxhmKlzykjW8T7Bdn2B6NvEEYqdww,1140
|
|
4
|
+
skillprobe/cases.py,sha256=qwPIdjXL2AQ_HgKRwGLZMiO4F1S17ICwlSDYqOtWy84,3981
|
|
5
|
+
skillprobe/checks.py,sha256=UA0mSoiSJom-cDKWgOeJXDlC9ib5Q1o4IfTAP_YWBoM,2658
|
|
6
|
+
skillprobe/cli.py,sha256=rfbE75wZS78h6pdXQIL1dOGlDazm6ErpgtfrlpJlENY,4216
|
|
7
|
+
skillprobe/config.py,sha256=2vXGcRz8mwOUB81vN4osZMXewdqhpF79UIXowiwH4D4,3693
|
|
8
|
+
skillprobe/engine.py,sha256=sLAxbFRJKl-D5sDJ6X6AFUe5j_uQIc7whAVWN4Bqw7s,12142
|
|
9
|
+
skillprobe/report.py,sha256=JlxSd8kO5Xk7zMMEfcyZGrRYpi4LbPpvLAdipzirk9M,6904
|
|
10
|
+
skillprobe/runner.py,sha256=ZQmZ14s2kQgjkgzv75aK0uoeaBeuDrePvW8R-pao3WE,8530
|
|
11
|
+
skillprobe/skills.py,sha256=1D-fMALASw2_v_kJqd2K_QTRH3X45VUZwxKXfeFA8pc,1442
|
|
12
|
+
skillprobe/stats.py,sha256=9HhJVLzIb1Bm9YqkyYcEqIa_bsNh914P6NI8LoqZhsM,697
|
|
13
|
+
skilldx-0.3.0.dist-info/METADATA,sha256=lZtRfISo0gOnRA_8-QhKBDDo_DJ5K4u-_04NVbxWhkk,24512
|
|
14
|
+
skilldx-0.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
15
|
+
skilldx-0.3.0.dist-info/entry_points.txt,sha256=Uzyvs8nGdFLHowWr4SEnmM-y1X6jEPURBctAE9MJoX0,81
|
|
16
|
+
skilldx-0.3.0.dist-info/licenses/LICENSE,sha256=dqBkitT6GmN6jP3oqbVOjkdiv3TW-9vkjpM8JhA8VhQ,2390
|
|
17
|
+
skilldx-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Source-Available License
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Andrey Sukhanov
|
|
5
|
+
|
|
6
|
+
The source code of skillprobe (the "Software") is publicly available.
|
|
7
|
+
It is free to read, run and learn from for personal, educational, research
|
|
8
|
+
and other non-commercial purposes. Commercial or derivative use requires
|
|
9
|
+
written permission. All other rights reserved.
|
|
10
|
+
|
|
11
|
+
You MAY, free of charge:
|
|
12
|
+
- View, read, and study the source code.
|
|
13
|
+
- Download, install and run the Software for personal, educational,
|
|
14
|
+
research or other non-commercial purposes.
|
|
15
|
+
- Fork this repository on GitHub and change your fork in order to
|
|
16
|
+
propose changes back to it (pull requests).
|
|
17
|
+
- Reference it for learning, education, and discussion of techniques.
|
|
18
|
+
- Cite or quote excerpts in academic, journalistic, or educational
|
|
19
|
+
contexts with proper attribution.
|
|
20
|
+
|
|
21
|
+
You MAY NOT, without prior written permission from the copyright holder:
|
|
22
|
+
- Use the Software or any part of it for commercial purposes, including
|
|
23
|
+
within a commercial organization, in any commercial product or
|
|
24
|
+
service, or to provide services to third parties for a fee.
|
|
25
|
+
- Copy, publish, distribute, sublicense, or sell copies of the Software
|
|
26
|
+
or substantial portions of it, modified or not, other than GitHub
|
|
27
|
+
forks made to propose changes back to this repository.
|
|
28
|
+
- Create derivative works based on this code or its design, other than
|
|
29
|
+
changes proposed back to this repository.
|
|
30
|
+
- Use this code, in whole or in part, as training data for machine
|
|
31
|
+
learning models or AI systems.
|
|
32
|
+
- Use the name "skillprobe" or the names of its authors to
|
|
33
|
+
promote derivative products.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
38
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
39
|
+
OTHER LIABILITY ARISING FROM USE OF THE SOFTWARE.
|
|
40
|
+
|
|
41
|
+
Third-party content: the skills under examples/public-skills/anthropic/skills
|
|
42
|
+
and examples/public-skills/superpowers/skills are copies of third-party works
|
|
43
|
+
and remain under their own licenses (Apache-2.0 and MIT respectively); see
|
|
44
|
+
examples/public-skills/SOURCES.md. This license does not apply to them.
|
|
45
|
+
|
|
46
|
+
For licensing inquiries (commercial use, derivative works, integrations),
|
|
47
|
+
please contact: avsukhanov21@gmail.com
|
skillprobe/__init__.py
ADDED
skillprobe/__main__.py
ADDED
skillprobe/cache.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""On-disk response cache: rerunning a probe after editing checks costs nothing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cache:
|
|
12
|
+
def __init__(self, directory: str | Path):
|
|
13
|
+
self.dir = Path(directory)
|
|
14
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def key(payload: dict[str, Any]) -> str:
|
|
18
|
+
blob = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
|
19
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
20
|
+
|
|
21
|
+
def _path(self, key: str) -> Path:
|
|
22
|
+
return self.dir / key[:2] / f"{key}.json"
|
|
23
|
+
|
|
24
|
+
def get(self, key: str) -> dict[str, Any] | None:
|
|
25
|
+
path = self._path(key)
|
|
26
|
+
if not path.is_file():
|
|
27
|
+
return None
|
|
28
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
29
|
+
|
|
30
|
+
def set(self, key: str, value: dict[str, Any]) -> None:
|
|
31
|
+
path = self._path(key)
|
|
32
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
tmp = path.with_suffix(".tmp")
|
|
34
|
+
tmp.write_text(json.dumps(value, ensure_ascii=False), encoding="utf-8")
|
|
35
|
+
tmp.replace(path)
|