verifiers 0.2.1.dev6__py3-none-any.whl → 0.2.1.dev7__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.
verifiers/v1/cli/debug.py CHANGED
@@ -3,9 +3,7 @@
3
3
  import asyncio
4
4
  import contextlib
5
5
  import logging
6
- import random
7
6
  import shlex
8
- import signal
9
7
  import sys
10
8
  import time
11
9
  import traceback
@@ -30,7 +28,9 @@ from verifiers.v1.runtimes import ProgramResult, Runtime, make_runtime
30
28
  from verifiers.v1.state import state_cls
31
29
  from verifiers.v1.task import Task
32
30
  from verifiers.v1.trace import Error, Trace, TraceTask
31
+ from verifiers.v1.utils.interrupt import install_interrupt
33
32
  from verifiers.v1.utils.logging import setup_logging
33
+ from verifiers.v1.utils.sampling import sample
34
34
 
35
35
  logger = logging.getLogger(__name__)
36
36
 
@@ -267,11 +267,7 @@ async def debug_task(task: Task, config: DebugConfig) -> tuple[Trace, bool]:
267
267
 
268
268
  async def run_debug(config: DebugConfig) -> list[Trace]:
269
269
  taskset = vf.load_taskset(config.taskset)
270
- tasks = taskset.load()
271
- if config.shuffle:
272
- random.Random(0).shuffle(tasks)
273
- if config.num_tasks is not None:
274
- tasks = tasks[: config.num_tasks]
270
+ tasks = sample(taskset.load(), config.shuffle, config.num_tasks)
275
271
  if isinstance(config.runtime, vf.SubprocessConfig) and any(
276
272
  type(t).NEEDS_CONTAINER or t.data.image for t in tasks
277
273
  ):
@@ -327,8 +323,9 @@ def main(argv: list[str] | None = None) -> None:
327
323
  sys.argv = [sys.argv[0], *argv]
328
324
  config = cli(config_type)
329
325
  setup_logging("DEBUG" if config.verbose else "INFO")
330
- # Translate SIGTERM so async finally blocks still tear down their resources.
331
- signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
326
+ # Graceful shutdown: first Ctrl-C/SIGTERM unwinds each task's teardown `finally`;
327
+ # a second is swallowed so it can't orphan containers/sandboxes mid-cleanup.
328
+ install_interrupt()
332
329
  asyncio.run(run_debug(config))
333
330
 
334
331
 
@@ -7,7 +7,7 @@ import sys
7
7
  from pydantic_config import cli
8
8
 
9
9
  import verifiers.v1 as vf
10
- from verifiers.v1.utils.interrupt import install as install_interrupt
10
+ from verifiers.v1.utils.interrupt import install_interrupt
11
11
  from verifiers.v1.utils.logging import setup_logging
12
12
  from verifiers.v1.cli.output import output_path, write_config
13
13
  from verifiers.v1.cli.resolve import (
@@ -3,7 +3,6 @@
3
3
  import asyncio
4
4
  import contextlib
5
5
  import logging
6
- import random
7
6
  import time
8
7
 
9
8
  from verifiers.v1.clients import ModelContext, resolve_client
@@ -14,21 +13,15 @@ from verifiers.v1.cli.output import append_trace, output_path, save_config
14
13
  from verifiers.v1.decorators import discover_decorated
15
14
  from verifiers.v1.env import Environment
16
15
  from verifiers.v1.trace import Trace
16
+ from verifiers.v1.utils.sampling import sample
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
- _SHUFFLE_SEED = (
21
- 0 # fixed so `--shuffle` samples the same tasks every run (reproducible)
22
- )
23
-
24
20
 
25
21
  async def run_eval(env: Environment, config: EvalConfig) -> list[Trace]:
26
22
  logger.info("eval config:\n%s", config.model_dump_json(indent=2))
27
23
  client = resolve_client(config.client)
28
- tasks = env.taskset.load()
29
- if config.shuffle:
30
- random.Random(_SHUFFLE_SEED).shuffle(tasks)
31
- tasks = tasks if config.num_tasks is None else tasks[: config.num_tasks]
24
+ tasks = sample(env.taskset.load(), config.shuffle, config.num_tasks)
32
25
  ctx = ModelContext(client=client, model=config.model, sampling=config.sampling)
33
26
  # One episode of `num_rollouts` rollouts per task; the shared semaphore bounds total
34
27
  # concurrent rollouts (across episodes), so group rewards still see their whole episode.
@@ -169,11 +162,7 @@ async def run_eval_server(config: EvalConfig) -> list[Trace]:
169
162
  await client.wait_for_server_startup(timeout=600)
170
163
  info = await client.info()
171
164
  group_scored = info.requires_group_scoring
172
- idxs = list(range(info.num_tasks))
173
- if config.shuffle:
174
- random.Random(_SHUFFLE_SEED).shuffle(idxs)
175
- if config.num_tasks is not None:
176
- idxs = idxs[: config.num_tasks]
165
+ idxs = sample(list(range(info.num_tasks)), config.shuffle, config.num_tasks)
177
166
  out = output_path(config)
178
167
  finished: list[Trace] = []
179
168
  if config.resume is not None:
@@ -0,0 +1,74 @@
1
+ """The GEPA entrypoint: `uv run 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 mirrors `eval`/`serve` (`verifiers.v1.cli.resolve`): a
6
+ leading bare token is the taskset id, the taskset/harness subconfigs are narrowed from their
7
+ `id`s so `--taskset.*` / `--harness.*` stay typed and `-h` renders them, `@ file.toml` loads,
8
+ and the actual parse is `pydantic_config.cli`. v1-native tasksets only — a legacy (v0) env is
9
+ rejected; run those through the existing `vf-gepa` command instead.
10
+ """
11
+
12
+ import logging
13
+ import sys
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
+ references_config_file,
23
+ with_positional_taskset,
24
+ )
25
+ from verifiers.v1.gepa import GEPAConfig, run_gepa
26
+ from verifiers.v1.utils.interrupt import install_interrupt
27
+ from verifiers.v1.utils.logging import setup_logging
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ USAGE = "usage: uv run gepa [<taskset-id>] [--harness.id <id>] --model <model> [options] [@ file.toml]"
32
+
33
+
34
+ def main(argv: list[str] | None = None) -> None:
35
+ argv = with_positional_taskset(list(sys.argv[1:]) if argv is None else list(argv))
36
+
37
+ if not argv or any(arg in ("-h", "--help") for arg in argv):
38
+ print(USAGE)
39
+ sys.argv = [sys.argv[0], "--help"]
40
+ cli(
41
+ narrow_config(GEPAConfig, argv)
42
+ ) # full option help, narrowed to the given ids
43
+ return
44
+ if not extract_id(argv, "taskset") and not references_config_file(argv):
45
+ raise SystemExit(
46
+ USAGE
47
+ ) # need a taskset (positional / --taskset.id) or a @ file.toml
48
+
49
+ config_type = narrow_config(GEPAConfig, argv)
50
+ sys.argv = [sys.argv[0], *argv] # let prime-pydantic-config render help/errors
51
+ config = cli(config_type)
52
+ if config.is_legacy:
53
+ raise SystemExit(
54
+ "gepa optimizes native v1 tasksets; run a legacy (v0) environment through "
55
+ "`vf-gepa` instead of `gepa`."
56
+ )
57
+ setup_logging("DEBUG" if config.verbose else "INFO")
58
+ if config.dry_run: # resolved + validated; write it to the output dir and exit
59
+ logger.info("wrote config to %s", write_config(config, output_path(config)))
60
+ return
61
+
62
+ # First Ctrl-C / SIGTERM warns and raises KeyboardInterrupt so a killed/timed-out run still
63
+ # runs the runner's `serving()` teardown (interception pool / tool-server runtimes); further
64
+ # signals during that cleanup are swallowed so an impatient second Ctrl-C can't orphan them.
65
+ install_interrupt()
66
+
67
+ env = vf.Environment(config)
68
+ try:
69
+ result = run_gepa(env, config)
70
+ except KeyboardInterrupt:
71
+ # Graceful cleanup already ran (run_gepa's finally tore down serving); exit on the
72
+ # conventional Ctrl-C code without dumping a traceback.
73
+ raise SystemExit(130)
74
+ print(f"best system prompt:\n{result.best_candidate.get('system_prompt', '')}")
@@ -10,7 +10,6 @@ config is the base for replay-specific overrides.
10
10
  import asyncio
