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 +6 -9
- verifiers/v1/cli/eval/main.py +1 -1
- verifiers/v1/cli/eval/runner.py +3 -14
- verifiers/v1/cli/gepa.py +74 -0
- verifiers/v1/cli/replay.py +4 -3
- verifiers/v1/cli/validate.py +6 -10
- verifiers/v1/gepa/__init__.py +12 -0
- verifiers/v1/gepa/adapter.py +112 -0
- verifiers/v1/gepa/config.py +78 -0
- verifiers/v1/gepa/dataset.py +63 -0
- verifiers/v1/gepa/reflection.py +28 -0
- verifiers/v1/gepa/runner.py +122 -0
- verifiers/v1/legacy.py +2 -6
- verifiers/v1/utils/interrupt.py +7 -5
- verifiers/v1/utils/sampling.py +26 -0
- {verifiers-0.2.1.dev6.dist-info → verifiers-0.2.1.dev7.dist-info}/METADATA +1 -1
- {verifiers-0.2.1.dev6.dist-info → verifiers-0.2.1.dev7.dist-info}/RECORD +20 -12
- {verifiers-0.2.1.dev6.dist-info → verifiers-0.2.1.dev7.dist-info}/entry_points.txt +1 -0
- {verifiers-0.2.1.dev6.dist-info → verifiers-0.2.1.dev7.dist-info}/WHEEL +0 -0
- {verifiers-0.2.1.dev6.dist-info → verifiers-0.2.1.dev7.dist-info}/licenses/LICENSE +0 -0
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
|
-
#
|
|
331
|
-
|
|
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
|
|
verifiers/v1/cli/eval/main.py
CHANGED
|
@@ -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
|
|
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 (
|
verifiers/v1/cli/eval/runner.py
CHANGED
|
@@ -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:
|
verifiers/v1/cli/gepa.py
ADDED
|
@@ -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', '')}")
|
verifiers/v1/cli/replay.py
CHANGED
|
@@ -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
|
-
#
|
|
224
|
-
|
|
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
|
|
verifiers/v1/cli/validate.py
CHANGED
|
@@ -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
|
-
#
|
|
277
|
-
# (
|
|
278
|
-
|
|
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)
|
verifiers/v1/utils/interrupt.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""Graceful-shutdown signal handling
|
|
2
|
-
|
|
3
|
-
are swallowed so a second
|
|
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
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
314
|
-
verifiers-0.2.1.
|
|
315
|
-
verifiers-0.2.1.
|
|
316
|
-
verifiers-0.2.1.
|
|
317
|
-
verifiers-0.2.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|