verifiers 0.4.2.dev34__py3-none-any.whl → 0.4.2.dev36__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.
@@ -3,8 +3,8 @@ every config that owns one. `merge_defaults` (`utils/generic.py`) layers shared
3
3
  defaults under it.
4
4
 
5
5
  A run composes the blocks it needs — `[env]` (what runs, `configs/env.py`) — plus its
6
- own fields. Nothing here is a base class: the eval
7
- CLI, GEPA, and a trainer each declare their blocks and call these.
6
+ own fields. Nothing here is a base class: the run CLI and a trainer each declare
7
+ their blocks and call these.
8
8
 
9
9
  Declare the env field as `SerializeAsAny[EnvConfig] = Field(default_factory=
10
10
  single_agent_env_config)`. The `SerializeAsAny` is load-bearing: pydantic serializes
@@ -1,5 +1,5 @@
1
1
  """Which of a taskset's tasks a run uses: `Taskset.select(SelectConfig)`, under
2
- `--select.*` on the eval, debug, validate and GEPA CLIs (`SelectCLIConfig`)."""
2
+ `--select.*` on the run, debug and validate CLIs (`SelectCLIConfig`)."""
3
3
 
4
4
  import re
5
5
  import sys
@@ -17,7 +17,7 @@ class SharedTasksetConfig(BaseConfig):
17
17
  """Config passed to each task, under `--env.taskset.task.*`."""
18
18
  system_prompt: Path | None = None
19
19
  """File whose text overrides each task's `TaskData.system_prompt` on
20
- iteration (e.g. a GEPA `best_system_prompt.txt`)."""
20
+ iteration."""
21
21
 
22
22
 
23
23
  class TasksetConfig(SharedTasksetConfig):
@@ -26,6 +26,7 @@ logger = get_logger(__name__)
26
26
  KEY_VAR = "PI_INTERCEPT_KEY"
27
27
 
28
28
  PI_DIR = "/var/tmp/vf-pi"
29
+ STATE_ROOT = "/tmp/vf-pi-runs"
29
30
  PACKAGES_DIR = f"{PI_DIR}/mcp"
30
31
  PI_BIN = f"{PACKAGES_DIR}/node_modules/.bin/pi"
31
32
  ACP_VERSION = "0.0.34"
@@ -104,7 +105,7 @@ class PiHarness(ACPHarness[PiHarnessConfig]):
104
105
  data: TaskData,
105
106
  ) -> ACPConfig:
106
107
  system_prompt, prompt = self.resolve_prompt(data)
107
- agent_dir = f".vf-pi-agent-{trace.id}"
108
+ agent_dir = f"{STATE_ROOT}/{trace.id}"
108
109
  skills_dir = f"{agent_dir}/skills"
109
110
  await self.install_skills(runtime, skills_dir)
110
111
  reasoning = ctx.sampling.reasoning_effort not in (
@@ -203,7 +204,7 @@ class PiHarness(ACPHarness[PiHarnessConfig]):
203
204
  )
204
205
 
205
206
  async def cleanup(self, trace: Trace, runtime: Runtime) -> None:
206
- await remove_dir(runtime, f".vf-pi-agent-{trace.id}", "Pi state")
207
+ await remove_dir(runtime, f"{STATE_ROOT}/{trace.id}", "Pi state")
207
208
 