11
11
  import contextlib
12
12
  import logging
13
- import signal
14
13
  import sys
15
14
  import time
16
15
  import tomllib
@@ -31,6 +30,7 @@ from verifiers.v1.configs.replay import ReplayConfig
31
30
  from verifiers.v1.state import state_cls
32
31
  from verifiers.v1.task import Task, WireTaskData, task_data_cls
33
32
  from verifiers.v1.trace import Trace
33
+ from verifiers.v1.utils.interrupt import install_interrupt
34
34
  from verifiers.v1.utils.logging import setup_logging
35
35
 
36
36
  logger = logging.getLogger(__name__)
@@ -220,8 +220,9 @@ def main(argv: list[str] | None = None) -> None:
220
220
  logging.lastResort = None
221
221
  else:
222
222
  setup_logging(level, log_file=log_file, console=True)
223
- # Translate SIGTERM so async finally blocks still tear down their resources.
224
- signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
223
+ # Graceful shutdown: first Ctrl-C/SIGTERM unwinds the scoring teardown `finally`;
224
+ # a second is swallowed so it can't orphan resources mid-cleanup.
225
+ install_interrupt()
225
226
  asyncio.run(run_replay(config, source, out))
226
227
 
227
228
 
@@ -3,8 +3,6 @@
3
3
  import asyncio
4
4
  import contextlib
5
5
  import logging
6
- import random
7
- import signal
8
6
  import sys
9
7
  import time
10
8
  from typing import Any
@@ -26,7 +24,9 @@ from verifiers.v1.runtimes import make_runtime
26
24
  from verifiers.v1.state import state_cls
27
25
  from verifiers.v1.task import Task
28
26
  from verifiers.v1.trace import Trace, TraceTask
27
+ from verifiers.v1.utils.interrupt import install_interrupt
29
28
  from verifiers.v1.utils.logging import setup_logging
29
+ from verifiers.v1.utils.sampling import sample
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -198,11 +198,7 @@ async def _validate_task(task: Task, config: ValidateConfig) -> ResultRow:
198
198
 
199
199
  async def run_validate(config: ValidateConfig) -> list[dict]:
200
200
  taskset = vf.load_taskset(config.taskset)
201
- tasks = taskset.load()
202
- if config.shuffle:
203
- random.Random(0).shuffle(tasks)
204
- if config.num_tasks is not None:
205
- tasks = tasks[: config.num_tasks]
201
+ tasks = sample(taskset.load(), config.shuffle, config.num_tasks)
206
202
  if isinstance(config.runtime, vf.SubprocessConfig) and any(
207
203
  type(t).NEEDS_CONTAINER or t.data.image for t in tasks
208
204
  ):
@@ -273,9 +269,9 @@ def main(argv: list[str] | None = None) -> None:
273
269
  setup_logging("DEBUG" if config.verbose else "INFO", console=not config.rich)
274
270
  if config.rich:
275
271
  logging.lastResort = None # drop stdlib records that bypass loguru
276
- # Make SIGTERM behave like Ctrl-C so a killed run still runs each task's `finally`
277
- # (tears down containers/sandboxes) and the atexit backstop catches the rest.
278
- signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
272
+ # Graceful shutdown: first Ctrl-C/SIGTERM unwinds each task's teardown `finally`
273
+ # (containers/sandboxes); a second is swallowed so it can't orphan them mid-cleanup.
274
+ install_interrupt()
279
275
  asyncio.run(run_validate(config))
