whetkit 0.4.0__tar.gz → 0.6.0__tar.gz
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.
- {whetkit-0.4.0 → whetkit-0.6.0}/PKG-INFO +13 -4
- {whetkit-0.4.0 → whetkit-0.6.0}/README.md +12 -3
- {whetkit-0.4.0 → whetkit-0.6.0}/pyproject.toml +1 -1
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/cli.py +50 -7
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/generate.py +5 -1
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/aggregate.py +22 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/LICENSE +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/export.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/optimizer.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/overlay.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/plan.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/datasets/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/datasets/tasks.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/doctor.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/anthropic_provider.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/base.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/openai_provider.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/pricing.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/registry.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/client.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/introspect.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/transport.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/py.typed +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/builder.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/html.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/runner/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/runner/agent.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/deterministic.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/judge.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/tracing/__init__.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/tracing/records.py +0 -0
- {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/tracing/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whetkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Measure and improve how well LLM agents select and use the tools exposed by an MCP server
|
|
5
5
|
Keywords: mcp,model-context-protocol,llm,agents,evals,tool-use,tool-selection,benchmark
|
|
6
6
|
Author: Mohammed Benlamlih
|
|
@@ -163,12 +163,21 @@ back to the original world.
|
|
|
163
163
|
|---|---|
|
|
164
164
|
| `whetkit doctor` | Lint the tool surface: vague descriptions, cryptic names, near-duplicates, context bloat. `--json`; `--fail-on warn` for CI. |
|
|
165
165
|
| `whetkit inspect` | Tool inventory: names, params, description tokens, schema complexity. |
|
|
166
|
-
| `whetkit generate` | Draft eval tasks from the inventory
|
|
167
|
-
| `whetkit run` | Agentic eval
|
|
168
|
-
| `whetkit curate` | Baseline → LLM-proposed overlay plan → curated eval → before/after report. |
|
|
166
|
+
| `whetkit generate` | Draft eval tasks from the inventory (server-context aware, read-only unless `--allow-writes`); review-before-trust YAML. |
|
|
167
|
+
| `whetkit run` | Agentic eval with real tool execution. `--runs N` for mean±range and flaky-task detection, `--concurrency` for independent tasks, `--reset-cmd` for stateful fixtures, `--plan` to score a curated view, `--summary-json` for machine-readable results (with cost estimates). |
|
|
168
|
+
| `whetkit curate` | Baseline → LLM-proposed overlay plan → curated eval → before/after report. `--prune-unused` for the cost play. |
|
|
169
|
+
| `whetkit fix` | Self-correcting curation: propose → eval → feed regressions back → revise, up to `--max-iterations`; keeps the best plan by measured results. |
|
|
170
|
+
| `whetkit plan-init` | Scaffold a view plan: `--keep a,b`, `--from-tasks tasks/`, `--from-traces traces.sqlite3` — hide everything else. |
|
|
171
|
+
| `whetkit diff` | Compare two `--summary-json` files: metric deltas + per-task PASS/MISS/FLAKY transitions. |
|
|
172
|
+
| `whetkit export` | Share a plan: `--to markdown` (upstream-PR fix table) or `--to json` (gateway overrides). |
|
|
169
173
|
| `whetkit report` | Rebuild the HTML/JSON report from stored traces. |
|
|
170
174
|
| `whetkit overlay` | Serve the curated view as a stdio MCP server. |
|
|
171
175
|
|
|
176
|
+
Authenticated servers: any string in `server.json` may reference `${ENV_VARS}`
|
|
177
|
+
(e.g. an `Authorization` header) — credentials never live in the file.
|
|
178
|
+
Summaries flag probable task-spec gaps, errored runs, and failed tool calls
|
|
179
|
+
so a bad score always says why.
|
|
180
|
+
|
|
172
181
|
## How scoring works
|
|
173
182
|
|
|
174
183
|
- **Deterministic tool-match**: each task lists the expected tool calls
|
|
@@ -132,12 +132,21 @@ back to the original world.
|
|
|
132
132
|
|---|---|
|
|
133
133
|
| `whetkit doctor` | Lint the tool surface: vague descriptions, cryptic names, near-duplicates, context bloat. `--json`; `--fail-on warn` for CI. |
|
|
134
134
|
| `whetkit inspect` | Tool inventory: names, params, description tokens, schema complexity. |
|
|
135
|
-
| `whetkit generate` | Draft eval tasks from the inventory
|
|
136
|
-
| `whetkit run` | Agentic eval
|
|
137
|
-
| `whetkit curate` | Baseline → LLM-proposed overlay plan → curated eval → before/after report. |
|
|
135
|
+
| `whetkit generate` | Draft eval tasks from the inventory (server-context aware, read-only unless `--allow-writes`); review-before-trust YAML. |
|
|
136
|
+
| `whetkit run` | Agentic eval with real tool execution. `--runs N` for mean±range and flaky-task detection, `--concurrency` for independent tasks, `--reset-cmd` for stateful fixtures, `--plan` to score a curated view, `--summary-json` for machine-readable results (with cost estimates). |
|
|
137
|
+
| `whetkit curate` | Baseline → LLM-proposed overlay plan → curated eval → before/after report. `--prune-unused` for the cost play. |
|
|
138
|
+
| `whetkit fix` | Self-correcting curation: propose → eval → feed regressions back → revise, up to `--max-iterations`; keeps the best plan by measured results. |
|
|
139
|
+
| `whetkit plan-init` | Scaffold a view plan: `--keep a,b`, `--from-tasks tasks/`, `--from-traces traces.sqlite3` — hide everything else. |
|
|
140
|
+
| `whetkit diff` | Compare two `--summary-json` files: metric deltas + per-task PASS/MISS/FLAKY transitions. |
|
|
141
|
+
| `whetkit export` | Share a plan: `--to markdown` (upstream-PR fix table) or `--to json` (gateway overrides). |
|
|
138
142
|
| `whetkit report` | Rebuild the HTML/JSON report from stored traces. |
|
|
139
143
|
| `whetkit overlay` | Serve the curated view as a stdio MCP server. |
|
|
140
144
|
|
|
145
|
+
Authenticated servers: any string in `server.json` may reference `${ENV_VARS}`
|
|
146
|
+
(e.g. an `Authorization` header) — credentials never live in the file.
|
|
147
|
+
Summaries flag probable task-spec gaps, errored runs, and failed tool calls
|
|
148
|
+
so a bad score always says why.
|
|
149
|
+
|
|
141
150
|
## How scoring works
|
|
142
151
|
|
|
143
152
|
- **Deterministic tool-match**: each task lists the expected tool calls
|
|
@@ -79,6 +79,8 @@ def _summary_payload(group_name: str, summary, task_runs: list) -> dict:
|
|
|
79
79
|
"tool_hit": score.tool_hit,
|
|
80
80
|
"judge_passed": score.judge.passed if score.judge else None,
|
|
81
81
|
"spec_gap": score.spec_gap,
|
|
82
|
+
"run_status": str(score.run_status),
|
|
83
|
+
"tool_errors": score.tool_errors,
|
|
82
84
|
"called": score.tool_match.called,
|
|
83
85
|
"missing": score.tool_match.missing_slots,
|
|
84
86
|
"extra_calls": score.tool_match.extra_calls,
|
|
@@ -271,6 +273,20 @@ def plan_init(
|
|
|
271
273
|
help="Also keep every tool referenced by these tasks' expected_tools",
|
|
272
274
|
),
|
|
273
275
|
] = None,
|
|
276
|
+
from_traces: Annotated[
|
|
277
|
+
str | None,
|
|
278
|
+
typer.Option(
|
|
279
|
+
"--from-traces",
|
|
280
|
+
help=(
|
|
281
|
+
"Also keep every tool actually called in this trace store "
|
|
282
|
+
"(what real runs used, including tools no spec lists)"
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
] = None,
|
|
286
|
+
traces_group: Annotated[
|
|
287
|
+
str | None,
|
|
288
|
+
typer.Option("--traces-group", help="Restrict --from-traces to one run group"),
|
|
289
|
+
] = None,
|
|
274
290
|
out: Annotated[
|
|
275
291
|
str, typer.Option("--out", help="Where to write the plan YAML")
|
|
276
292
|
] = ".whetkit/curation-plan.yaml",
|
|
@@ -286,8 +302,18 @@ def plan_init(
|
|
|
286
302
|
for task in load_tasks(from_tasks):
|
|
287
303
|
for slot in task.expected_tool_slots:
|
|
288
304
|
keep_set.update(slot)
|
|
305
|
+
if from_traces:
|
|
306
|
+
from whetkit.tracing import TraceStore
|
|
307
|
+
|
|
308
|
+
if not Path(from_traces).is_file():
|
|
309
|
+
raise typer.BadParameter(f"no trace store at {from_traces}")
|
|
310
|
+
with TraceStore(from_traces) as trace_store:
|
|
311
|
+
for run in trace_store.load_runs(traces_group):
|
|
312
|
+
keep_set.update(run.called_tool_names)
|
|
289
313
|
if not keep_set:
|
|
290
|
-
raise typer.BadParameter(
|
|
314
|
+
raise typer.BadParameter(
|
|
315
|
+
"nothing to keep — pass --keep, --from-tasks, and/or --from-traces"
|
|
316
|
+
)
|
|
291
317
|
|
|
292
318
|
spec = _resolve_server(server, http_mode)
|
|
293
319
|
inventory = asyncio.run(inspect_server(spec))
|
|
@@ -480,6 +506,17 @@ def run(
|
|
|
480
506
|
help="Write a machine-readable summary (metrics + per-task outcomes) to this path",
|
|
481
507
|
),
|
|
482
508
|
] = None,
|
|
509
|
+
concurrency: Annotated[
|
|
510
|
+
int,
|
|
511
|
+
typer.Option(
|
|
512
|
+
"--concurrency",
|
|
513
|
+
help=(
|
|
514
|
+
"Run up to N tasks of a repetition in parallel (each gets its own "
|
|
515
|
+
"server connection). Only safe when tasks are independent — writes "
|
|
516
|
+
"to shared state should stay at 1."
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
] = 1,
|
|
483
520
|
reset_cmd: Annotated[
|
|
484
521
|
str | None,
|
|
485
522
|
typer.Option(
|
|
@@ -500,6 +537,8 @@ def run(
|
|
|
500
537
|
|
|
501
538
|
if runs < 1:
|
|
502
539
|
raise typer.BadParameter("--runs must be at least 1")
|
|
540
|
+
if concurrency < 1:
|
|
541
|
+
raise typer.BadParameter("--concurrency must be at least 1")
|
|
503
542
|
|
|
504
543
|
task_list = load_tasks(tasks)
|
|
505
544
|
servers = _resolve_task_servers(task_list, server, http_mode)
|
|
@@ -523,12 +562,16 @@ def run(
|
|
|
523
562
|
name_map = curation_plan.rename_map()
|
|
524
563
|
|
|
525
564
|
async def _run_once(group_name: str, cache: JudgeCache):
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
565
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
566
|
+
|
|
567
|
+
async def _one(task):
|
|
568
|
+
async with semaphore:
|
|
569
|
+
typer.echo(f"running {task.id} ...", err=True)
|
|
570
|
+
return await run_task(
|
|
571
|
+
task, servers[task.server], config, client_factory=client_factory
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
task_runs = list(await asyncio.gather(*(_one(task) for task in task_list)))
|
|
532
575
|
|
|
533
576
|
with TraceStore(store_path) as trace_store:
|
|
534
577
|
trace_store.save_runs(task_runs, run_group=group_name)
|
|
@@ -31,7 +31,11 @@ Rules for every task:
|
|
|
31
31
|
is either one tool name, or a list of genuinely interchangeable
|
|
32
32
|
alternatives for that step. Use ONLY tool names from the given list.
|
|
33
33
|
- success_criteria is one or two concrete, checkable sentences a grader can
|
|
34
|
-
verify from the agent's final answer alone.
|
|
34
|
+
verify from the agent's final answer alone. When the data source is live
|
|
35
|
+
or changing (news pages, network traffic, timestamps, prices), grade the
|
|
36
|
+
SHAPE of the answer ("names the current top story", "lists the requests
|
|
37
|
+
that were observed"), never a specific volatile value — a criterion that
|
|
38
|
+
is wrong an hour later is a flaky eval, not a strict one.
|
|
35
39
|
{writes_rule}
|
|
36
40
|
- Set "ordered": true only when the steps must happen in sequence.
|
|
37
41
|
- ids are short kebab-case slugs, unique across the set.
|
|
@@ -15,6 +15,7 @@ class TaskScore(BaseModel):
|
|
|
15
15
|
run_status: RunStatus
|
|
16
16
|
tool_match: ToolMatchResult
|
|
17
17
|
judge: JudgeVerdict | None = None
|
|
18
|
+
tool_errors: int = 0
|
|
18
19
|
|
|
19
20
|
@property
|
|
20
21
|
def tool_hit(self) -> bool:
|
|
@@ -82,6 +83,14 @@ class EvalSummary(BaseModel):
|
|
|
82
83
|
return 0.0
|
|
83
84
|
return sum(len(s.tool_match.extra_calls) for s in self.scores) / len(self.scores)
|
|
84
85
|
|
|
86
|
+
@property
|
|
87
|
+
def error_run_count(self) -> int:
|
|
88
|
+
return sum(s.run_status == RunStatus.ERROR for s in self.scores)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def total_tool_errors(self) -> int:
|
|
92
|
+
return sum(s.tool_errors for s in self.scores)
|
|
93
|
+
|
|
85
94
|
def summary_lines(self) -> list[str]:
|
|
86
95
|
lines = [
|
|
87
96
|
f"Tasks: {self.task_count}",
|
|
@@ -93,6 +102,17 @@ class EvalSummary(BaseModel):
|
|
|
93
102
|
]
|
|
94
103
|
if (rate := self.judge_pass_rate) is not None:
|
|
95
104
|
lines.insert(2, f"Judge pass-rate: {rate:.0%}")
|
|
105
|
+
if self.error_run_count:
|
|
106
|
+
lines.append(
|
|
107
|
+
f"⚠ Errored runs: {self.error_run_count}/{self.task_count} — the agent "
|
|
108
|
+
"loop failed (connection/provider); scores below reflect failures, "
|
|
109
|
+
"not tool selection"
|
|
110
|
+
)
|
|
111
|
+
if self.total_tool_errors:
|
|
112
|
+
lines.append(
|
|
113
|
+
f"⚠ Failed tool calls: {self.total_tool_errors} — agents received "
|
|
114
|
+
"errors from the server; read the traces before trusting the scores"
|
|
115
|
+
)
|
|
96
116
|
return lines
|
|
97
117
|
|
|
98
118
|
|
|
@@ -175,12 +195,14 @@ async def score_runs(
|
|
|
175
195
|
called = run.called_tool_names
|
|
176
196
|
if name_map:
|
|
177
197
|
called = [name_map.get(name, name) for name in called]
|
|
198
|
+
tool_errors = sum(call.is_error for turn in run.turns for call in turn.tool_calls)
|
|
178
199
|
scores.append(
|
|
179
200
|
TaskScore(
|
|
180
201
|
task_id=task.id,
|
|
181
202
|
run_status=run.status,
|
|
182
203
|
tool_match=score_tool_match(task, called, mode),
|
|
183
204
|
judge=verdict,
|
|
205
|
+
tool_errors=tool_errors,
|
|
184
206
|
)
|
|
185
207
|
)
|
|
186
208
|
return EvalSummary(scores=scores)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|