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.
Files changed (38) hide show
  1. {whetkit-0.4.0 → whetkit-0.6.0}/PKG-INFO +13 -4
  2. {whetkit-0.4.0 → whetkit-0.6.0}/README.md +12 -3
  3. {whetkit-0.4.0 → whetkit-0.6.0}/pyproject.toml +1 -1
  4. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/cli.py +50 -7
  5. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/generate.py +5 -1
  6. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/aggregate.py +22 -0
  7. {whetkit-0.4.0 → whetkit-0.6.0}/LICENSE +0 -0
  8. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/__init__.py +0 -0
  9. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/__init__.py +0 -0
  10. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/export.py +0 -0
  11. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/optimizer.py +0 -0
  12. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/overlay.py +0 -0
  13. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/curation/plan.py +0 -0
  14. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/datasets/__init__.py +0 -0
  15. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/datasets/tasks.py +0 -0
  16. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/doctor.py +0 -0
  17. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/__init__.py +0 -0
  18. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/anthropic_provider.py +0 -0
  19. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/base.py +0 -0
  20. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/openai_provider.py +0 -0
  21. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/pricing.py +0 -0
  22. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/llm/registry.py +0 -0
  23. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/__init__.py +0 -0
  24. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/client.py +0 -0
  25. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/introspect.py +0 -0
  26. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/mcp/transport.py +0 -0
  27. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/py.typed +0 -0
  28. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/__init__.py +0 -0
  29. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/builder.py +0 -0
  30. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/report/html.py +0 -0
  31. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/runner/__init__.py +0 -0
  32. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/runner/agent.py +0 -0
  33. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/__init__.py +0 -0
  34. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/deterministic.py +0 -0
  35. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/scoring/judge.py +0 -0
  36. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/tracing/__init__.py +0 -0
  37. {whetkit-0.4.0 → whetkit-0.6.0}/src/whetkit/tracing/records.py +0 -0
  38. {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.4.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; validated, review-before-trust YAML. |
167
- | `whetkit run` | Agentic eval loop with real tool execution; scored results + traces. `--plan` scores a curated view. |
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; validated, review-before-trust YAML. |
136
- | `whetkit run` | Agentic eval loop with real tool execution; scored results + traces. `--plan` scores a curated view. |
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "whetkit"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "Measure and improve how well LLM agents select and use the tools exposed by an MCP server"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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("nothing to keep — pass --keep and/or --from-tasks")
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
- task_runs = []
527
- for task in task_list:
528
- typer.echo(f"running {task.id} ...", err=True)
529
- task_runs.append(
530
- await run_task(task, servers[task.server], config, client_factory=client_factory)
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