whetkit 0.3.0__tar.gz → 0.4.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.3.0 → whetkit-0.4.0}/PKG-INFO +1 -1
- {whetkit-0.3.0 → whetkit-0.4.0}/pyproject.toml +1 -1
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/cli.py +224 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/curation/optimizer.py +129 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/LICENSE +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/README.md +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/curation/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/curation/export.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/curation/overlay.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/curation/plan.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/datasets/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/datasets/tasks.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/doctor.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/generate.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/anthropic_provider.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/base.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/openai_provider.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/pricing.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/llm/registry.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/mcp/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/mcp/client.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/mcp/introspect.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/mcp/transport.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/py.typed +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/report/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/report/builder.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/report/html.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/runner/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/runner/agent.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/scoring/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/scoring/aggregate.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/scoring/deterministic.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/scoring/judge.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/tracing/__init__.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.0}/src/whetkit/tracing/records.py +0 -0
- {whetkit-0.3.0 → whetkit-0.4.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.4.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
|
|
@@ -255,6 +255,62 @@ def generate(
|
|
|
255
255
|
typer.echo(f" whetkit run --server {server} --tasks {out}")
|
|
256
256
|
|
|
257
257
|
|
|
258
|
+
@app.command(name="plan-init")
|
|
259
|
+
def plan_init(
|
|
260
|
+
server: Annotated[
|
|
261
|
+
str,
|
|
262
|
+
typer.Option("--server", help="MCP server: URL, directory, server.json, or server.py"),
|
|
263
|
+
],
|
|
264
|
+
keep: Annotated[
|
|
265
|
+
str, typer.Option("--keep", help="Comma-separated tool names to keep visible")
|
|
266
|
+
] = "",
|
|
267
|
+
from_tasks: Annotated[
|
|
268
|
+
str | None,
|
|
269
|
+
typer.Option(
|
|
270
|
+
"--from-tasks",
|
|
271
|
+
help="Also keep every tool referenced by these tasks' expected_tools",
|
|
272
|
+
),
|
|
273
|
+
] = None,
|
|
274
|
+
out: Annotated[
|
|
275
|
+
str, typer.Option("--out", help="Where to write the plan YAML")
|
|
276
|
+
] = ".whetkit/curation-plan.yaml",
|
|
277
|
+
http_mode: Annotated[HttpMode, typer.Option("--http-mode")] = HttpMode.STATEFUL,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Scaffold a view plan: keep the named tools, hide everything else.
|
|
280
|
+
The fastest way to serve a lean read-only slice of a big server."""
|
|
281
|
+
from whetkit.curation import CurationPlan, ToolOverride, save_plan
|
|
282
|
+
from whetkit.datasets import load_tasks
|
|
283
|
+
|
|
284
|
+
keep_set = {name.strip() for name in keep.split(",") if name.strip()}
|
|
285
|
+
if from_tasks:
|
|
286
|
+
for task in load_tasks(from_tasks):
|
|
287
|
+
for slot in task.expected_tool_slots:
|
|
288
|
+
keep_set.update(slot)
|
|
289
|
+
if not keep_set:
|
|
290
|
+
raise typer.BadParameter("nothing to keep — pass --keep and/or --from-tasks")
|
|
291
|
+
|
|
292
|
+
spec = _resolve_server(server, http_mode)
|
|
293
|
+
inventory = asyncio.run(inspect_server(spec))
|
|
294
|
+
names = {t.name for t in inventory.tools}
|
|
295
|
+
if unknown := sorted(keep_set - names):
|
|
296
|
+
typer.echo(f"warning: not on the server, ignoring: {', '.join(unknown)}", err=True)
|
|
297
|
+
|
|
298
|
+
hidden = [
|
|
299
|
+
ToolOverride(original_name=t.name, hidden=True, reason="Not part of this view's workflows.")
|
|
300
|
+
for t in inventory.tools
|
|
301
|
+
if t.name not in keep_set
|
|
302
|
+
]
|
|
303
|
+
plan = CurationPlan(
|
|
304
|
+
server=spec.label(),
|
|
305
|
+
notes=f"View plan: keep {len(names & keep_set)} tool(s), hide the rest.",
|
|
306
|
+
overrides=hidden,
|
|
307
|
+
)
|
|
308
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
save_plan(plan, out)
|
|
310
|
+
typer.echo(f"kept {len(names & keep_set)}, hidden {len(hidden)} — wrote {out}")
|
|
311
|
+
typer.echo(f"score it: whetkit run --server {server} --tasks <tasks> --plan {out}")
|
|
312
|
+
|
|
313
|
+
|
|
258
314
|
@app.command()
|
|
259
315
|
def export(
|
|
260
316
|
plan: Annotated[
|
|
@@ -617,6 +673,17 @@ def curate(
|
|
|
617
673
|
help="Completion-token budget per model turn (raise for reasoning models)",
|
|
618
674
|
),
|
|
619
675
|
] = 1024,
|
|
676
|
+
prune_unused: Annotated[
|
|
677
|
+
bool,
|
|
678
|
+
typer.Option(
|
|
679
|
+
"--prune-unused",
|
|
680
|
+
help=(
|
|
681
|
+
"Additionally hide every tool the eval never touched — the cost "
|
|
682
|
+
"play for big servers. Only sound when the tasks cover all the "
|
|
683
|
+
"workflows the curated view will serve."
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
] = False,
|
|
620
687
|
store: Annotated[str | None, typer.Option("--store")] = None,
|
|
621
688
|
http_mode: Annotated[HttpMode, typer.Option("--http-mode")] = HttpMode.STATEFUL,
|
|
622
689
|
) -> None:
|
|
@@ -624,6 +691,7 @@ def curate(
|
|
|
624
691
|
it, and show the before/after hit-rate."""
|
|
625
692
|
from whetkit.curation import CuratedMCPClient, propose_plan, save_plan
|
|
626
693
|
from whetkit.curation.optimizer import OptimizerConfig
|
|
694
|
+
from whetkit.curation.optimizer import prune_unused as apply_prune_unused
|
|
627
695
|
from whetkit.datasets import load_tasks
|
|
628
696
|
from whetkit.mcp import inspect_server
|
|
629
697
|
from whetkit.runner import RunConfig, run_task
|
|
@@ -666,6 +734,9 @@ def curate(
|
|
|
666
734
|
)
|
|
667
735
|
for warning in warnings:
|
|
668
736
|
typer.echo(f"warning: {warning}", err=True)
|
|
737
|
+
if prune_unused:
|
|
738
|
+
pruned = apply_prune_unused(plan, inventory, task_list, baseline_runs)
|
|
739
|
+
typer.echo(f"--prune-unused: hid {pruned} untouched tool(s)", err=True)
|
|
669
740
|
save_plan(plan, plan_path)
|
|
670
741
|
typer.echo(f"curation plan written to {plan_path}", err=True)
|
|
671
742
|
|
|
@@ -742,6 +813,159 @@ def curate(
|
|
|
742
813
|
asyncio.run(_curate())
|
|
743
814
|
|
|
744
815
|
|
|
816
|
+
@app.command()
|
|
817
|
+
def fix(
|
|
818
|
+
tasks: Annotated[
|
|
819
|
+
str, typer.Option("--tasks", help="Task YAML file or directory of task files")
|
|
820
|
+
],
|
|
821
|
+
server: Annotated[
|
|
822
|
+
str | None, typer.Option("--server", help="Override the server every task runs against")
|
|
823
|
+
] = None,
|
|
824
|
+
model: Annotated[
|
|
825
|
+
str, typer.Option("--model", help="Agent model as provider:model_id")
|
|
826
|
+
] = "anthropic:claude-sonnet-5",
|
|
827
|
+
optimizer_model: Annotated[
|
|
828
|
+
str, typer.Option("--optimizer-model")
|
|
829
|
+
] = "anthropic:claude-sonnet-5",
|
|
830
|
+
judge: Annotated[str, typer.Option("--judge", help="'auto', 'on', or 'off'")] = "auto",
|
|
831
|
+
judge_model: Annotated[str, typer.Option("--judge-model")] = "anthropic:claude-sonnet-5",
|
|
832
|
+
max_iterations: Annotated[
|
|
833
|
+
int, typer.Option("--max-iterations", help="Propose→eval→revise rounds (≥1)")
|
|
834
|
+
] = 3,
|
|
835
|
+
plan_path: Annotated[
|
|
836
|
+
str, typer.Option("--plan", help="Where to write the best plan YAML")
|
|
837
|
+
] = ".whetkit/curation-plan.yaml",
|
|
838
|
+
match_mode: Annotated[str, typer.Option("--match-mode")] = "order_tolerant",
|
|
839
|
+
max_turns: Annotated[int, typer.Option("--max-turns")] = 10,
|
|
840
|
+
max_tokens: Annotated[int, typer.Option("--max-tokens")] = 1024,
|
|
841
|
+
store: Annotated[str | None, typer.Option("--store")] = None,
|
|
842
|
+
http_mode: Annotated[HttpMode, typer.Option("--http-mode")] = HttpMode.STATEFUL,
|
|
843
|
+
) -> None:
|
|
844
|
+
"""Self-correcting curation: propose a plan, eval through it, feed the
|
|
845
|
+
regressions and remaining waste back to the optimizer, revise — up to
|
|
846
|
+
--max-iterations — and keep the best plan by measured results."""
|
|
847
|
+
from functools import partial
|
|
848
|
+
|
|
849
|
+
from whetkit.curation import CuratedMCPClient, propose_plan, save_plan
|
|
850
|
+
from whetkit.curation.optimizer import OptimizerConfig, propose_revision
|
|
851
|
+
from whetkit.datasets import load_tasks
|
|
852
|
+
from whetkit.runner import RunConfig, run_task
|
|
853
|
+
from whetkit.scoring import JudgeCache, JudgeConfig, MatchMode, score_runs
|
|
854
|
+
from whetkit.tracing import TraceStore, default_store_path
|
|
855
|
+
|
|
856
|
+
if max_iterations < 1:
|
|
857
|
+
raise typer.BadParameter("--max-iterations must be at least 1")
|
|
858
|
+
task_list = load_tasks(tasks)
|
|
859
|
+
servers = _resolve_task_servers(task_list, server, http_mode)
|
|
860
|
+
config = RunConfig(model=model, max_turns=max_turns, max_tokens=max_tokens)
|
|
861
|
+
use_judge = _judge_enabled(judge, judge_model)
|
|
862
|
+
judge_config = JudgeConfig(model=judge_model)
|
|
863
|
+
mode = MatchMode(match_mode)
|
|
864
|
+
store_path = store or str(default_store_path())
|
|
865
|
+
optimizer_config = OptimizerConfig(model=optimizer_model)
|
|
866
|
+
|
|
867
|
+
from whetkit.mcp import MCPClient
|
|
868
|
+
|
|
869
|
+
async def _eval(group: str, cache, client_factory=MCPClient):
|
|
870
|
+
runs = []
|
|
871
|
+
for task in task_list:
|
|
872
|
+
typer.echo(f"running {task.id} ...", err=True)
|
|
873
|
+
runs.append(
|
|
874
|
+
await run_task(task, servers[task.server], config, client_factory=client_factory)
|
|
875
|
+
)
|
|
876
|
+
with TraceStore(store_path) as trace_store:
|
|
877
|
+
trace_store.save_runs(runs, run_group=group)
|
|
878
|
+
return runs
|
|
879
|
+
|
|
880
|
+
async def _fix() -> None:
|
|
881
|
+
from whetkit.mcp import inspect_server as _inspect
|
|
882
|
+
|
|
883
|
+
cache = JudgeCache(default_store_path().parent / "judge_cache.sqlite3")
|
|
884
|
+
try:
|
|
885
|
+
typer.echo("== baseline ==", err=True)
|
|
886
|
+
baseline_runs = await _eval("baseline", cache)
|
|
887
|
+
baseline = await score_runs(
|
|
888
|
+
task_list,
|
|
889
|
+
baseline_runs,
|
|
890
|
+
mode=mode,
|
|
891
|
+
judge_config=judge_config,
|
|
892
|
+
judge_cache=cache,
|
|
893
|
+
use_judge=use_judge,
|
|
894
|
+
)
|
|
895
|
+
inventory = await _inspect(next(iter(servers.values())))
|
|
896
|
+
|
|
897
|
+
typer.echo("== proposing plan (iteration 1) ==", err=True)
|
|
898
|
+
plan, warnings = await propose_plan(
|
|
899
|
+
inventory, task_list, baseline_runs, baseline.scores, optimizer_config
|
|
900
|
+
)
|
|
901
|
+
for w in warnings:
|
|
902
|
+
typer.echo(f"warning: {w}", err=True)
|
|
903
|
+
|
|
904
|
+
def metric(summary):
|
|
905
|
+
return (summary.hit_rate, -summary.avg_extra_calls, summary.avg_precision)
|
|
906
|
+
|
|
907
|
+
best_plan, best_summary = None, None
|
|
908
|
+
for iteration in range(1, max_iterations + 1):
|
|
909
|
+
typer.echo(f"== eval through plan (iteration {iteration}) ==", err=True)
|
|
910
|
+
curated_runs = await _eval(
|
|
911
|
+
f"fix-{iteration}",
|
|
912
|
+
cache,
|
|
913
|
+
client_factory=partial(CuratedMCPClient, plan=plan),
|
|
914
|
+
)
|
|
915
|
+
curated = await score_runs(
|
|
916
|
+
task_list,
|
|
917
|
+
curated_runs,
|
|
918
|
+
mode=mode,
|
|
919
|
+
judge_config=judge_config,
|
|
920
|
+
judge_cache=cache,
|
|
921
|
+
use_judge=use_judge,
|
|
922
|
+
name_map=plan.rename_map(),
|
|
923
|
+
)
|
|
924
|
+
typer.echo(
|
|
925
|
+
f"iteration {iteration}: hit {curated.hit_rate:.0%} "
|
|
926
|
+
f"(baseline {baseline.hit_rate:.0%}), "
|
|
927
|
+
f"extra calls {curated.avg_extra_calls:.1f}/task",
|
|
928
|
+
err=True,
|
|
929
|
+
)
|
|
930
|
+
if best_summary is None or metric(curated) > metric(best_summary):
|
|
931
|
+
best_plan, best_summary = plan, curated
|
|
932
|
+
|
|
933
|
+
converged = (
|
|
934
|
+
curated.hit_rate >= baseline.hit_rate and curated.avg_extra_calls <= 0.25
|
|
935
|
+
)
|
|
936
|
+
if converged or iteration == max_iterations:
|
|
937
|
+
if converged:
|
|
938
|
+
typer.echo("converged — no regressions, negligible waste", err=True)
|
|
939
|
+
break
|
|
940
|
+
|
|
941
|
+
typer.echo("== revising plan ==", err=True)
|
|
942
|
+
plan, warnings = await propose_revision(
|
|
943
|
+
plan,
|
|
944
|
+
inventory,
|
|
945
|
+
task_list,
|
|
946
|
+
baseline_runs,
|
|
947
|
+
baseline.scores,
|
|
948
|
+
curated_runs,
|
|
949
|
+
curated.scores,
|
|
950
|
+
optimizer_config,
|
|
951
|
+
)
|
|
952
|
+
for w in warnings:
|
|
953
|
+
typer.echo(f"warning: {w}", err=True)
|
|
954
|
+
finally:
|
|
955
|
+
cache.close()
|
|
956
|
+
|
|
957
|
+
save_plan(best_plan, plan_path)
|
|
958
|
+
typer.echo(f"\nbest plan (of {max_iterations} max iterations) -> {plan_path}")
|
|
959
|
+
typer.echo(
|
|
960
|
+
f"Hit-rate: {baseline.hit_rate:.0%} -> {best_summary.hit_rate:.0%} "
|
|
961
|
+
f"Extra calls: {baseline.avg_extra_calls:.1f} -> "
|
|
962
|
+
f"{best_summary.avg_extra_calls:.1f}/task"
|
|
963
|
+
)
|
|
964
|
+
typer.echo(f"serve it: whetkit overlay --server <origin> --plan {plan_path}")
|
|
965
|
+
|
|
966
|
+
asyncio.run(_fix())
|
|
967
|
+
|
|
968
|
+
|
|
745
969
|
@app.command()
|
|
746
970
|
def report(
|
|
747
971
|
tasks: Annotated[
|
|
@@ -197,3 +197,132 @@ async def propose_plan(
|
|
|
197
197
|
plan.overrides = []
|
|
198
198
|
break
|
|
199
199
|
return plan, warnings
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def prune_unused(
|
|
203
|
+
plan: CurationPlan,
|
|
204
|
+
inventory: ServerInventory,
|
|
205
|
+
tasks: list[TaskSpec],
|
|
206
|
+
runs: list[TaskRun],
|
|
207
|
+
) -> int:
|
|
208
|
+
"""Hide every tool the eval never touched: not called in any baseline
|
|
209
|
+
run, not accepted by any task's expected_tools, and not already
|
|
210
|
+
overridden. The cost play for big servers — only sound when the task
|
|
211
|
+
set really covers the workflows the curated view will serve, which is
|
|
212
|
+
why it's opt-in. Returns how many tools were hidden."""
|
|
213
|
+
used = {name for run in runs for name in run.called_tool_names}
|
|
214
|
+
for task in tasks:
|
|
215
|
+
for slot in task.expected_tool_slots:
|
|
216
|
+
used.update(slot)
|
|
217
|
+
overridden = {o.original_name for o in plan.overrides}
|
|
218
|
+
added = 0
|
|
219
|
+
for tool in inventory.tools:
|
|
220
|
+
if tool.name in used or tool.name in overridden:
|
|
221
|
+
continue
|
|
222
|
+
plan.overrides.append(
|
|
223
|
+
ToolOverride(
|
|
224
|
+
original_name=tool.name,
|
|
225
|
+
hidden=True,
|
|
226
|
+
reason="Unused by the eval workflows (pruned by --prune-unused).",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
added += 1
|
|
230
|
+
return added
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
REVISION_INSTRUCTIONS = """\
|
|
234
|
+
You previously proposed the curation plan below, and it was re-evaluated.
|
|
235
|
+
Revise it: KEEP the changes that worked, FIX or DROP the ones implicated in
|
|
236
|
+
regressions or remaining waste. A rename the agent still misused needs a
|
|
237
|
+
better name or a sharper description; a prune that broke a task must be
|
|
238
|
+
un-hidden. Respond with the SAME JSON shape as before — the FULL revised
|
|
239
|
+
plan, not a delta.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_revision_prompt(
|
|
244
|
+
plan: CurationPlan,
|
|
245
|
+
inventory: ServerInventory,
|
|
246
|
+
tasks: list[TaskSpec],
|
|
247
|
+
baseline_runs: list[TaskRun],
|
|
248
|
+
baseline_scores: list[TaskScore],
|
|
249
|
+
curated_runs: list[TaskRun],
|
|
250
|
+
curated_scores: list[TaskScore],
|
|
251
|
+
) -> str:
|
|
252
|
+
import yaml
|
|
253
|
+
|
|
254
|
+
baseline_by_id = {s.task_id: s for s in baseline_scores}
|
|
255
|
+
regressed = [
|
|
256
|
+
s.task_id
|
|
257
|
+
for s in curated_scores
|
|
258
|
+
if not s.hit and (b := baseline_by_id.get(s.task_id)) is not None and b.hit
|
|
259
|
+
]
|
|
260
|
+
regression_line = (
|
|
261
|
+
f"REGRESSIONS your plan caused (fix these first): {', '.join(regressed)}"
|
|
262
|
+
if regressed
|
|
263
|
+
else "No regressions — focus on remaining waste (unnecessary calls)."
|
|
264
|
+
)
|
|
265
|
+
plan_yaml = yaml.safe_dump(plan.model_dump(exclude_defaults=True), sort_keys=False)
|
|
266
|
+
return (
|
|
267
|
+
f"## Tools exposed by the server\n{_inventory_block(inventory)}\n\n"
|
|
268
|
+
f"## Baseline eval (no plan)\n{_trace_block(tasks, baseline_runs, baseline_scores)}\n\n"
|
|
269
|
+
f"## Your previous plan\n```yaml\n{plan_yaml}```\n\n"
|
|
270
|
+
f"## Eval THROUGH your plan\n{_trace_block(tasks, curated_runs, curated_scores)}\n\n"
|
|
271
|
+
f"{regression_line}\n\nRevise the plan now."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def propose_revision(
|
|
276
|
+
plan: CurationPlan,
|
|
277
|
+
inventory: ServerInventory,
|
|
278
|
+
tasks: list[TaskSpec],
|
|
279
|
+
baseline_runs: list[TaskRun],
|
|
280
|
+
baseline_scores: list[TaskScore],
|
|
281
|
+
curated_runs: list[TaskRun],
|
|
282
|
+
curated_scores: list[TaskScore],
|
|
283
|
+
config: OptimizerConfig | None = None,
|
|
284
|
+
provider: LLMProvider | None = None,
|
|
285
|
+
) -> tuple[CurationPlan, list[str]]:
|
|
286
|
+
"""One revision round: same parsing/validation as propose_plan, but the
|
|
287
|
+
model sees its previous plan and what happened through it."""
|
|
288
|
+
config = config or OptimizerConfig()
|
|
289
|
+
provider_name, model_id = parse_model(config.model)
|
|
290
|
+
provider = provider or get_provider(provider_name)
|
|
291
|
+
|
|
292
|
+
prompt = build_revision_prompt(
|
|
293
|
+
plan, inventory, tasks, baseline_runs, baseline_scores, curated_runs, curated_scores
|
|
294
|
+
)
|
|
295
|
+
parsed = None
|
|
296
|
+
for _attempt in range(2):
|
|
297
|
+
turn = await provider.complete(
|
|
298
|
+
model=model_id,
|
|
299
|
+
system=OPTIMIZER_SYSTEM_PROMPT + "\n" + REVISION_INSTRUCTIONS,
|
|
300
|
+
messages=[ChatMessage(role="user", content=prompt)],
|
|
301
|
+
tools=[],
|
|
302
|
+
max_tokens=config.max_tokens,
|
|
303
|
+
)
|
|
304
|
+
parsed = _parse_overrides(turn.text or "")
|
|
305
|
+
if parsed is not None:
|
|
306
|
+
break
|
|
307
|
+
if parsed is None:
|
|
308
|
+
return plan, ["revision output was not valid JSON after 2 attempts; keeping previous plan"]
|
|
309
|
+
|
|
310
|
+
notes, overrides = parsed
|
|
311
|
+
revised = CurationPlan(server=inventory.server, notes=notes, overrides=overrides)
|
|
312
|
+
origin_names = {t.name for t in inventory.tools}
|
|
313
|
+
warnings: list[str] = []
|
|
314
|
+
while problems := revised.validate_against(origin_names):
|
|
315
|
+
problem = problems[0]
|
|
316
|
+
name_match = re.search(r"'([^']+)'", problem)
|
|
317
|
+
offender = name_match.group(1) if name_match else None
|
|
318
|
+
before = len(revised.overrides)
|
|
319
|
+
revised.overrides = [
|
|
320
|
+
o
|
|
321
|
+
for o in revised.overrides
|
|
322
|
+
if offender not in (o.original_name, o.new_name, o.presented_name)
|
|
323
|
+
]
|
|
324
|
+
warnings.append(f"dropped unsafe override(s): {problem}")
|
|
325
|
+
if len(revised.overrides) == before:
|
|
326
|
+
warnings.append("could not repair revised plan; keeping previous plan")
|
|
327
|
+
return plan, warnings
|
|
328
|
+
return revised, warnings
|
|
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
|
|
File without changes
|
|
File without changes
|