208
209
  async def gate_tools(
209
210
  self, config: ACPConfig, runtime: Runtime, url: str, secret: str
verifiers/v1/taskset.py CHANGED
@@ -15,8 +15,8 @@ Lazy views pick which tasks an iteration yields, chainable in any order:
15
15
 
16
16
  taskset.include(idx=["0:100"]).exclude(names=["broken"]).shuffle(seed=0).take(5)
17
17
 
18
- `select(SelectConfig)` chains them in the config's fixed order; the eval, debug,
19
- validate and GEPA entrypoints and prime-rl all select through it.
18
+ `select(SelectConfig)` chains them in the config's fixed order; the run, debug,
19
+ and validate entrypoints and prime-rl all select through it.
20
20
  """
21
21
 
22
22
  from __future__ import annotations
@@ -1,4 +1,4 @@
1
- """Graceful-shutdown signal handling shared across the v1 CLI entrypoints (run, gepa,
1
+ """Graceful-shutdown signal handling shared across the v1 CLI entrypoints (run,
2
2
  validate, debug): the first Ctrl-C/SIGTERM warns and raises KeyboardInterrupt so
3
3
  asyncio unwinds each run's teardown `finally`; further signals are swallowed so a second
4
4
  Ctrl-C can't orphan containers/sandboxes/tool-server runtimes mid-cleanup."""
@@ -197,7 +197,7 @@ def environment_class(taskset_id: str, env_id: str = "") -> type[Env]:
197
197
 
198
198
 
199
199
  def load_environment(config: EnvConfig) -> Env:
200
- """Construct the env for `config`. Every construction site (eval, serve, gepa)
200
+ """Construct the env for `config`. Every construction site (eval, serve)
201
201
  goes through here so subclass envs load everywhere."""
202
202
  return environment_class(config.taskset.id, config.id)(config)
203
203
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifiers
3
- Version: 0.4.2.dev34
3
+ Version: 0.4.2.dev36
4
4
  Summary: Verifiers: Environments for LLM Reinforcement Learning
5
5
  Project-URL: Homepage, https://github.com/primeintellect-ai/verifiers
6
6
  Project-URL: Documentation, https://github.com/primeintellect-ai/verifiers
@@ -24,7 +24,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Python: <3.15,>=3.11
25
25
  Requires-Dist: aiohttp>=3.14.1
26
26
  Requires-Dist: anthropic>=1.0.0
27
- Requires-Dist: gepa>=0.0.6
28
27
  Requires-Dist: h11>=0.16.0
29
28
  Requires-Dist: httpx2>=2.12.0
30
29
  Requires-Dist: httpx>=0.27.0
@@ -14,14 +14,13 @@ verifiers/v1/semantic.py,sha256=nqKPPUoPL5Oh8Es_XjtcbDiQiIMKTYayXEm-EPB-jAE,5129
14
14
  verifiers/v1/session.py,sha256=Vu7YPopaiTEdmDup-aXaUIFVtPAojl5XXdcOHjrcrdI,30157
15
15
  verifiers/v1/state.py,sha256=EckF2bWp-vV4b1jYJ9sLI5xrfGuI5spIgYYwW926toI,595
16
16
  verifiers/v1/task.py,sha256=zcWjBjhJrkGIL29JTnxxw6v2pbJoiek_tF5RVAVV2ZY,15919
17
- verifiers/v1/taskset.py,sha256=tkv74x1TMrZWDkcIKBo8XvpAo_ehynWJ5koT3QEK5zQ,9510
17
+ verifiers/v1/taskset.py,sha256=PRwMfPtUL0Sg9pwKzJYnpLBDqJCmQ4skQPhF1oocyyw,9504
18
18
  verifiers/v1/trace.py,sha256=o7vnIt3pXRbn5JT_gYO05YaL4NlP0k8T_UlpJK3oqnA,40595
19
19
  verifiers/v1/types.py,sha256=0yhesjtRWnhgJz2cTJWWm4EaieE7ozXwH382onCHrMA,13104
20
20
  verifiers/v1/acp/__init__.py,sha256=jY7nLAbB6uzSl-f6YCz05zBH5tarBLErNTkvV0vlaho,16613
21
21
  verifiers/v1/acp/runner.py,sha256=sFsg0UgWeDjpjihejUidIV2yTapVj1Ap0cDTaEktjkI,15484
22
22
  verifiers/v1/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  verifiers/v1/cli/debug.py,sha256=baX6LRtqBNjIbmHbdaogmz3FyyItN1Z4B-TVccuYI4k,11688
24
- verifiers/v1/cli/gepa.py,sha256=OlO8f2mOAcZiaQVLOla5ZT36oD9XRmdDWgz4WPxNYpY,4352
25
24
  verifiers/v1/cli/output.py,sha256=vCscZ-g-sa-SLWDDQNoXBWelC_Je8lBdodvZGOe__eo,3285
26
25
  verifiers/v1/cli/resolve.py,sha256=Q1EHM7wWQo0YkwJA898qtZbYLaIkjIVq3Q7kUp2YIxs,4346
27
26
  verifiers/v1/cli/resume.py,sha256=hqV5AxxqeIInJeEZQerQznuijhMNijFEFcHZIltfLdE,470
@@ -44,12 +43,12 @@ verifiers/v1/configs/retries.py,sha256=GIrwS0lnMKnlQ_Wj1Z2J6NlEYK4jX5weZxYmQmLcp
44
43
  verifiers/v1/configs/runtime.py,sha256=7To2aecnYuv_ThVt5OIlKUiocldmt4O0hZzkSwuJsBI,11773
45
44
  verifiers/v1/configs/sampling.py,sha256=bd6lietww4htrNYmUEqwK5yfKMhUT3nEjHoCg5xl_ys,1174
46
45
  verifiers/v1/configs/scoring.py,sha256=8t5NrQU9b8R9dGdcG-aQm_U1m3TgeMIbw_XUN4esoF0,3115
47
- verifiers/v1/configs/select.py,sha256=bLMJAb7i23IxU6BZMIS0Q_K5JYLlcwL8zi87REhrccc,5827
46
+ verifiers/v1/configs/select.py,sha256=a72aweHXsA1B9i-ShR710NZr4ZFC-0xfSQ5J8aQGrew,5820
48
47
  verifiers/v1/configs/task.py,sha256=3CB9pumHdxAHlxLKuLEiCfQPW37tNqy_HC6DgG2GKj4,2958
49
- verifiers/v1/configs/taskset.py,sha256=rQow8AJL34n-ZVQ_4GPjPIW5fzBL5QU_RTjJO9tlmwU,945
48
+ verifiers/v1/configs/taskset.py,sha256=OJG_DfM2_6I4Mm1-egMcrIEN6UtD2JaBaXnzW9SZXk8,906
50
49
  verifiers/v1/configs/cli/__init__.py,sha256=YMDUPdbRlDJ77dhj3QqzmyRhqNgUvuQdpj_bdBqzZPM,78
51
50
  verifiers/v1/configs/cli/debug.py,sha256=1vddnShdwYzRJp0Ubl9sp7sgnR3m2JKHkAOk741UnWU,2748
52
- verifiers/v1/configs/cli/env.py,sha256=MdN9VyGqecALu6IILYffjMXNcPcdAXvafisKq52Eajg,2584
51
+ verifiers/v1/configs/cli/env.py,sha256=ysTSo2UDZIXS08LUrch87XLHsdTQ-emXj6QWRsLs4lk,2576
53
52
  verifiers/v1/configs/cli/run.py,sha256=j6ccPxFPiWsmCAmbZcc_EtElNOMaKS9YiiSDMqudczw,3794
54
53
  verifiers/v1/configs/cli/validate.py,sha256=j6qRi2EvacXTCARnVhkFRtX9xF3IP3eVVt-Zv2ER5Vw,4073
55
54
  verifiers/v1/dialects/__init__.py,sha256=MNOYM2NdTlSxGK4VtoEjMtD5zya2MpjPDX5jMvZUWEo,736
@@ -68,12 +67,6 @@ verifiers/v1/envs/single_agent/__init__.py,sha256=r0edAc_6gtHBhPzeBGVcdHZZqSdCPZ
68
67
  verifiers/v1/envs/single_agent/env.py,sha256=iH-AnS5-5rDZM3MaYFxzaHSVqJAaw9mQJaaJvsDMMX4,854
69
68
  verifiers/v1/envs/user_sim/__init__.py,sha256=nnT695HdqjAEouYL5wtVu6MhIp2wf5yuK-EYgxOVx0s,118
70
69
  verifiers/v1/envs/user_sim/env.py,sha256=deI4r-Utn_v4E_Vb9P_X3LzFh2GRkCO4OlQt8BOCMGY,4942
71
- verifiers/v1/gepa/__init__.py,sha256=6nmdRE0-34AKioPHBjTxUg5Jo_2z7tMX-OU3zMNpAJI,197
72
- verifiers/v1/gepa/adapter.py,sha256=J35ZBiI4N--Zgt0RuBzGZwOGxeF-nnIVEBf3rIc0Pgc,6362
73
- verifiers/v1/gepa/config.py,sha256=e1qYZOKzvfN6hehFSKba4hEdbR22yQEYeSEFOCqchYI,4735
74
- verifiers/v1/gepa/dataset.py,sha256=cS4MtQVoBOnAHKs8REQm5lKGeqUpdXPU1hROU67aBG4,2130
75
- verifiers/v1/gepa/reflection.py,sha256=V1VSXuszkw5Fikwqwe5KSwunhlibudZ7F8csUXraENk,1144
76
- verifiers/v1/gepa/runner.py,sha256=CnnOv9pvj7QCwPYDZBODUwdDEJ1yMP51bgTEm2Xxtf0,5897
77
70
  verifiers/v1/harnesses/__init__.py,sha256=SBNQmzQREsKG1ZLe1hAWdFWdXh11akXaF_9JbmJImg8,2203
78
71
  verifiers/v1/harnesses/node.py,sha256=G6hnEms726QH9KJiNZCMJUfrIrb-8hjfHkzNJP8kxMo,5160
79
72
  verifiers/v1/harnesses/bash/__init__.py,sha256=IAV4eMMqDHSl-9ANb0Q3kxmBZ22SeHZFBM448X4gQio,140
@@ -107,7 +100,7 @@ verifiers/v1/harnesses/opencode/harness.py,sha256=xKVGBaQv22dRF09Bu0f3PvehgF1q0e
107
100
  verifiers/v1/harnesses/opencode/mcp-ready.js,sha256=QpAJcego5pNOZJu1dfpRLd873xitYGYwE9qwmgmvMvI,1904
108
101
  verifiers/v1/harnesses/pi/__init__.py,sha256=1mSnzOf-RdEchdeg6IHtcdobG5KIpAhuTdUIgsCIn74,117
109
102
  verifiers/v1/harnesses/pi/gate.mjs,sha256=vk_OqHXhRdYzQL7YMX8Teajm0fh9tokzDvLHVOd2I9o,449
110
- verifiers/v1/harnesses/pi/harness.py,sha256=NURYJCiE5kIB9CXIMYgvaCtFdiGpHaiWU84rOntF-3A,7753
103
+ verifiers/v1/harnesses/pi/harness.py,sha256=K2FtvDhoAUU_CdKVqE_NmDwsw3vqlSaZuLJWkAX0JQE,7784
111
104
  verifiers/v1/harnesses/prime_agent/__init__.py,sha256=xfPC4RrlnyYWaf38tPAOZq4YL6M55lMpj5BoRLmaK8I,171
112
105
  verifiers/v1/harnesses/prime_agent/harness.py,sha256=sV82MCi7uZTKlOH8Vg9_WSytgZNSbknShOhmYcdPPaE,12354
113
106
  verifiers/v1/harnesses/rlm/__init__.py,sha256=hwx51xTdEWzzYgMj-p4ETNcptoub3IPMDbEdJ4uB8Ns,122
@@ -181,8 +174,8 @@ verifiers/v1/utils/format.py,sha256=zk6p_UrzIK5Rx7_lvgVuAxjlST35I9vwVDc6GCieo1o,
181
174
  verifiers/v1/utils/generic.py,sha256=lKIzu5rvwnuSlr_kTFdupf-ElubjjvOo0vnBHYMUpC4,5036
182
175
  verifiers/v1/utils/git.py,sha256=MTRinOvG4EddfjdahHkFZUihHmhJ37n5ROtgjzjE1NA,8366
183
176
  verifiers/v1/utils/image.py,sha256=OFw_wdwVtbdwa8uJ3X3vYfhpUAi0CTH5HMBupjVsRRQ,282
184
- verifiers/v1/utils/interrupt.py,sha256=brRlP_YHOqNlxGlj7tQmRj9R4deT_qw5EfQhm711psw,1049
185
- verifiers/v1/utils/loaders.py,sha256=Oq0nzTxnrfE6mKXBjD2aj4LF3MqFpK-WWrObVQQ_SQ8,13049
177
+ verifiers/v1/utils/interrupt.py,sha256=HueVSI54ZVIKcJ0uZ62uQduUSgXEXJRL3tlzlo1FCak,1043
178
+ verifiers/v1/utils/loaders.py,sha256=lEeRQ5j6-YJ9UKshhGKBz6g5yx6dn-M1T8urXpNP5KI,13043
186
179
  verifiers/v1/utils/logging.py,sha256=F1ymwd1A8Unu85kAQ52MAowHnR7EKQIl0RDRY8u-N5I,5392
187
180
  verifiers/v1/utils/memory.py,sha256=ZkIvGk6uITAH5sKon65LifKPbvZr8mJ__PVc23FZpOQ,1835
188
181
  verifiers/v1/utils/network.py,sha256=SI6_PXHIAPows6VBpK75N6dMbPw6lWQQ3OjBQkSDGN4,1463
@@ -193,8 +186,8 @@ verifiers/v1/utils/scope.py,sha256=zbs4pj6AVk4jywlAJ3AX0Q0_kzB9dBB02nY-9H4ELdQ,8
193
186
  verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
194
187
  verifiers/v1/utils/trace_store.py,sha256=Qo9qvsJAl8PoRUavjQAfUB2VNxmUtYQtU2ejLKPzY9c,3382
195
188
  verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
196
- verifiers-0.4.2.dev34.dist-info/METADATA,sha256=Fwyk4rWxlpqu_x0GHsW8HLdS0kQwKjRuCSTRDMfZeLg,5142
197
- verifiers-0.4.2.dev34.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
198
- verifiers-0.4.2.dev34.dist-info/entry_points.txt,sha256=3M8Wr0w1cUKiB-fJ6YHB6eK5WuKYg-Y3OC4wCuUje_c,174
199
- verifiers-0.4.2.dev34.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
200
- verifiers-0.4.2.dev34.dist-info/RECORD,,
189
+ verifiers-0.4.2.dev36.dist-info/METADATA,sha256=rDjstLcPqgBJzJiro5gbMxVFQpKQKkRDdXSSdkGgqlM,5115
190
+ verifiers-0.4.2.dev36.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
191
+ verifiers-0.4.2.dev36.dist-info/entry_points.txt,sha256=qdMg7ajiFpDsHf7tuPMRclJXizu1J6LTWawS5YS0B0I,137
192
+ verifiers-0.4.2.dev36.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
193
+ verifiers-0.4.2.dev36.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  [console_scripts]
2
2
  vf-debug = verifiers.v1.cli.debug:main
3
- vf-gepa = verifiers.v1.cli.gepa:main
4
3
  vf-run = verifiers.v1.cli.run:main
5
4
  vf-validate = verifiers.v1.cli.validate:main
verifiers/v1/cli/gepa.py DELETED
@@ -1,104 +0,0 @@
1
- """The GEPA entrypoint: `uv run vf-gepa [<taskset-id>] --model <model> [options]`.
2
-
3
- Registered as the `gepa` console script. Optimizes a v1 taskset's `Task.system_prompt` via
4
- GEPA (Genetic-Pareto): alternating rollouts with a teacher LM reflecting on results — see
5
- `verifiers.v1.gepa`. CLI resolution goes through `verifiers.v1.cli.resolve`: a
6
- leading bare token is the taskset id, the `env` subconfig is narrowed from the ids so
7
- `--env.taskset.*` / `--env.<role>.*` stay typed and `-h` renders them, `@ file.toml` loads,
8
- and the actual parse is `pydantic_config.cli`.
9
- """
10
-
11
- import os
12
- import sys
13
- import uuid
14
-
15
- from pydantic_config import cli
16
-
17
- import verifiers.v1 as vf
18
- from verifiers.v1.cli.output import output_path, write_config
19
- from verifiers.v1.cli.resolve import (
20
- extract_id,
21
- narrow_config,
22
- plugin_errors,
23
- references_config_file,
24
- with_positional_taskset,
25
- )
26
- from verifiers.v1.gepa import GEPAConfig, run_gepa
27
- from verifiers.v1.utils.interrupt import install_interrupt
28
- from verifiers.v1.utils.logging import get_logger, setup_logging
29
- from verifiers.v1.utils.trace_store import TRACES_FILE
30
-
31
- logger = get_logger(__name__)
32
-
33
- USAGE = "usage: uv run vf-gepa [<taskset-id>] [--env.id <id>] --model <model> [options] [@ file.toml]"
34
-
35
-
36
- def main(argv: list[str] | None = None) -> None:
37
- # The run identity: every process this run spawns inherits it.
38
- os.environ.setdefault("VF_RUN_ID", uuid.uuid4().hex)
39
- argv = with_positional_taskset(list(sys.argv[1:]) if argv is None else list(argv))
40
-
41
- if not argv or any(arg in ("-h", "--help") for arg in argv):
42
- print(USAGE)
43
- sys.argv = [sys.argv[0], "--help"]
44
- with plugin_errors():
45
- cli(
46
- narrow_config(GEPAConfig, argv)
47
- ) # full option help, narrowed to the given ids
48
- return
49
- typed_axis = any(a.startswith("--env.") for a in argv)
50
- if (
51
- not extract_id(argv, "env.taskset")
52
- and not references_config_file(argv)
53
- and not typed_axis
54
- ):
55
- raise SystemExit(
56
- USAGE
57
- ) # need a taskset (positional / --env.taskset.id) or a @ file.toml
58
-
59
- with plugin_errors():
60
- config_type = narrow_config(GEPAConfig, argv)
61
- sys.argv = [sys.argv[0], *argv] # let prime-pydantic-config render help/errors
62
- config = cli(config_type)
63
- setup_logging("DEBUG" if config.verbose else "INFO")
64
- # Refuse multi-agent before the dry-run return, so --dry-run can't write a
65
- # config the real invocation would reject.
66
- env_cls = vf.environment_class(
67
- config.env.taskset.id,
68
- config.env.id,
69
- )
70
- if not issubclass(env_cls, vf.SingleAgentEnv):
71
- raise SystemExit(
72
- f"gepa: {config.env.env_id!r} runs {env_cls.__name__}, a multi-agent env; "
73
- "gepa optimizes one agent's prompt against per-trace rewards and can't "
74
- "drive a multi-agent interaction — only eval runs those"
75
- )
76
- # A named run directory is never silently reused: any write into it — the dry-run
77
- # config included, which would destroy the existing traces' config provenance —
78
- # would overwrite the previous run.
79
- traces_file = output_path(config) / TRACES_FILE
80
- if traces_file.exists() and traces_file.stat().st_size > 0:
81
- raise SystemExit(
82
- f"run directory {output_path(config)} already contains results - "
83
- "pick another --run.name or delete it"
84
- )
85
-
86
- if config.dry_run: # resolved + validated; write it to the output dir and exit
87
- logger.info(
88
- "wrote config to %s", write_config(config, output_path(config), "gepa.json")
89
- )
90
- return
91
-
92
- # First Ctrl-C / SIGTERM warns and raises KeyboardInterrupt so a killed/timed-out run still
93
- # runs the runner's `serving()` teardown (interception / tool-server runtimes); further
94
- # signals during that cleanup are swallowed so an impatient second Ctrl-C can't orphan them.
95
- install_interrupt()
96
-
97
- env = vf.load_environment(config.env)
98
- try:
99
- result = run_gepa(env, config)
100
- except KeyboardInterrupt:
101
- # Graceful cleanup already ran (run_gepa's finally tore down serving); exit on the
102
- # conventional Ctrl-C code without dumping a traceback.
103
- raise SystemExit(130)
104
- print(f"best system prompt:\n{result.best_candidate.get('system_prompt', '')}")
@@ -1,5 +0,0 @@
1
- from verifiers.v1.gepa.adapter import GEPAAdapter
2
- from verifiers.v1.gepa.config import GEPAConfig
3
- from verifiers.v1.gepa.runner import run_gepa
4
-
5
- __all__ = ["GEPAAdapter", "GEPAConfig", "run_gepa"]
@@ -1,128 +0,0 @@
1
- """The GEPA <-> v1 bridge: run a candidate system prompt over a batch of tasks and score it.
2
-
3
- GEPA's adapter protocol (`evaluate`, `make_reflective_dataset`) is synchronous and
4
- `gepa.api.optimize` blocks, but v1 rollouts are async. The runner manages one event loop by
5
- hand: it enters `env.serving()` on that loop and runs the blocking `optimize()` on the main
6
- thread, so each synchronous `evaluate()` drives its batch of rollouts with
7
- `loop.run_until_complete` — the one sync↔async hop. (Mirrors how v0 vf-gepa bridged; no worker
8
- thread, so a Ctrl-C unwinds straight through `optimize()` into the runner's teardown.)
9
- """
10
-
11
- import asyncio
12
- from collections.abc import Awaitable, Callable, Mapping, Sequence
13
- from dataclasses import dataclass, field
14
- from typing import Any
15
-
16
- from gepa.core.adapter import EvaluationBatch
17
- from pydantic_core import to_jsonable_python
18
-
19
- from verifiers.v1.clients import ModelContext
20
- from verifiers.v1.env import Env
21
- from verifiers.v1.episode import Episode
22
- from verifiers.v1.task import Task
23
-
24
- Candidate = dict[str, str]
25
-
26
-
27
- @dataclass
28
- class GEPAAdapter:
29
- """Bridges GEPA's optimization loop with a native v1 `Env`. `tasks` covers only the
30
- trainset + valset tasks GEPA was given (not the whole taskset), keyed by `task.data.idx` —
31
- GEPA's `batch` is a list of those idxs, and injecting the candidate rebuilds each `Task`
32
- around a `data` row carrying the new `system_prompt`. `loop` is the runner's persistent event
33
- loop (which holds `env.serving()` open); `evaluate` drives its rollouts on it with
34
- `run_until_complete`."""
35
-
36
- env: Env
37
- ctx: ModelContext
38
- tasks: dict[int, Task]
39
- loop: asyncio.AbstractEventLoop
40
- semaphore: asyncio.Semaphore | None = None
41
- on_complete: Callable[[Episode], Awaitable[None]] | None = None
42
- """Called with each rollout's episode as it finalizes — the runner's persist hook that
43
- streams episodes to `traces.jsonl`."""
44
- reflection_columns: list[str] = field(default_factory=list)
45
- propose_new_texts: Callable[..., Candidate] | None = None
46
- """Part of GEPA's adapter protocol — its proposer reads this attribute on every reflection
47
- step. None = use GEPA's default reflection-LM proposer (the AttributeError from leaving it
48
- undeclared silently disables all mutation proposals)."""
49
-
50
- def evaluate(
51
- self,
52
- batch: list[int],
53
- candidate: Candidate,
54
- capture_traces: bool = False,
55
- ) -> EvaluationBatch[Episode, Episode]:
56
- """Run `candidate`'s system prompt on the tasks named by `batch` (`Task.idx` values)
57
- and score them — one episode and one score per batch entry, so the batch stays
58
- aligned however many traces the env's episode holds. Called synchronously by GEPA on
59
- the main thread; each batch's rollouts run on the runner's persistent loop via
60
- `run_until_complete`."""
61
- system_prompt = candidate.get("system_prompt", "")
62
- episodes = self.loop.run_until_complete(self._run_batch(batch, system_prompt))
63
- scores = [_episode_score(episode) for episode in episodes]
64
- return EvaluationBatch(
65
- outputs=episodes,
66
- scores=scores,
67
- trajectories=episodes if capture_traces else None,
68
- )
69
-
70
- async def _run_batch(self, batch: list[int], system_prompt: str) -> list[Episode]:
71
- # Inject the candidate as a copy of each base task with its system_prompt overridden
72
- # (`with_system_prompt` copies rather than reconstructs, so subclass state survives and
73
- # the shared base task in `self.tasks` is left untouched for the next candidate).
74
- tasks = [self.tasks[idx].with_system_prompt(system_prompt) for idx in batch]
75
- slots = [slot for task in tasks for slot in self.env.slots(task)]
76
- results = await asyncio.gather(
77
- *(
78
- self.env.run_slot(slot, self.ctx, self.semaphore, self.on_complete)
79
- for slot in slots
80
- )
81
- )
82
- return list(results)
83
-
84
- def make_reflective_dataset(
85
- self,
86
- candidate: Candidate, # Required by GEPA's adapter protocol.
87
- eval_batch: EvaluationBatch[Episode, Episode],
88
- components_to_update: list[str],
89
- ) -> Mapping[str, Sequence[Mapping[str, Any]]]:
90
- """Build the reflective dataset the teacher LM reads to propose a new system prompt,
91
- from `eval_batch.trajectories` (episodes captured by a prior
92
- `evaluate(capture_traces=True)` on the same batch) — one record per trace, so a
93
- multi-agent episode shows the teacher every seat's turn (stamped with its role)."""
94
- episodes = eval_batch.trajectories or []
95
- records = []
96
- for episode in episodes:
97
- for trace in episode.traces:
98
- record: dict[str, Any] = {
99
- "query": trace.task.data.prompt_text,
100
- "completion": trace.last_reply,
101
- "reward": trace.reward,
102
- }
103
- record["agent"] = trace.agent.name
104
- if trace.has_error:
105
- record["error"] = str(trace.last_error)
106
- if trace.stop_condition:
107
- record["stop_condition"] = trace.stop_condition
108
- for column in self.reflection_columns:
109
- if column in trace.info:
110
- record[column] = to_jsonable_python(trace.info[column])
111
- elif hasattr(trace.task.data, column):
112
- record[column] = to_jsonable_python(
113
- getattr(trace.task.data, column)
114
- )
115
- records.append(record)
116
- return {comp: records for comp in components_to_update}
117
-
118
-
119
- def _episode_score(episode: Episode) -> float:
120
- """A candidate's score on one episode: the mean reward of the episode's scored
121
- traces. Seats that recorded no rewards (a reward-less judge) don't dilute the
122
- signal; a successful episode with no scored traces scores 0."""
123
- if any(trace.is_dropped for trace in episode.traces):
124
- raise ValueError("GEPA cannot score an episode containing dropped traces")
125
- if not episode.ok:
126
- raise ValueError(f"GEPA cannot score failed episode {episode.id!r}")
127
- scored = [trace.reward for trace in episode.traces if trace.rewards]
128
- return sum(scored) / len(scored) if scored else 0.0
@@ -1,96 +0,0 @@
1
- """The `GEPAConfig`: the single config object the `gepa` CLI parses.
2
-
3
- GEPA optimizes one taskset's `Task.system_prompt` by alternating rollouts (`evaluate`) with a
4
- teacher LM reflecting on the reflective dataset (`make_reflective_dataset`) — see
5
- `verifiers.v1.gepa.adapter.GEPAAdapter`. It owns an `env` field (the
6
- environment: its taskset, seats, limits) and adds the optimization loop's own knobs (model,
7
- reflection model, train/val split, budget). GEPA always runs in-process, since its adapter protocol is itself synchronous (see
8
- `GEPAAdapter`)."""
9
-
10
- from pathlib import Path
11
-
12
- from pydantic import AliasChoices, Field, SerializeAsAny, model_validator
13
- from pydantic_config import BaseConfig
14
-
15
- from verifiers.v1.clients import EvalClientConfig
16
- from verifiers.v1.configs.cli.env import narrowed_env_annotation, resolve_env_field
17
- from verifiers.v1.configs.cli.run import RunConfig, default_run_name
18
- from verifiers.v1.configs.env import EnvConfig
19
- from verifiers.v1.configs.sampling import SamplingConfig
20
- from verifiers.v1.configs.select import SelectCLIConfig
21
- from verifiers.v1.envs.single_agent import SingleAgentEnvConfig
22
-
23
-
24
- class GEPAConfig(BaseConfig):
25
- """The GEPA run plus its environment. `model` runs the rollouts under optimization;
26
- `reflection_model` (defaults to `model`) proposes new system prompts from the reflective
27
- dataset."""
28
-
29
- env: SerializeAsAny[EnvConfig] = SingleAgentEnvConfig()
30
- """The environment under optimization — the same `[env]` block as an eval's
31
- (`--env.taskset.*`, seats, limits), narrowed to the selected env's config class."""
32
-
33
- @model_validator(mode="before")
34
- @classmethod
35
- def _resolve_env(cls, data):
36
- return resolve_env_field(data, narrowed_env_annotation(cls))
37
-
38
- run: RunConfig = Field(default_factory=RunConfig)
39
- """Run identity: `run.name` is the display name and `run.dir` names the directory
40
- under `output_dir`; both auto-generate like an eval's when unset."""
41
- model: str = Field(
42
- "deepseek/deepseek-v4-flash", validation_alias=AliasChoices("model", "m")
43
- )
44
- """Model id for rollouts under optimization."""
45
- client: EvalClientConfig = EvalClientConfig()
46
- sampling: SamplingConfig = SamplingConfig()
47
- reflection_model: str | None = None
48
- """Teacher model that proposes new system prompts. None = reuse `model`."""
49
- reflection_client: EvalClientConfig | None = None
50
- """Endpoint for `reflection_model`. None = reuse `client`."""
51
-
52
- select: SelectCLIConfig = SelectCLIConfig()
53
- """Which of the taskset's tasks GEPA splits into train/val, under `--select.*`
54
- (`-n` sets `select.limit`, `-s` sets `select.shuffle`)."""
55
- num_train: int = Field(100, ge=1)
56
- """Tasks reserved for reflection minibatches (GEPA never scores the full trainset at once)."""
57
- num_val: int = Field(50, ge=1)
58
- """Tasks held out to score each candidate system prompt for the pareto frontier."""
59
- seed: int = 0
60
- """Seed for GEPA's optimizer (candidate selection / minibatch sampling). The train/val
61
- split comes from `select` (`-s` shuffles it under `select.seed`), so this doesn't
62
- change it."""
63
-
64
- max_total_rollouts: int = Field(500)
65
- """Total rollouts GEPA may spend across the whole optimization run."""
66
- reflection_minibatch_size: int = 3
67
- """Train tasks sampled per reflection step."""
68
- reflection_columns: list[str] = Field(default_factory=list)
69
- """Extra per-trace fields (from `trace.info`, else `task`) to surface to the teacher LM."""
70
- initial_prompt: str | None = None
71
- """Seed system prompt. None = the first loaded task's `Task.system_prompt`, if any task
72
- sets one (see `resolve_gepa_seed_prompt`)."""
73
-
74
- max_concurrent: int | None = Field(
75
- 128, validation_alias=AliasChoices("max_concurrent", "c")
76
- )
77
- """Max rollouts in flight at once, across the whole run."""
78
- output_dir: Path = Field(
79
- Path("outputs"), validation_alias=AliasChoices("output_dir", "o")
80
- )
81
- """Directory that groups related runs. The run (`configs/gepa.json` + the streamed
82
- `traces.jsonl`, alongside GEPA's own `candidates.json` / `run_log.json`) writes to
83
- `output_dir / run.dir`."""
84
- save_results: bool = True
85
- verbose: bool = Field(False, validation_alias=AliasChoices("verbose", "v"))
86
- dry_run: bool = Field(False, exclude=True)
87
- """Resolve + validate the config and dump it, then exit. Excluded from the
88
- saved config so re-running `@ configs/gepa.json` runs for real."""
89
-
90
- @model_validator(mode="after")
91
- def auto_setup_run_name(self):
92
- if self.run.name is None:
93
- self.run.name = default_run_name(self.env, self.model)
94
- if self.run.dir is None:
95
- self.run.dir = self.run.name
96
- return self
@@ -1,44 +0,0 @@
1
- """Train/val split and upfront validation for a GEPA run.
2
-
3
- v1 tasksets have no generic train/val split concept (`TasksetConfig` has no `split` field;
4
- individual tasksets define ad hoc ones inconsistently), so GEPA carves one out of the tasks
5
- `select` yields (reproducible across runs like every other entrypoint): two
6
- disjoint slices.
7
- """
8
-
9
- from verifiers.v1.task import Task
10
-
11
-
12
- def split_tasks(
13
- tasks: list[Task], num_train: int, num_val: int
14
- ) -> tuple[list[Task], list[Task]]:
15
- """The selected tasks, split into disjoint `(train, val)` slices (`train` feeds reflection
16
- minibatches; `val` scores each candidate for the pareto frontier)."""
17
- if num_train + num_val > len(tasks):
18
- raise ValueError(
19
- f"requested {num_train} train + {num_val} val tasks, but the taskset only "
20
- f"loaded {len(tasks)}"
21
- )
22
- return tasks[:num_train], tasks[num_train : num_train + num_val]
23
-
24
-
25
- def resolve_gepa_seed_prompt(tasks: list[Task], initial_prompt: str | None) -> str:
26
- """The system prompt GEPA starts optimizing from: `initial_prompt` if given, else the first
27
- task that sets `system_prompt`. Some tasksets (e.g. `gsm8k`) bake instructions into
28
- `prompt` rather than `system_prompt` and can't be optimized this way — pass `--initial-prompt`
29
- to seed one explicitly.
30
-
31
- How the resolved prompt reaches the model at rollout time — a real system message vs. folded
32
- into the user prompt — is the harness's call: `Harness.resolve_prompt` owns that policy and
33
- warns when it folds, so it isn't re-policed here."""
34
- if initial_prompt is not None:
35
- return initial_prompt
36
- for task in tasks:
37
- if task.data.system_prompt is not None:
38
- return task.data.system_prompt
39
- raise ValueError(
40
- "no task in this taskset sets Task.system_prompt — some tasksets bake instructions "
41
- "directly into `prompt` instead (e.g. gsm8k) and can't be optimized this way. Pass "
42
- "--initial-prompt to seed one explicitly, or pick a taskset whose load() sets "
43
- "system_prompt on its task data (e.g. reverse-text, textarena)."
44
- )
@@ -1,31 +0,0 @@
1
- """The reflection (teacher) LM: GEPA's proposer calls this synchronously to turn a prompt into
2
- text. Backed by a vf `Judge` — `Judge.complete` owns the model client (same key / header /
3
- Prime-billing resolution rollouts use), so we never build one here. GEPA calls it from the
4
- worker thread running `optimize()`, so each call drives the async `complete` on a private loop.
5
- """
6
-
7
- import asyncio
8
- from collections.abc import Callable
9
-
10
- from verifiers.v1.configs.judge import JudgeConfig
11
- from verifiers.v1.gepa.config import GEPAConfig
12
- from verifiers.v1.judge import Judge
13
-
14
-
15
- def build_reflection_lm(config: GEPAConfig) -> Callable[[str], str]:
16
- client = config.reflection_client or config.client
17
- judge = Judge(
18
- JudgeConfig(
19
- model=config.reflection_model or config.model,
20
- base_url=client.base_url,
21
- api_key_var=client.api_key_var,
22
- headers=client.headers,
23
- headers_from_env=client.headers_from_env,
24
- timeout=client.timeout,
25
- )
26
- )
27
-
28
- def reflection_lm(prompt: str) -> str:
29
- return asyncio.run(judge.complete(prompt)).text
30
-
31
- return reflection_lm
@@ -1,126 +0,0 @@
1
- """The GEPA runner: split tasks, drive `gepa.api.optimize` against a `GEPAAdapter`, save
2
- results. Unlike the async sibling runners, `optimize()` is synchronous and blocking, so it runs
3
- on the main thread and the runner manages one event loop by hand: `env.serving()` is entered on
4
- it once, each rollout batch runs via `loop.run_until_complete` (see `GEPAAdapter.evaluate`),
5
- and everything is torn down in `finally`. Mirrors how v0 vf-gepa bridged sync GEPA to async
6
- rollouts — a Ctrl-C raises on the main thread inside `optimize()` and unwinds through teardown."""
7
-
8
- import asyncio
9
-
10
- from gepa.api import optimize
11
- from gepa.core.result import GEPAResult
12
-
13
- from verifiers.v1.cli.output import output_path, save_config
14
- from verifiers.v1.clients import ModelContext
15
- from verifiers.v1.env import Env
16
- from verifiers.v1.episode import Episode
17
- from verifiers.v1.gepa.adapter import GEPAAdapter
18
- from verifiers.v1.gepa.config import GEPAConfig
19
- from verifiers.v1.gepa.dataset import (
20
- resolve_gepa_seed_prompt,
21
- split_tasks,
22
- )
23
- from verifiers.v1.gepa.reflection import build_reflection_lm
24
- from verifiers.v1.utils.logging import get_logger
25
- from verifiers.v1.utils.trace_store import append_episode
26
-
27
- logger = get_logger(__name__)
28
-
29
-
30
- class _GEPALog:
31
- """Forwards GEPA's optimizer log lines to v1 logging. GEPA's `LoggerProtocol` needs only
32
- `.log(str)`, so its per-iteration progress rides the same loguru setup as the rest of the
33
- v1 CLIs."""
34
-
35
- def log(self, message: str) -> None:
36
- logger.info(message)
37
-
38
-
39
- def run_gepa(env: Env, config: GEPAConfig) -> GEPAResult:
40
- logger.info("gepa config:\n%s", config.model_dump_json(indent=2))
41
- taskset = env.taskset.select(config.select)
42
- all_tasks = list(taskset.take(config.num_train + config.num_val))
43
- train_tasks, val_tasks = split_tasks(all_tasks, config.num_train, config.num_val)
44
- selected_tasks = [*train_tasks, *val_tasks]
45
- # Seed from the tasks GEPA actually evaluates (train ∪ val), not the full pre-split pool —
46
- # a taskset with per-task system prompts could otherwise seed from a task in neither split.
47
- seed_prompt = resolve_gepa_seed_prompt(selected_tasks, config.initial_prompt)
48
- tasks_by_idx = {task.data.idx: task for task in selected_tasks}
49
-
50
- run_dir = output_path(config) if config.save_results else None
51
- if run_dir is not None:
52
- save_config(
53
- config, run_dir, "gepa.json"
54
- ) # resolved config + a fresh traces.jsonl
55
- logger.info("results: %s", run_dir)
56
-
57
- # optimize() is synchronous and blocking, so it drives the run from this (main) thread. We
58
- # own one event loop: `env.serving()` (shared tool servers + interception pool, built
59
- # once) is entered on it, each rollout batch runs on it via `loop.run_until_complete`
60
- # (GEPAAdapter.evaluate), and it's all torn down in `finally`. Keeping optimize() on the
61
- # main thread means a Ctrl-C raises straight through it into this teardown.
62
- loop = asyncio.new_event_loop()
63
- semaphore = (
64
- asyncio.Semaphore(config.max_concurrent) if config.max_concurrent else None
65
- )
66
- # Stream every rollout's episode to traces.jsonl as it finalizes — the same persist hook
67
- # `env.run_slot` takes (each trace records its candidate prompt).
68
- write_lock = asyncio.Lock()
69
-
70
- async def on_complete(episode: Episode) -> None:
71
- if run_dir is not None:
72
- await append_episode(run_dir, episode, write_lock)
73
-
74
- try:
75
- # The endpoint stays config: the interception server builds the live client.
76
- ctx = ModelContext(
77
- client=config.client, model=config.model, sampling=config.sampling
78
- )
79
- reflection_lm = build_reflection_lm(config)
80
- serving = env.serving()
81
- loop.run_until_complete(serving.__aenter__())
82
- # Pair __aexit__ with a *successful* __aenter__: this inner block is reached only once
83
- # serving is up, so a startup failure propagates as-is instead of being masked by
84
- # tearing down a context that never entered.
85
- try:
86
- adapter = GEPAAdapter(
87
- env=env,
88
- ctx=ctx,
89
- tasks=tasks_by_idx,
90
- loop=loop,
91
- semaphore=semaphore,
92
- on_complete=on_complete,
93
- reflection_columns=config.reflection_columns,
94
- )
95
- optimize_kwargs: dict = {
96
- "seed_candidate": {"system_prompt": seed_prompt},
97
- "trainset": [task.data.idx for task in train_tasks],
98
- "valset": [task.data.idx for task in val_tasks],
99
- "adapter": adapter,
100
- "reflection_lm": reflection_lm,
101
- "max_metric_calls": config.max_total_rollouts,
102
- "reflection_minibatch_size": config.reflection_minibatch_size,
103
- "run_dir": str(run_dir) if run_dir is not None else None,
104
- "seed": config.seed,
105
- "display_progress_bar": False,
106
- "skip_perfect_score": False,
107
- "logger": _GEPALog(),
108
- }
109
- result = optimize(**optimize_kwargs)
110
- if run_dir is not None:
111
- # Persist the winning prompt as a plain file so it can be handed straight to
112
- # eval/train via `--env.taskset.system-prompt` (see TasksetConfig).
113
- candidate = result.best_candidate
114
- best = (
115
- candidate.get("system_prompt", "")
116
- if isinstance(candidate, dict)
117
- else str(candidate)
118
- )
119
- best_path = run_dir / "best_system_prompt.txt"
120
- best_path.write_text(best, encoding="utf-8")
121
- logger.info("best system prompt: %s", best_path)
122
- return result
123
- finally:
124
- loop.run_until_complete(serving.__aexit__(None, None, None))
125
- finally:
126
- loop.close()