verifiers 0.4.2.dev34__py3-none-any.whl → 0.4.2.dev44__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/clients/eval.py +8 -0
- verifiers/v1/clients/prompt_cache.py +350 -0
- verifiers/v1/configs/cli/env.py +2 -2
- verifiers/v1/configs/client.py +3 -0
- verifiers/v1/configs/select.py +1 -1
- verifiers/v1/configs/taskset.py +1 -1
- verifiers/v1/harnesses/pi/harness.py +3 -2
- verifiers/v1/taskset.py +2 -2
- verifiers/v1/utils/interrupt.py +1 -1
- verifiers/v1/utils/loaders.py +1 -1
- {verifiers-0.4.2.dev34.dist-info → verifiers-0.4.2.dev44.dist-info}/METADATA +1 -2
- {verifiers-0.4.2.dev34.dist-info → verifiers-0.4.2.dev44.dist-info}/RECORD +15 -21
- {verifiers-0.4.2.dev34.dist-info → verifiers-0.4.2.dev44.dist-info}/entry_points.txt +0 -1
- verifiers/v1/cli/gepa.py +0 -104
- verifiers/v1/gepa/__init__.py +0 -5
- verifiers/v1/gepa/adapter.py +0 -128
- verifiers/v1/gepa/config.py +0 -96
- verifiers/v1/gepa/dataset.py +0 -44
- verifiers/v1/gepa/reflection.py +0 -31
- verifiers/v1/gepa/runner.py +0 -126
- {verifiers-0.4.2.dev34.dist-info → verifiers-0.4.2.dev44.dist-info}/WHEEL +0 -0
- {verifiers-0.4.2.dev34.dist-info → verifiers-0.4.2.dev44.dist-info}/licenses/LICENSE +0 -0
verifiers/v1/clients/eval.py
CHANGED
|
@@ -9,6 +9,7 @@ from pydantic_core import from_json, to_json
|
|
|
9
9
|
|
|
10
10
|
from verifiers.v1.clients.base import build_async_httpx, join_url
|
|
11
11
|
from verifiers.v1.clients.client import SESSION_ID_HEADER, Client, RelayReply
|
|
12
|
+
from verifiers.v1.clients.prompt_cache import maybe_add_prompt_cache_control
|
|
12
13
|
from verifiers.v1.configs.client import (
|
|
13
14
|
BaseClientConfig,
|
|
14
15
|
resolve_api_key,
|
|
@@ -70,6 +71,7 @@ class EvalClient(Client):
|
|
|
70
71
|
def __init__(self, config: BaseClientConfig) -> None:
|
|
71
72
|
self.base_url = config.base_url
|
|
72
73
|
self.api_key = resolve_api_key(config)
|
|
74
|
+
self.cache_retention = getattr(config, "cache_retention", None)
|
|
73
75
|
# Keep endpoint headers separate so they can override intercepted request headers before
|
|
74
76
|
# the dialect's provider authentication is applied.
|
|
75
77
|
self.headers = resolve_headers(config)
|
|
@@ -84,6 +86,9 @@ class EvalClient(Client):
|
|
|
84
86
|
turn: PendingTurn | None = None,
|
|
85
87
|
headers: Mapping[str, str] | None = None,
|
|
86
88
|
) -> Response:
|
|
89
|
+
body = maybe_add_prompt_cache_control(
|
|
90
|
+
dialect, body, self.base_url, self.cache_retention
|
|
91
|
+
)
|
|
87
92
|
resp = await self._request(
|
|
88
93
|
join_url(self.base_url, dialect.upstream_path),
|
|
89
94
|
body,
|
|
@@ -177,6 +182,9 @@ class EvalClient(Client):
|
|
|
177
182
|
) -> RelayReply:
|
|
178
183
|
# Relay complete SSE events so the interception server can safely insert keepalives
|
|
179
184
|
# between them. Error responses are mapped before any event is handed back.
|
|
185
|
+
body = maybe_add_prompt_cache_control(
|
|
186
|
+
dialect, body, self.base_url, self.cache_retention
|
|
187
|
+
)
|
|
180
188
|
resp = await self._request(
|
|
181
189
|
join_url(self.base_url, dialect.upstream_path),
|
|
182
190
|
body,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Gateway-side prompt-cache markers for Anthropic models.
|
|
2
|
+
|
|
3
|
+
Anthropic has no implicit prompt cache: a request only caches content when a
|
|
4
|
+
block carries ``cache_control``. Verifiers relays harness-native bodies, and
|
|
5
|
+
many harnesses send no marker, so Claude traffic through Prime Inference or
|
|
6
|
+
OpenRouter pays full input price. When the caller sends no marker, mark the
|
|
7
|
+
static prefix (last custom tool and last system/developer block) that repeats
|
|
8
|
+
across rollouts; multi-turn requests also mark the latest message.
|
|
9
|
+
|
|
10
|
+
Markers default to the five-minute cache (no ``ttl``). Set
|
|
11
|
+
``cache_retention = "long"`` on the eval client, or ``PI_CACHE_RETENTION=long``,
|
|
12
|
+
to emit ``ttl: "1h"`` instead; one-hour writes cost twice as much. ``"none"``
|
|
13
|
+
disables injection entirely. Any caller marker wins over generation here.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
from urllib.parse import urlsplit
|
|
22
|
+
|
|
23
|
+
from verifiers.v1.dialects.base import Dialect
|
|
24
|
+
|
|
25
|
+
CacheRetention = Literal["none", "short", "long"]
|
|
26
|
+
|
|
27
|
+
_CACHE_MARKERS = frozenset({"cache_control", "prompt_cache_breakpoint"})
|
|
28
|
+
_SCHEMA_KEYS = frozenset({"input_schema", "parameters", "json_schema"})
|
|
29
|
+
_ANTHROPIC_BLOCK_TYPES = frozenset({"text", "image", "tool_result", "document"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_marker(value: Any) -> bool:
|
|
33
|
+
return isinstance(value, Mapping) and not _CACHE_MARKERS.isdisjoint(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _content_has_marker(content: Any) -> bool:
|
|
37
|
+
if isinstance(content, Mapping):
|
|
38
|
+
return _has_marker(content)
|
|
39
|
+
if not isinstance(content, list):
|
|
40
|
+
return False
|
|
41
|
+
return any(isinstance(block, Mapping) and _has_marker(block) for block in content)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _has_caller_marker(body: Mapping[str, Any]) -> bool:
|
|
45
|
+
"""Only scan positions where Anthropic honors cache markers.
|
|
46
|
+
|
|
47
|
+
Tool-use inputs, tool arguments, metadata, and JSON schemas can contain a
|
|
48
|
+
``cache_control`` key without being prompt-cache markers.
|
|
49
|
+
"""
|
|
50
|
+
if _has_marker(body):
|
|
51
|
+
return True
|
|
52
|
+
if _content_has_marker(body.get("system")):
|
|
53
|
+
return True
|
|
54
|
+
messages = body.get("messages")
|
|
55
|
+
if isinstance(messages, list):
|
|
56
|
+
for message in messages:
|
|
57
|
+
if not isinstance(message, Mapping):
|
|
58
|
+
continue
|
|
59
|
+
if _has_marker(message):
|
|
60
|
+
return True
|
|
61
|
+
content = message.get("content")
|
|
62
|
+
if _content_has_marker(content):
|
|
63
|
+
return True
|
|
64
|
+
if isinstance(content, list):
|
|
65
|
+
for block in content:
|
|
66
|
+
if (
|
|
67
|
+
isinstance(block, Mapping)
|
|
68
|
+
and block.get("type") == "tool_result"
|
|
69
|
+
and _content_has_marker(block.get("content"))
|
|
70
|
+
):
|
|
71
|
+
return True
|
|
72
|
+
tools = body.get("tools")
|
|
73
|
+
if isinstance(tools, list):
|
|
74
|
+
for tool in tools:
|
|
75
|
+
if _has_marker(tool):
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_cache_retention(
|
|
81
|
+
cache_retention: CacheRetention | None,
|
|
82
|
+
) -> CacheRetention:
|
|
83
|
+
"""Explicit setting wins, then ``PI_CACHE_RETENTION=long``, then short."""
|
|
84
|
+
if cache_retention is not None:
|
|
85
|
+
return cache_retention
|
|
86
|
+
env = os.environ.get("PI_CACHE_RETENTION", "").strip().casefold()
|
|
87
|
+
if env in {"none", "short", "long"}:
|
|
88
|
+
return env
|
|
89
|
+
return "short"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cache_marker(retention: CacheRetention) -> dict[str, str]:
|
|
93
|
+
marker = {"type": "ephemeral"}
|
|
94
|
+
if retention == "long":
|
|
95
|
+
marker["ttl"] = "1h"
|
|
96
|
+
return marker
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_anthropic_model(body: Mapping[str, Any]) -> bool:
|
|
100
|
+
model = body.get("model")
|
|
101
|
+
return isinstance(model, str) and model.casefold().startswith("anthropic/")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_gateway(base_url: str) -> bool:
|
|
105
|
+
hostname = urlsplit(base_url).hostname
|
|
106
|
+
if hostname is None:
|
|
107
|
+
return False
|
|
108
|
+
host = hostname.casefold()
|
|
109
|
+
return host in {"openrouter.ai", "pinference.ai"} or host.endswith(
|
|
110
|
+
(".openrouter.ai", ".pinference.ai")
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _mark_last_tool(
|
|
115
|
+
body: dict[str, Any], *, anthropic: bool, marker: dict[str, str]
|
|
116
|
+
) -> bool:
|
|
117
|
+
tools = body.get("tools")
|
|
118
|
+
if not isinstance(tools, list) or not tools:
|
|
119
|
+
return False
|
|
120
|
+
for index in range(len(tools) - 1, -1, -1):
|
|
121
|
+
tool = tools[index]
|
|
122
|
+
if not isinstance(tool, dict):
|
|
123
|
+
continue
|
|
124
|
+
if anthropic:
|
|
125
|
+
# Server tools (web search, computer use, ...) do not accept a
|
|
126
|
+
# cache marker; fall back to the last custom tool.
|
|
127
|
+
tool_type = tool.get("type")
|
|
128
|
+
if (tool_type is not None and tool_type != "custom") or "name" not in tool:
|
|
129
|
+
continue
|
|
130
|
+
elif tool.get("type") != "function":
|
|
131
|
+
continue
|
|
132
|
+
updated = list(tools)
|
|
133
|
+
updated[index] = {**tool, "cache_control": dict(marker)}
|
|
134
|
+
body["tools"] = updated
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _mark_last_text_block(
|
|
140
|
+
message: dict[str, Any], marker: dict[str, str]
|
|
141
|
+
) -> dict | None:
|
|
142
|
+
content = message.get("content")
|
|
143
|
+
if isinstance(content, str):
|
|
144
|
+
if not content:
|
|
145
|
+
return None
|
|
146
|
+
return {
|
|
147
|
+
**message,
|
|
148
|
+
"content": [
|
|
149
|
+
{
|
|
150
|
+
"type": "text",
|
|
151
|
+
"text": content,
|
|
152
|
+
"cache_control": dict(marker),
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
if not isinstance(content, list):
|
|
157
|
+
return None
|
|
158
|
+
for index in range(len(content) - 1, -1, -1):
|
|
159
|
+
block = content[index]
|
|
160
|
+
if (
|
|
161
|
+
isinstance(block, dict)
|
|
162
|
+
and block.get("type") == "text"
|
|
163
|
+
and isinstance(block.get("text"), str)
|
|
164
|
+
and block["text"]
|
|
165
|
+
):
|
|
166
|
+
updated = list(content)
|
|
167
|
+
updated[index] = {**block, "cache_control": dict(marker)}
|
|
168
|
+
return {**message, "content": updated}
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _mark_last_anthropic_block(
|
|
173
|
+
message: dict[str, Any], marker: dict[str, str]
|
|
174
|
+
) -> dict | None:
|
|
175
|
+
content = message.get("content")
|
|
176
|
+
if isinstance(content, str):
|
|
177
|
+
if not content:
|
|
178
|
+
return None
|
|
179
|
+
return {
|
|
180
|
+
**message,
|
|
181
|
+
"content": [
|
|
182
|
+
{
|
|
183
|
+
"type": "text",
|
|
184
|
+
"text": content,
|
|
185
|
+
"cache_control": dict(marker),
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
if not isinstance(content, list):
|
|
190
|
+
return None
|
|
191
|
+
for index in range(len(content) - 1, -1, -1):
|
|
192
|
+
block = content[index]
|
|
193
|
+
if not isinstance(block, dict):
|
|
194
|
+
continue
|
|
195
|
+
block_type = block.get("type")
|
|
196
|
+
if not isinstance(block_type, str) or block_type not in _ANTHROPIC_BLOCK_TYPES:
|
|
197
|
+
continue
|
|
198
|
+
if block_type == "text" and not (
|
|
199
|
+
isinstance(block.get("text"), str) and block["text"]
|
|
200
|
+
):
|
|
201
|
+
continue
|
|
202
|
+
updated = list(content)
|
|
203
|
+
updated[index] = {**block, "cache_control": dict(marker)}
|
|
204
|
+
return {**message, "content": updated}
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _leading_instruction_indices(messages: list[Any]) -> list[int]:
|
|
209
|
+
indices: list[int] = []
|
|
210
|
+
for index, message in enumerate(messages):
|
|
211
|
+
if not isinstance(message, dict):
|
|
212
|
+
break
|
|
213
|
+
role = message.get("role")
|
|
214
|
+
if not isinstance(role, str) or role not in {"system", "developer"}:
|
|
215
|
+
break
|
|
216
|
+
indices.append(index)
|
|
217
|
+
return indices
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _has_chat_history(messages: list[Any]) -> bool:
|
|
221
|
+
user_turns = 0
|
|
222
|
+
for message in messages:
|
|
223
|
+
if not isinstance(message, dict):
|
|
224
|
+
continue
|
|
225
|
+
role = message.get("role")
|
|
226
|
+
if not isinstance(role, str):
|
|
227
|
+
continue
|
|
228
|
+
if role == "user":
|
|
229
|
+
user_turns += 1
|
|
230
|
+
elif role in {"assistant", "tool"}:
|
|
231
|
+
return True
|
|
232
|
+
return user_turns > 1
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _has_messages_history(messages: list[Any]) -> bool:
|
|
236
|
+
if len([message for message in messages if isinstance(message, dict)]) <= 1:
|
|
237
|
+
return False
|
|
238
|
+
return isinstance(messages[-1], dict) and messages[-1].get("role") == "user"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _mark_chat(body: dict[str, Any], marker: dict[str, str]) -> bool:
|
|
242
|
+
applied = _mark_last_tool(body, anthropic=False, marker=marker)
|
|
243
|
+
messages = body.get("messages")
|
|
244
|
+
if not isinstance(messages, list) or not messages:
|
|
245
|
+
return applied
|
|
246
|
+
|
|
247
|
+
updated_messages = list(messages)
|
|
248
|
+
messages_changed = False
|
|
249
|
+
for index in reversed(_leading_instruction_indices(messages)):
|
|
250
|
+
marked = _mark_last_text_block(messages[index], marker)
|
|
251
|
+
if marked is not None:
|
|
252
|
+
updated_messages[index] = marked
|
|
253
|
+
messages_changed = True
|
|
254
|
+
applied = True
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
last_message = messages[-1] if messages else None
|
|
258
|
+
last_role = last_message.get("role") if isinstance(last_message, dict) else None
|
|
259
|
+
if (
|
|
260
|
+
_has_chat_history(messages)
|
|
261
|
+
and isinstance(last_message, dict)
|
|
262
|
+
and isinstance(last_role, str)
|
|
263
|
+
and last_role in {"user", "tool"}
|
|
264
|
+
):
|
|
265
|
+
marked = _mark_last_text_block(last_message, marker)
|
|
266
|
+
if marked is not None:
|
|
267
|
+
updated_messages[-1] = marked
|
|
268
|
+
messages_changed = True
|
|
269
|
+
applied = True
|
|
270
|
+
|
|
271
|
+
if messages_changed:
|
|
272
|
+
body["messages"] = updated_messages
|
|
273
|
+
return applied
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _mark_anthropic_messages(body: dict[str, Any], marker: dict[str, str]) -> bool:
|
|
277
|
+
applied = _mark_last_tool(body, anthropic=True, marker=marker)
|
|
278
|
+
|
|
279
|
+
system = body.get("system")
|
|
280
|
+
if isinstance(system, str):
|
|
281
|
+
if system:
|
|
282
|
+
body["system"] = [
|
|
283
|
+
{
|
|
284
|
+
"type": "text",
|
|
285
|
+
"text": system,
|
|
286
|
+
"cache_control": dict(marker),
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
applied = True
|
|
290
|
+
elif isinstance(system, list):
|
|
291
|
+
for index in range(len(system) - 1, -1, -1):
|
|
292
|
+
block = system[index]
|
|
293
|
+
if (
|
|
294
|
+
isinstance(block, dict)
|
|
295
|
+
and block.get("type") == "text"
|
|
296
|
+
and isinstance(block.get("text"), str)
|
|
297
|
+
and block["text"]
|
|
298
|
+
):
|
|
299
|
+
updated = list(system)
|
|
300
|
+
updated[index] = {**block, "cache_control": dict(marker)}
|
|
301
|
+
body["system"] = updated
|
|
302
|
+
applied = True
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
messages = body.get("messages")
|
|
306
|
+
if isinstance(messages, list) and messages and _has_messages_history(messages):
|
|
307
|
+
marked = _mark_last_anthropic_block(messages[-1], marker)
|
|
308
|
+
if marked is not None:
|
|
309
|
+
body["messages"] = [*messages[:-1], marked]
|
|
310
|
+
applied = True
|
|
311
|
+
return applied
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def maybe_add_prompt_cache_control(
|
|
315
|
+
dialect: Dialect,
|
|
316
|
+
body: dict[str, Any],
|
|
317
|
+
base_url: str,
|
|
318
|
+
cache_retention: CacheRetention | None = None,
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
"""Return a request body with prompt-cache markers when eligible.
|
|
321
|
+
|
|
322
|
+
Only Anthropic models on Prime Inference/OpenRouter are touched, and only
|
|
323
|
+
when the caller has not already set any cache marker. The original body is
|
|
324
|
+
left untouched; edited lists and blocks are copied.
|
|
325
|
+
"""
|
|
326
|
+
if dialect.name not in {"anthropic_messages", "chat_completions"}:
|
|
327
|
+
return body
|
|
328
|
+
if not _is_gateway(base_url) or not _is_anthropic_model(body):
|
|
329
|
+
return body
|
|
330
|
+
retention = resolve_cache_retention(cache_retention)
|
|
331
|
+
if retention == "none":
|
|
332
|
+
return body
|
|
333
|
+
if _has_caller_marker(body):
|
|
334
|
+
return body
|
|
335
|
+
|
|
336
|
+
marker = _cache_marker(retention)
|
|
337
|
+
updated = dict(body)
|
|
338
|
+
if isinstance(body.get("tools"), list):
|
|
339
|
+
updated["tools"] = list(body["tools"])
|
|
340
|
+
if isinstance(body.get("messages"), list):
|
|
341
|
+
updated["messages"] = list(body["messages"])
|
|
342
|
+
if isinstance(body.get("system"), list):
|
|
343
|
+
updated["system"] = list(body["system"])
|
|
344
|
+
|
|
345
|
+
applied = (
|
|
346
|
+
_mark_anthropic_messages(updated, marker)
|
|
347
|
+
if dialect.name == "anthropic_messages"
|
|
348
|
+
else _mark_chat(updated, marker)
|
|
349
|
+
)
|
|
350
|
+
return updated if applied else body
|
verifiers/v1/configs/cli/env.py
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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
|
verifiers/v1/configs/client.py
CHANGED
|
@@ -75,6 +75,9 @@ class EvalClientConfig(BaseClientConfig):
|
|
|
75
75
|
"""The default (eval): forward each request to a matching endpoint via `EvalClient`."""
|
|
76
76
|
|
|
77
77
|
type: Literal["eval"] = "eval"
|
|
78
|
+
cache_retention: Literal["none", "short", "long"] | None = None
|
|
79
|
+
"""Anthropic prompt-cache lifetime. ``None`` follows ``PI_CACHE_RETENTION``
|
|
80
|
+
(short by default); ``long`` emits ``ttl: "1h"``; ``none`` disables markers."""
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
class TrainClientConfig(BaseClientConfig):
|
verifiers/v1/configs/select.py
CHANGED
verifiers/v1/configs/taskset.py
CHANGED
|
@@ -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
|
|
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"
|
|
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"
|
|
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
|
|
19
|
-
|
|
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
|
verifiers/v1/utils/interrupt.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Graceful-shutdown signal handling shared across the v1 CLI entrypoints (run,
|
|
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."""
|
verifiers/v1/utils/loaders.py
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
Version: 0.4.2.dev44
|
|
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=
|
|
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
|
|
@@ -30,11 +29,12 @@ verifiers/v1/cli/validate.py,sha256=IXvY9Ey7snPc6pJCX9QlGO_t9KFYRwcIVj0G5TFDanU,
|
|
|
30
29
|
verifiers/v1/clients/__init__.py,sha256=Ysig0tE_0E4Jsfgfes1XHN-fK1s_RCXdqZD6E7lK4PU,507
|
|
31
30
|
verifiers/v1/clients/base.py,sha256=C0G5lRm3nxt1xmPoA2cweBgCnN2KcLepmCYq1vdDrnU,1691
|
|
32
31
|
verifiers/v1/clients/client.py,sha256=HT3aQiOyYYjvz_cKEXchtUSD07-4VehebWUJL9AyON8,3166
|
|
33
|
-
verifiers/v1/clients/eval.py,sha256=
|
|
32
|
+
verifiers/v1/clients/eval.py,sha256=GgpHZGh4IW3gC-tOZiFPp62MCL4aHik0b4afz2g3-Qo,8729
|
|
33
|
+
verifiers/v1/clients/prompt_cache.py,sha256=shJhbTUwKwao6m3DsYDyHPWmhXSJJnXk3wYQhzXFsnY,11849
|
|
34
34
|
verifiers/v1/clients/train.py,sha256=v1YCVNCEXw2FNQw4lpbLoq18u8TQeo9TnsYOoMWr4zE,20277
|
|
35
35
|
verifiers/v1/configs/__init__.py,sha256=X7u6X7B3ieD1HZl79Tv1kg-yPHbT0QLvAebaGqxVtvA,317
|
|
36
36
|
verifiers/v1/configs/agent.py,sha256=xn9j1WcBIGz7LjgW8WOWDvSvk7vPLztisSLVcT2foxk,7530
|
|
37
|
-
verifiers/v1/configs/client.py,sha256=
|
|
37
|
+
verifiers/v1/configs/client.py,sha256=nFUI5oCih6L7by6M2l3wevghzpct8E_grmabT8lYWrA,6381
|
|
38
38
|
verifiers/v1/configs/deployment.py,sha256=Eggd8phUNVcq_f56Haw-OT5U6fTBLDhBnXFJ5AEb7MI,389
|
|
39
39
|
verifiers/v1/configs/env.py,sha256=DgjIbU5NxLGV7h4v-2lcDJF-y4S_VqnEqNNrNt7Ztrk,7532
|
|
40
40
|
verifiers/v1/configs/harness.py,sha256=I9DcHzml17T7SYHobP7E6jHwNSL2eSqEtckioQ2pGNQ,3356
|
|
@@ -44,12 +44,12 @@ verifiers/v1/configs/retries.py,sha256=GIrwS0lnMKnlQ_Wj1Z2J6NlEYK4jX5weZxYmQmLcp
|
|
|
44
44
|
verifiers/v1/configs/runtime.py,sha256=7To2aecnYuv_ThVt5OIlKUiocldmt4O0hZzkSwuJsBI,11773
|
|
45
45
|
verifiers/v1/configs/sampling.py,sha256=bd6lietww4htrNYmUEqwK5yfKMhUT3nEjHoCg5xl_ys,1174
|
|
46
46
|
verifiers/v1/configs/scoring.py,sha256=8t5NrQU9b8R9dGdcG-aQm_U1m3TgeMIbw_XUN4esoF0,3115
|
|
47
|
-
verifiers/v1/configs/select.py,sha256=
|
|
47
|
+
verifiers/v1/configs/select.py,sha256=a72aweHXsA1B9i-ShR710NZr4ZFC-0xfSQ5J8aQGrew,5820
|
|
48
48
|
verifiers/v1/configs/task.py,sha256=3CB9pumHdxAHlxLKuLEiCfQPW37tNqy_HC6DgG2GKj4,2958
|
|
49
|
-
verifiers/v1/configs/taskset.py,sha256=
|
|
49
|
+
verifiers/v1/configs/taskset.py,sha256=OJG_DfM2_6I4Mm1-egMcrIEN6UtD2JaBaXnzW9SZXk8,906
|
|
50
50
|
verifiers/v1/configs/cli/__init__.py,sha256=YMDUPdbRlDJ77dhj3QqzmyRhqNgUvuQdpj_bdBqzZPM,78
|
|
51
51
|
verifiers/v1/configs/cli/debug.py,sha256=1vddnShdwYzRJp0Ubl9sp7sgnR3m2JKHkAOk741UnWU,2748
|
|
52
|
-
verifiers/v1/configs/cli/env.py,sha256=
|
|
52
|
+
verifiers/v1/configs/cli/env.py,sha256=ysTSo2UDZIXS08LUrch87XLHsdTQ-emXj6QWRsLs4lk,2576
|
|
53
53
|
verifiers/v1/configs/cli/run.py,sha256=j6ccPxFPiWsmCAmbZcc_EtElNOMaKS9YiiSDMqudczw,3794
|
|
54
54
|
verifiers/v1/configs/cli/validate.py,sha256=j6qRi2EvacXTCARnVhkFRtX9xF3IP3eVVt-Zv2ER5Vw,4073
|
|
55
55
|
verifiers/v1/dialects/__init__.py,sha256=MNOYM2NdTlSxGK4VtoEjMtD5zya2MpjPDX5jMvZUWEo,736
|
|
@@ -68,12 +68,6 @@ verifiers/v1/envs/single_agent/__init__.py,sha256=r0edAc_6gtHBhPzeBGVcdHZZqSdCPZ
|
|
|
68
68
|
verifiers/v1/envs/single_agent/env.py,sha256=iH-AnS5-5rDZM3MaYFxzaHSVqJAaw9mQJaaJvsDMMX4,854
|
|
69
69
|
verifiers/v1/envs/user_sim/__init__.py,sha256=nnT695HdqjAEouYL5wtVu6MhIp2wf5yuK-EYgxOVx0s,118
|
|
70
70
|
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
71
|
verifiers/v1/harnesses/__init__.py,sha256=SBNQmzQREsKG1ZLe1hAWdFWdXh11akXaF_9JbmJImg8,2203
|
|
78
72
|
verifiers/v1/harnesses/node.py,sha256=G6hnEms726QH9KJiNZCMJUfrIrb-8hjfHkzNJP8kxMo,5160
|
|
79
73
|
verifiers/v1/harnesses/bash/__init__.py,sha256=IAV4eMMqDHSl-9ANb0Q3kxmBZ22SeHZFBM448X4gQio,140
|
|
@@ -107,7 +101,7 @@ verifiers/v1/harnesses/opencode/harness.py,sha256=xKVGBaQv22dRF09Bu0f3PvehgF1q0e
|
|
|
107
101
|
verifiers/v1/harnesses/opencode/mcp-ready.js,sha256=QpAJcego5pNOZJu1dfpRLd873xitYGYwE9qwmgmvMvI,1904
|
|
108
102
|
verifiers/v1/harnesses/pi/__init__.py,sha256=1mSnzOf-RdEchdeg6IHtcdobG5KIpAhuTdUIgsCIn74,117
|
|
109
103
|
verifiers/v1/harnesses/pi/gate.mjs,sha256=vk_OqHXhRdYzQL7YMX8Teajm0fh9tokzDvLHVOd2I9o,449
|
|
110
|
-
verifiers/v1/harnesses/pi/harness.py,sha256=
|
|
104
|
+
verifiers/v1/harnesses/pi/harness.py,sha256=K2FtvDhoAUU_CdKVqE_NmDwsw3vqlSaZuLJWkAX0JQE,7784
|
|
111
105
|
verifiers/v1/harnesses/prime_agent/__init__.py,sha256=xfPC4RrlnyYWaf38tPAOZq4YL6M55lMpj5BoRLmaK8I,171
|
|
112
106
|
verifiers/v1/harnesses/prime_agent/harness.py,sha256=sV82MCi7uZTKlOH8Vg9_WSytgZNSbknShOhmYcdPPaE,12354
|
|
113
107
|
verifiers/v1/harnesses/rlm/__init__.py,sha256=hwx51xTdEWzzYgMj-p4ETNcptoub3IPMDbEdJ4uB8Ns,122
|
|
@@ -181,8 +175,8 @@ verifiers/v1/utils/format.py,sha256=zk6p_UrzIK5Rx7_lvgVuAxjlST35I9vwVDc6GCieo1o,
|
|
|
181
175
|
verifiers/v1/utils/generic.py,sha256=lKIzu5rvwnuSlr_kTFdupf-ElubjjvOo0vnBHYMUpC4,5036
|
|
182
176
|
verifiers/v1/utils/git.py,sha256=MTRinOvG4EddfjdahHkFZUihHmhJ37n5ROtgjzjE1NA,8366
|
|
183
177
|
verifiers/v1/utils/image.py,sha256=OFw_wdwVtbdwa8uJ3X3vYfhpUAi0CTH5HMBupjVsRRQ,282
|
|
184
|
-
verifiers/v1/utils/interrupt.py,sha256=
|
|
185
|
-
verifiers/v1/utils/loaders.py,sha256=
|
|
178
|
+
verifiers/v1/utils/interrupt.py,sha256=HueVSI54ZVIKcJ0uZ62uQduUSgXEXJRL3tlzlo1FCak,1043
|
|
179
|
+
verifiers/v1/utils/loaders.py,sha256=lEeRQ5j6-YJ9UKshhGKBz6g5yx6dn-M1T8urXpNP5KI,13043
|
|
186
180
|
verifiers/v1/utils/logging.py,sha256=F1ymwd1A8Unu85kAQ52MAowHnR7EKQIl0RDRY8u-N5I,5392
|
|
187
181
|
verifiers/v1/utils/memory.py,sha256=ZkIvGk6uITAH5sKon65LifKPbvZr8mJ__PVc23FZpOQ,1835
|
|
188
182
|
verifiers/v1/utils/network.py,sha256=SI6_PXHIAPows6VBpK75N6dMbPw6lWQQ3OjBQkSDGN4,1463
|
|
@@ -193,8 +187,8 @@ verifiers/v1/utils/scope.py,sha256=zbs4pj6AVk4jywlAJ3AX0Q0_kzB9dBB02nY-9H4ELdQ,8
|
|
|
193
187
|
verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
|
|
194
188
|
verifiers/v1/utils/trace_store.py,sha256=Qo9qvsJAl8PoRUavjQAfUB2VNxmUtYQtU2ejLKPzY9c,3382
|
|
195
189
|
verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
|
|
196
|
-
verifiers-0.4.2.
|
|
197
|
-
verifiers-0.4.2.
|
|
198
|
-
verifiers-0.4.2.
|
|
199
|
-
verifiers-0.4.2.
|
|
200
|
-
verifiers-0.4.2.
|
|
190
|
+
verifiers-0.4.2.dev44.dist-info/METADATA,sha256=1QsclZmVbRWmAKlpEy0XS6It5xZpqx0mfwtAzwiH7VA,5115
|
|
191
|
+
verifiers-0.4.2.dev44.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
192
|
+
verifiers-0.4.2.dev44.dist-info/entry_points.txt,sha256=qdMg7ajiFpDsHf7tuPMRclJXizu1J6LTWawS5YS0B0I,137
|
|
193
|
+
verifiers-0.4.2.dev44.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
|
|
194
|
+
verifiers-0.4.2.dev44.dist-info/RECORD,,
|
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', '')}")
|
verifiers/v1/gepa/__init__.py
DELETED
verifiers/v1/gepa/adapter.py
DELETED
|
@@ -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
|
verifiers/v1/gepa/config.py
DELETED
|
@@ -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
|
verifiers/v1/gepa/dataset.py
DELETED
|
@@ -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
|
-
)
|
verifiers/v1/gepa/reflection.py
DELETED
|
@@ -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
|
verifiers/v1/gepa/runner.py
DELETED
|
@@ -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()
|
|
File without changes
|
|
File without changes
|