280
276
 
281
277
 
@@ -0,0 +1,12 @@
1
+ """GEPA (Genetic-Pareto) system-prompt optimization for native v1 environments.
2
+
3
+ `run_gepa(env, config)` drives the third-party `gepa` optimizer against a v1 `Environment`
4
+ via `GEPAAdapter`, seeding from and improving `Task.system_prompt`. The `gepa` console
5
+ script (`verifiers.v1.cli.gepa`) is a thin entrypoint over this package.
6
+ """
7
+
8
+ from verifiers.v1.gepa.adapter import GEPAAdapter
9
+ from verifiers.v1.gepa.config import GEPAConfig
10
+ from verifiers.v1.gepa.runner import run_gepa
11
+
12
+ __all__ = ["GEPAAdapter", "GEPAConfig", "run_gepa"]
@@ -0,0 +1,112 @@
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
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Callable, Mapping, Sequence
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 Environment
21
+ from verifiers.v1.task import Task
22
+ from verifiers.v1.trace import Trace
23
+
24
+ Candidate = dict[str, str]
25
+
26
+
27
+ @dataclass
28
+ class GEPAAdapter:
29
+ """Bridges GEPA's optimization loop with a native v1 `Environment`. `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: Environment
37
+ ctx: ModelContext
38
+ tasks: dict[int, Task]
39
+ loop: asyncio.AbstractEventLoop
40
+ semaphore: asyncio.Semaphore | None = None
41
+ on_complete: Callable[[Trace], Awaitable[None]] | None = None
42
+ """Called with each rollout's trace as it finalizes — the runner's persist hook that
43
+ streams traces to `traces.jsonl`, exactly as `run_eval` does."""
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[Trace, Trace]:
56
+ """Run `candidate`'s system prompt on the tasks named by `batch` (`Task.idx` values)
57
+ and score them. Called synchronously by GEPA on the main thread; each batch's rollouts
58
+ run on the runner's persistent loop via `run_until_complete`."""
59
+ system_prompt = candidate.get("system_prompt", "")
60
+ traces = self.loop.run_until_complete(self._run_batch(batch, system_prompt))
61
+ scores = [trace.reward for trace in traces]
62
+ return EvaluationBatch(
63
+ outputs=traces,
64
+ scores=scores,
65
+ trajectories=traces if capture_traces else None,
66
+ )
67
+
68
+ async def _run_batch(self, batch: list[int], system_prompt: str) -> list[Trace]:
69
+ # Inject the candidate by rebuilding each Task around a data row with the new
70
+ # system_prompt (TaskData is frozen; behavior/config carry over unchanged).
71
+ tasks = [
72
+ type(t)(
73
+ t.data.model_copy(update={"system_prompt": system_prompt}), t.config
74
+ )
75
+ for t in (self.tasks[idx] for idx in batch)
76
+ ]
77
+ episodes = [self.env.episode(task, self.ctx, n=1) for task in tasks]
78
+ results = await asyncio.gather(
79
+ *(episode.run(self.semaphore, self.on_complete) for episode in episodes)
80
+ )
81
+ return [trace for episode_traces in results for trace in episode_traces]
82
+
83
+ def make_reflective_dataset(
84
+ self,
85
+ candidate: Candidate, # noqa: ARG002 - required by GEPA's adapter protocol
86
+ eval_batch: EvaluationBatch[Trace, Trace],
87
+ components_to_update: list[str],
88
+ ) -> Mapping[str, Sequence[Mapping[str, Any]]]:
89
+ """Build the reflective dataset the teacher LM reads to propose a new system prompt,
90
+ from `eval_batch.trajectories` (traces captured by a prior `evaluate(capture_traces=True)`
91
+ on the same batch)."""
92
+ traces = eval_batch.trajectories or []
93
+ records = []
94
+ for trace, score in zip(traces, eval_batch.scores):
95
+ record: dict[str, Any] = {
96
+ "query": trace.task.data.prompt_text,
97
+ "completion": trace.last_reply,
98
+ "reward": score,
99
+ }
100
+ if trace.has_error:
101
+ record["error"] = str(trace.error)
102
+ if trace.stop_condition:
103
+ record["stop_condition"] = trace.stop_condition
104
+ for column in self.reflection_columns:
105
+ if column in trace.info:
106
+ record[column] = to_jsonable_python(trace.info[column])
107
+ elif hasattr(trace.task.data, column):
108
+ record[column] = to_jsonable_python(
109
+ getattr(trace.task.data, column)
110
+ )
111
+ records.append(record)
112
+ return {comp: records for comp in components_to_update}
@@ -0,0 +1,78 @@
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`. This inherits `EnvConfig`'s fields (`taskset`,
6
+ `harness`, `max_turns`, token limits, timeouts) as top-level flags, the same way `EvalConfig`
7
+ does, and adds the optimization loop's own knobs (model, reflection model, train/val split,
8
+ budget). There is no worker pool here (`EnvServerConfig` is not a base) — GEPA always runs
9
+ in-process, since its adapter protocol is itself synchronous (see `GEPAAdapter`)."""
10
+
11
+ from pathlib import Path
12
+ from uuid import uuid4
13
+
14
+ from pydantic import AliasChoices, Field
15
+
16
+ from verifiers.v1.clients import EvalClientConfig
17
+ from verifiers.v1.env import EnvConfig
18
+ from verifiers.v1.types import SamplingConfig
19
+
20
+
21
+ class GEPAConfig(EnvConfig):
22
+ """The GEPA run plus its environment. `model` runs the rollouts under optimization;
23
+ `reflection_model` (defaults to `model`) proposes new system prompts from the reflective
24
+ dataset. No default for `model` — a GEPA run can spend a large eval budget, so pick it
25
+ explicitly rather than silently optimizing (and spending) against a fallback."""
26
+
27
+ uuid: str = Field(default_factory=lambda: str(uuid4()), exclude=True)
28
+ """Auto-generated run id — the leaf of the output dir, so runs never overwrite.
29
+ Excluded from the saved config so re-running `@ config.toml` lands in a fresh dir."""
30
+ model: str = Field(..., validation_alias=AliasChoices("model", "m"))
31
+ """Model id for rollouts under optimization."""
32
+ client: EvalClientConfig = EvalClientConfig()
33
+ sampling: SamplingConfig = SamplingConfig()
34
+ reflection_model: str | None = None
35
+ """Teacher model that proposes new system prompts. None = reuse `model`."""
36
+ reflection_client: EvalClientConfig | None = None
37
+ """Endpoint for `reflection_model`. None = reuse `client`."""
38
+
39
+ num_train: int = Field(100, ge=1)
40
+ """Tasks reserved for reflection minibatches (GEPA never scores the full trainset at once)."""
41
+ num_val: int = Field(50, ge=1)
42
+ """Tasks held out to score each candidate system prompt for the pareto frontier."""
43
+ shuffle: bool = Field(True, validation_alias=AliasChoices("shuffle", "s"))
44
+ """Shuffle tasks before splitting into train/val — v1 tasksets have no generic train/val
45
+ split, so GEPA carves one out of `taskset.load()` the way `run_eval` samples (fixed
46
+ seed, so the split is reproducible across runs)."""
47
+ seed: int = 0
48
+ """Seed for GEPA's optimizer (candidate selection / minibatch sampling). Task shuffling
49
+ uses a fixed seed, matching eval — so this doesn't change the train/val split."""
50
+
51
+ max_metric_calls: int = Field(
52
+ 500, validation_alias=AliasChoices("max_metric_calls", "B")
53
+ )
54
+ """Total rollouts GEPA may spend across the whole optimization run."""
55
+ reflection_minibatch_size: int = 3
56
+ """Train tasks sampled per reflection step."""
57
+ perfect_score: float | None = None
58
+ """Skip reflecting on a minibatch that already scores this well."""
59
+ reflection_columns: list[str] = Field(default_factory=list)
60
+ """Extra per-trace fields (from `trace.info`, else `task`) to surface to the teacher LM."""
61
+ initial_prompt: str | None = None
62
+ """Seed system prompt. None = the first loaded task's `Task.system_prompt`, if any task
63
+ sets one (see `resolve_gepa_seed_prompt`)."""
64
+
65
+ max_concurrent: int | None = Field(
66
+ 128, validation_alias=AliasChoices("max_concurrent", "c")
67
+ )
68
+ """Max rollouts in flight at once, across the whole run."""
69
+ output_dir: Path | None = Field(
70
+ None, validation_alias=AliasChoices("output_dir", "o")
71
+ )
72
+ """Where to write results (config.toml + the streamed traces.jsonl, alongside GEPA's own
73
+ candidates.json / run_log.json). None = a fresh per-run dir under
74
+ `outputs/<taskset>--<model>--<harness>/<uuid>` (via `output_path`)."""
75
+ save_results: bool = True
76
+ verbose: bool = Field(False, validation_alias=AliasChoices("verbose", "v"))
77
+ dry_run: bool = False
78
+ """Resolve + validate the config and dump it, then exit."""
@@ -0,0 +1,63 @@
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
5
+ `taskset.load()` itself: the shared fixed-seed shuffle (`verifiers.v1.utils.sampling.sample`,
6
+ reproducible across runs like every other entrypoint), then two disjoint slices.
7
+ """
8
+
9
+ from verifiers.v1.decorators import discover_decorated
10
+ from verifiers.v1.task import Task
11
+ from verifiers.v1.utils.sampling import sample
12
+
13
+
14
+ def reject_group_reward_tasksets(tasks: list[Task]) -> None:
15
+ """GEPA scores one rollout per task (`n=1`) to get the per-task scalar its pareto frontier
16
+ needs, but `@group_reward`s compare a task's rollouts and need >=2 — so a group-reward
17
+ taskset can't be optimized this way (`Environment.episode` would raise on the first batch).
18
+ Reject it up front with a clear message instead of crashing mid-run. A taskset loads one task
19
+ type, so the first task is representative (as `run_eval` also assumes)."""
20
+ if tasks and discover_decorated(tasks[0], "group_reward"):
21
+ raise ValueError(
22
+ "this taskset defines @group_reward(s), which compare >=2 rollouts of a task; GEPA "
23
+ "optimizes a single system prompt from per-task scalar scores (one rollout per task) "
24
+ "and can't score group rewards. Optimize a taskset without @group_reward instead."
25
+ )
26
+
27
+
28
+ def split_tasks(
29
+ tasks: list[Task], num_train: int, num_val: int, shuffle: bool
30
+ ) -> tuple[list[Task], list[Task]]:
31
+ """The taskset's tasks, split into disjoint `(train, val)` slices (`train` feeds reflection
32
+ minibatches; `val` scores each candidate for the pareto frontier)."""
33
+ pool = sample(
34
+ tasks, shuffle
35
+ ) # shared fixed-seed shuffle; GEPA does its own slicing below
36
+ if num_train + num_val > len(pool):
37
+ raise ValueError(
38
+ f"requested {num_train} train + {num_val} val tasks, but the taskset only "
39
+ f"loaded {len(pool)}"
40
+ )
41
+ return pool[:num_train], pool[num_train : num_train + num_val]
42
+
43
+
44
+ def resolve_gepa_seed_prompt(tasks: list[Task], initial_prompt: str | None) -> str:
45
+ """The system prompt GEPA starts optimizing from: `initial_prompt` if given, else the first
46
+ task that sets `system_prompt`. Some tasksets (e.g. `gsm8k-v1`) bake instructions into
47
+ `prompt` rather than `system_prompt` and can't be optimized this way — pass `--initial-prompt`
48
+ to seed one explicitly.
49
+
50
+ How the resolved prompt reaches the model at rollout time — a real system message vs. folded
51
+ into the user prompt — is the harness's call: `Harness.resolve_prompt` owns that policy and
52
+ warns when it folds, so it isn't re-policed here."""
53
+ if initial_prompt is not None:
54
+ return initial_prompt
55
+ for task in tasks:
56
+ if task.data.system_prompt is not None:
57
+ return task.data.system_prompt
58
+ raise ValueError(
59
+ "no task in this taskset sets Task.system_prompt — some tasksets bake instructions "
60
+ "directly into `prompt` instead (e.g. gsm8k-v1) and can't be optimized this way. Pass "
61
+ "--initial-prompt to seed one explicitly, or pick a taskset whose load() sets "
62
+ "system_prompt on its task data (e.g. reverse-text-v1, lean, textarena)."
63
+ )
@@ -0,0 +1,28 @@
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 typing import Callable
9
+
10
+ from verifiers.v1.gepa.config import GEPAConfig
11
+ from verifiers.v1.judge import Judge, JudgeConfig
12
+
13
+
14
+ def build_reflection_lm(config: GEPAConfig) -> Callable[[str], str]:
15
+ client = config.reflection_client or config.client
16
+ judge = Judge(
17
+ JudgeConfig(
18
+ model=config.reflection_model or config.model,
19
+ base_url=client.base_url,
20
+ api_key_var=client.api_key_var,
21
+ headers=client.headers,
22
+ )
23
+ )
24
+
25
+ def reflection_lm(prompt: str) -> str:
26
+ return asyncio.run(judge.complete(prompt)).text
27
+
28
+ return reflection_lm
@@ -0,0 +1,122 @@
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
+ import logging
10
+
11
+ from gepa.api import optimize
12
+ from gepa.core.result import GEPAResult
13
+
14
+ from verifiers.v1.cli.output import append_trace, output_path, save_config
15
+ from verifiers.v1.clients import ModelContext, resolve_client
16
+ from verifiers.v1.env import Environment
17
+ from verifiers.v1.gepa.adapter import GEPAAdapter
18
+ from verifiers.v1.gepa.config import GEPAConfig
19
+ from verifiers.v1.gepa.dataset import (
20
+ reject_group_reward_tasksets,
21
+ resolve_gepa_seed_prompt,
22
+ split_tasks,
23
+ )
24
+ from verifiers.v1.gepa.reflection import build_reflection_lm
25
+ from verifiers.v1.trace import Trace
26
+
27
+ logger = logging.getLogger(__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 instead of a bespoke dashboard."""
34
+
35
+ def log(self, message: str) -> None:
36
+ logger.info(message)
37
+
38
+
39
+ def run_gepa(env: Environment, config: GEPAConfig) -> GEPAResult:
40
+ logger.info("gepa config:\n%s", config.model_dump_json(indent=2))
41
+ all_tasks = env.taskset.load()
42
+ train_tasks, val_tasks = split_tasks(
43
+ all_tasks, config.num_train, config.num_val, config.shuffle
44
+ )
45
+ selected_tasks = [*train_tasks, *val_tasks]
46
+ reject_group_reward_tasksets(
47
+ selected_tasks
48
+ ) # GEPA scores n=1 per task; groups need >=2
49
+ # Seed from the tasks GEPA actually evaluates (train ∪ val), not the full pre-split pool —
50
+ # a taskset with per-task system prompts could otherwise seed from a task in neither split.
51
+ seed_prompt = resolve_gepa_seed_prompt(selected_tasks, config.initial_prompt)
52
+ tasks_by_idx = {task.data.idx: task for task in selected_tasks}
53
+
54
+ run_dir = output_path(config) if config.save_results else None
55
+ if run_dir is not None:
56
+ save_config(
57
+ config, run_dir
58
+ ) # config.toml + a fresh traces.jsonl (like run_eval)
59
+ logger.info("results: %s", run_dir)
60
+
61
+ # optimize() is synchronous and blocking, so it drives the run from this (main) thread. We
62
+ # own one event loop: `env.serving()` (shared tool servers + interception pool, built once
63
+ # like run_eval) is entered on it, each rollout batch runs on it via `loop.run_until_complete`
64
+ # (GEPAAdapter.evaluate), and it's all torn down in `finally`. Keeping optimize() on the
65
+ # main thread means a Ctrl-C raises straight through it into this teardown.
66
+ loop = asyncio.new_event_loop()
67
+ semaphore = (
68
+ asyncio.Semaphore(config.max_concurrent) if config.max_concurrent else None
69
+ )
70
+ # Stream every rollout's trace to traces.jsonl as it finalizes — the same persist hook
71
+ # run_eval passes to Episode.run (each trace records its candidate prompt).
72
+ write_lock = asyncio.Lock()
73
+
74
+ async def on_complete(trace: Trace) -> None:
75
+ if run_dir is not None:
76
+ await append_trace(run_dir, trace, write_lock)
77
+
78
+ # The client opens an httpx pool at construction, so build it inside the try that closes it —
79
+ # a failure while building ctx/reflection_lm must not leak the pool.
80
+ client = None
81
+ try:
82
+ client = resolve_client(config.client)
83
+ ctx = ModelContext(client=client, model=config.model, sampling=config.sampling)
84
+ reflection_lm = build_reflection_lm(config)
85
+ serving = env.serving()
86
+ loop.run_until_complete(serving.__aenter__())
87
+ # Pair __aexit__ with a *successful* __aenter__: this inner block is reached only once
88
+ # serving is up, so a startup failure propagates as-is instead of being masked by
89
+ # tearing down a context that never entered.
90
+ try:
91
+ adapter = GEPAAdapter(
92
+ env=env,
93
+ ctx=ctx,
94
+ tasks=tasks_by_idx,
95
+ loop=loop,
96
+ semaphore=semaphore,
97
+ on_complete=on_complete,
98
+ reflection_columns=config.reflection_columns,
99
+ )
100
+ optimize_kwargs: dict = dict(
101
+ seed_candidate={"system_prompt": seed_prompt},
102
+ trainset=[task.data.idx for task in train_tasks],
103
+ valset=[task.data.idx for task in val_tasks],
104
+ adapter=adapter,
105
+ reflection_lm=reflection_lm,
106
+ max_metric_calls=config.max_metric_calls,
107
+ reflection_minibatch_size=config.reflection_minibatch_size,
108
+ run_dir=str(run_dir) if run_dir is not None else None,
109
+ seed=config.seed,
110
+ display_progress_bar=False,
111
+ skip_perfect_score=config.perfect_score is not None,
112
+ logger=_GEPALog(),
113
+ )
114
+ if config.perfect_score is not None:
115
+ optimize_kwargs["perfect_score"] = config.perfect_score
116
+ return optimize(**optimize_kwargs)
117
+ finally:
118
+ loop.run_until_complete(serving.__aexit__(None, None, None))
119
+ finally:
120
+ if client is not None:
121
+ loop.run_until_complete(client.close())
122
+ loop.close()
verifiers/v1/legacy.py CHANGED
@@ -455,12 +455,12 @@ def _legacy_output_dir(config) -> Path:
455
455
  async def run_legacy_eval(config) -> list[Trace]:
456
456
  """Run a legacy environment in process and return v1 traces."""
457
457
  import asyncio
458
- import random
459
458
 
460
459
  from verifiers import load_environment
461
460
 
462
461
  from verifiers.v1.cli.output import append_trace, save_config
463
462
  from verifiers.v1.utils.install import ensure_installed
463
+ from verifiers.v1.utils.sampling import sample
464
464
 
465
465
  # Install from the env hub on demand for an `org/name[@version]` id (a local id is
466
466
  # already importable), then load by module name.
@@ -468,11 +468,7 @@ async def run_legacy_eval(config) -> list[Trace]:
468
468
  if config.extra_env_kwargs: # post-load knobs (max_total_completion_tokens, …)
469
469
  env.set_kwargs(**config.extra_env_kwargs)
470
470
  dataset = env.get_eval_dataset() # the eval split (falls back to train when unset)
471
- idxs = list(range(len(dataset)))
472
- if config.shuffle:
473
- random.Random(0).shuffle(idxs) # fixed seed: same sample every run
474
- if config.num_tasks is not None:
475
- idxs = idxs[: config.num_tasks]
471
+ idxs = sample(list(range(len(dataset))), config.shuffle, config.num_tasks)
476
472
 
477
473
  client = _eval_client(config.client, config.model)
478
474
  sampling_args = config.sampling.model_dump(exclude_none=True)
@@ -1,6 +1,7 @@
1
- """Graceful-shutdown signal handling for the eval CLI: the first Ctrl-C/SIGTERM warns and
2
- raises KeyboardInterrupt so asyncio unwinds each rollout's teardown `finally`; further signals
3
- are swallowed so a second Ctrl-C can't orphan containers/sandboxes mid-cleanup."""
1
+ """Graceful-shutdown signal handling shared across the v1 CLI entrypoints (eval, gepa,
2
+ validate, debug, replay): the first Ctrl-C/SIGTERM warns and raises KeyboardInterrupt so
3
+ asyncio unwinds each run's teardown `finally`; further signals are swallowed so a second
4
+ Ctrl-C can't orphan containers/sandboxes/tool-server runtimes mid-cleanup."""
4
5
 
5
6
  import logging
6
7
  import signal
@@ -15,8 +16,9 @@ def cleaning_up() -> bool:
15
16
  return _cleaning_up
16
17
 
17
18
 
18
- def install() -> None:
19
- """Route SIGINT/SIGTERM through the graceful-shutdown handler."""
19
+ def install_interrupt() -> None:
20
+ """Route SIGINT/SIGTERM through the graceful-shutdown handler. Call once from a CLI
21
+ entrypoint before running its async main."""
20
22
 
21
23
  def handle(*_) -> None:
22
24
  global _cleaning_up
@@ -0,0 +1,26 @@
1
+ """Shared task sampling: an optional fixed-seed shuffle, then an optional head-slice.
2
+
3
+ Every taskset entrypoint — eval, server eval, debug, validate, legacy eval, and GEPA — narrows
4
+ the loaded tasks (or their indices) the same way: with `--shuffle`, a shuffle under a fixed seed
5
+ so the sampled subset is the *same* every run (reproducible), then an optional slice to the
6
+ first `limit`. GEPA layers its disjoint train/val split on top of this shuffle (see
7
+ `verifiers.v1.gepa.dataset`).
8
+ """
9
+
10
+ import random
11
+ from typing import TypeVar
12
+
13
+ _SHUFFLE_SEED = (
14
+ 0 # fixed so `--shuffle` samples the same items every run (reproducible)
15
+ )
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ def sample(items: list[T], shuffle: bool, limit: int | None = None) -> list[T]:
21
+ """`items` optionally shuffled under the fixed seed, then optionally sliced to the first
22
+ `limit`. Returns a new list; the input is left untouched."""
23
+ items = list(items)
24
+ if shuffle:
25
+ random.Random(_SHUFFLE_SEED).shuffle(items)
26
+ return items if limit is None else items[:limit]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verifiers
3
- Version: 0.2.1.dev6
3
+ Version: 0.2.1.dev7
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
@@ -205,7 +205,7 @@ verifiers/v1/errors.py,sha256=GbFh_figoWzej2A1BEEB_quoaj1Xtt-rRIkb7KET1sA,6019
205
205
  verifiers/v1/graph.py,sha256=EVTjXUTrGIdcnekXdDpYYW7u5aXGLBPYUc0BeDjSmtA,25517
206
206
  verifiers/v1/harness.py,sha256=WochG6egKqhyf5qs_xWfUOPbVGWcE1mju9qZScxNmW0,5965
207
207
  verifiers/v1/judge.py,sha256=z7vLODlEwi5lk-TI92U46QQEyrl6clwQlg3R2uJiBFs,11094
208
- verifiers/v1/legacy.py,sha256=9WBVER4alu3Jy4lnhTZ9qht06JZCkB8O2g-3Kom9AsM,20560
208
+ verifiers/v1/legacy.py,sha256=oQRMJOcc-vFSqJLUssUvyivLp1K-4FHEIwv4in7rI1Q,20459
209
209
  verifiers/v1/loaders.py,sha256=UBiwHl76cjgw41xsz07bc9hKg1cnT1c4m05G-R1ipt4,5044
210
210
  verifiers/v1/push.py,sha256=-jZdFjPTLuyBD5H_YeUYz5KFpbFHKzIhnW3BtiY_f8o,8545
211
211
  verifiers/v1/retries.py,sha256=oZYN0S5XQjvMHLxB_06iMNAzBMjlKotTwuQtgIGUgfw,5748
@@ -217,22 +217,23 @@ verifiers/v1/taskset.py,sha256=bOTITbhFqQS6XybjKdQE5nHuVlLOGObnwmJLRbFOh1A,2907
217
217
  verifiers/v1/trace.py,sha256=TZVV4J_nM-M5OeYvThW8RGrzaKdB3mTqk6QXw7vUL8U,15634
218
218
  verifiers/v1/types.py,sha256=rLeCLwzuN1sUzMSF2F_OqBOzS3Vd36aT7QbDf4xFfHQ,8264
219
219
  verifiers/v1/cli/__init__.py,sha256=x0ov9198kpwPlVd8IRbcbVtTYGAG5exo-IYocqa7gxw,23
220
- verifiers/v1/cli/debug.py,sha256=bpqXakBiKcJs8HlbupQjybLGHHj6gMoJRKEmHI3-B0M,10963
220
+ verifiers/v1/cli/debug.py,sha256=duifS0H6CXfLsKJla1c-IwFr3zCErYZxfzy_eORLajQ,10962
221
+ verifiers/v1/cli/gepa.py,sha256=jXzCVe4v10ogDOShN0lEXx5Dy1tu-ciiF_HXv4a3Byw,3183
221
222
  verifiers/v1/cli/init.py,sha256=foejcU68vsXHpVmpir3n-N3uKLX-CdsL-yI-9p9T4cg,9770
222
223
  verifiers/v1/cli/output.py,sha256=L0MaFqbGpFl_lOxNG7H43xKHF21zlwy4NoMb5aWBY3I,4026
223
- verifiers/v1/cli/replay.py,sha256=AN-Q4YRCSP_CE-b-2dDIFQNHY3osKDNXabqeY1K0lVc,9758
224
+ verifiers/v1/cli/replay.py,sha256=H11cUNRKh9w41DygV8dqBm24fHpFFfyPxPKOnKsT_U0,9813
224
225
  verifiers/v1/cli/resolve.py,sha256=7xPR4enanS574I0GIcv30Hg0xX8OmC9GzVq93Mb9LS8,3441
225
226
  verifiers/v1/cli/serve.py,sha256=7T_pQdMihmGj7CdsEyVqlro_fBgdJVRkb3ak2qCkZgc,2240
226
- verifiers/v1/cli/validate.py,sha256=CjjNYXeS8z8Ow0eKcdiKCNVDiMCbOv-tH1mo2mutgpM,9513
227
+ verifiers/v1/cli/validate.py,sha256=56CEagb7fvJQSj7qil-1wpH8ocCiPMalIJaF6PeTVUU,9427
227
228
  verifiers/v1/cli/dashboard/__init__.py,sha256=nWhXVYuAw-eF4TZbhtSoKWRQytv67WzIEnGISQnRHqY,227
228
229
  verifiers/v1/cli/dashboard/base.py,sha256=kUP93zJSIVLptSbnWX6MOy8kGg63fpeAzPqakCpPyDc,3547
229
230
  verifiers/v1/cli/dashboard/eval.py,sha256=hYRC_2HVlia87yR1MOGjMwzet1rVpeQzxZaKmo3wjRg,26166
230
231
  verifiers/v1/cli/dashboard/replay.py,sha256=4SmCFx7UbDbdRKszrPkOKSZQ303ezc9SqJLbAbClL6s,3119
231
232
  verifiers/v1/cli/dashboard/validate.py,sha256=1EyP2wkm0KMwl1Xf32my-ulmHwHHWzlHNgtbf-3ZQXs,3355
232
233
  verifiers/v1/cli/eval/__init__.py,sha256=64KcYxE9GDt_TB1CNuRI5uOx3A5npitYRKFuCHbV4DM,22
233
- verifiers/v1/cli/eval/main.py,sha256=48dGLSq8CZ48pjCiMRd8p1gRFcYXVwVinDVdYdThzfU,4931
234
+ verifiers/v1/cli/eval/main.py,sha256=uch0w30MChcdofaprw48vGcJ2LXB-ms2Blb4bkXaQ6c,4920
234
235
  verifiers/v1/cli/eval/resume.py,sha256=CWKtKYmU6vtHD_rd4dfV4YV7ccgRNs7jBtA0zeOfHzA,5077
235
- verifiers/v1/cli/eval/runner.py,sha256=iSnocgsuZALpVCX0TewlHhisGFgnRoaIxePET70c7nM,10769
236
+ verifiers/v1/cli/eval/runner.py,sha256=i7mzCOIF4mrKdWm6t5HqW7IB9-4cExKnt2h8bNX4208,10468
236
237
  verifiers/v1/clients/__init__.py,sha256=EgDgl5RYcjQ5W5ROjleL91eJoVfI36SsV9pSLrWVMVI,595
237
238
  verifiers/v1/clients/client.py,sha256=Y5Bvi13SNKC7jz4H8as9F98tzc7HY-VGxyo96_Y_FWQ,3247
238
239
  verifiers/v1/clients/config.py,sha256=QMR0PkpFLcLi_Zw6bVimigNjNiWeJ-4YvJ_JsSS9yGw,5228
@@ -250,6 +251,12 @@ verifiers/v1/dialects/anthropic.py,sha256=_WxY3EcZAx_HO-tMFpBz4ylcU9El3CPylRfLnb
250
251
  verifiers/v1/dialects/base.py,sha256=SIir2dCzlhhmjgqZv-fuw6aAgOQ8-OwOecEEvLc8UV4,8680
251
252
  verifiers/v1/dialects/chat.py,sha256=JJK0MH2_xygH8jYS6MCqPZ0-yKqLgOXMGDZCYD9F4P4,13326
252
253
  verifiers/v1/dialects/responses.py,sha256=d6FFXpAjrjxlaUwYVkywNC5iQZPCZp-vlTrABowC0ko,14592
254
+ verifiers/v1/gepa/__init__.py,sha256=rZsikZtKKDKm7eU8hnn8fJi7FHScsTQJXNtFw1f-pD4,534
255
+ verifiers/v1/gepa/adapter.py,sha256=-OyQwbsF6eNoql5TJ_qGs32zcTuUA1w4zNDvY-UfE5s,5336
256
+ verifiers/v1/gepa/config.py,sha256=llwaOkWBBLKcCqDbeGXPJA1PWGtJZi3_47_FXXRolNk,4255
257
+ verifiers/v1/gepa/dataset.py,sha256=YnoRxaDGKpyOwPQsvCOV1PlS9MjtaTBc6qFXlpaH14U,3355
258
+ verifiers/v1/gepa/reflection.py,sha256=3wiMaXphu40i3nt38UhwNP06dMihqoVvaaBDO-52IR4,1007
259
+ verifiers/v1/gepa/runner.py,sha256=xc9afhn2kHtqXb8p4-7EXROnNNYt8tm8Nfr7qpRZFcQ,5638
253
260
  verifiers/v1/harnesses/__init__.py,sha256=NF3TBQ9WYlTkODwYTEbqeM7zbwbRCW6onEfdQgL_Vqg,937
254
261
  verifiers/v1/harnesses/codex/__init__.py,sha256=ocyBvlpSO8c3pT6D1ebrQohoXYESQcCyPT5kN3j1-N4,132
255
262
  verifiers/v1/harnesses/codex/harness.py,sha256=08T08uU-q9NSS2KWFQm5jaKaQ2Y-3NS3NfbKXqpRIVg,4525
@@ -307,11 +314,12 @@ verifiers/v1/utils/aio.py,sha256=ZKTHeURNbWhTpHMyYuqMLsdPWVHyn5anfG1IJs5y-Zg,148
307
314
  verifiers/v1/utils/format.py,sha256=NfQo9M5KMzWCZpQaet5YtsJx56vCKAZPfbjZZJLJhhM,2187
308
315
  verifiers/v1/utils/generic.py,sha256=cpiyt_GIhFcHVK3dh7eBDqQJ6tQFXSQyHUuPKRLZfj4,647
309
316
  verifiers/v1/utils/install.py,sha256=fWNsyKrw_PyhC0Qhqv5Ri0adFm5Ddx4AVy_hbsra1GU,1390
310
- verifiers/v1/utils/interrupt.py,sha256=OTb6zQ9lpS3xQV5j4VDNiSB1N20fllaDbY8glI8Akx4,1027
317
+ verifiers/v1/utils/interrupt.py,sha256=F-KKhc5ndPJJfhd3SuMqyqhXhA32FhCRy5KWFJpEoM4,1179
311
318
  verifiers/v1/utils/logging.py,sha256=OcMHA6NsYux3oIzjPuI95rDWmFBHNcHDjZeNIhXTX-Y,2084
312
319
  verifiers/v1/utils/memory.py,sha256=ZkIvGk6uITAH5sKon65LifKPbvZr8mJ__PVc23FZpOQ,1835
313
- verifiers-0.2.1.dev6.dist-info/METADATA,sha256=Ws2IpM5z_18GDHH6kmpnrN-HXxyAoME-RhpHA1ywnZs,5135
314
- verifiers-0.2.1.dev6.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
315
- verifiers-0.2.1.dev6.dist-info/entry_points.txt,sha256=vYJT3h4I0-pEnXGaizK-6J56h-qrh-HvMtBTiATK06w,517
316
- verifiers-0.2.1.dev6.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
317
- verifiers-0.2.1.dev6.dist-info/RECORD,,
320
+ verifiers/v1/utils/sampling.py,sha256=pzCf5HSYl41KFfX90hVNREwRkGncmcJYQ2ndRfVekI0,1037
321
+ verifiers-0.2.1.dev7.dist-info/METADATA,sha256=HmlQhEtw1yRqv4_kNx1HrAtdkuBWYGPXETUU8g1Ydvk,5135
322
+ verifiers-0.2.1.dev7.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
323
+ verifiers-0.2.1.dev7.dist-info/entry_points.txt,sha256=dF82JUYEFslR1AOW8LZfuBWeZeQoyX4wCRrduklg8-I,551
324
+ verifiers-0.2.1.dev7.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
325
+ verifiers-0.2.1.dev7.dist-info/RECORD,,
@@ -1,6 +1,7 @@
1
1
  [console_scripts]
2
2
  debug = verifiers.v1.cli.debug:main
3
3
  eval = verifiers.v1.cli.eval.main:main
4
+ gepa = verifiers.v1.cli.gepa:main
4
5
  init = verifiers.v1.cli.init:main
5
6
  replay = verifiers.v1.cli.replay:main
6
7
  serve = verifiers.v1.cli.serve:main