vanishingradient commited on
Commit
82c54e4
·
verified ·
1 Parent(s): d91c758

Add deepagents_rl package sources

Browse files
Files changed (1) hide show
  1. src/deepagents_rl/train/swe_env.py +269 -49
src/deepagents_rl/train/swe_env.py CHANGED
@@ -2,6 +2,9 @@
2
 
3
  Kaggle has no Docker for instance images — `get_reward` POSTs the model patch
4
  to `SWE_GRADER_URL` (local `scripts/swe_grader_server.py` + tunnel).
 
 
 
5
  """
6
  from __future__ import annotations
7
 
@@ -9,8 +12,11 @@ import json
9
  import os
10
  import shutil
11
  import tempfile
 
 
 
12
  from pathlib import Path
13
- from typing import Any
14
 
15
 
16
  def _result_to_str(obj: Any) -> str:
@@ -47,6 +53,16 @@ def _as_str_list(val: Any) -> list[str]:
47
  return [str(val)]
48
 
49
 
 
 
 
 
 
 
 
 
 
 
50
  def grade_patch_remote(
51
  *,
52
  instance_id: str,
@@ -66,7 +82,6 @@ def grade_patch_remote(
66
 
67
  headers = {
68
  "Content-Type": "application/json",
69
- # ngrok free tier interstitial
70
  "ngrok-skip-browser-warning": "1",
71
  }
72
  token = os.environ.get("SWE_GRADER_TOKEN") or ""
@@ -87,6 +102,16 @@ def grade_patch_remote(
87
  return resp.json()
88
 
89
 
 
 
 
 
 
 
 
 
 
 
90
  class DeepAgentSWEEnv:
91
  """Per-rollout TRL environment: LocalShellBackend tools + remote Docker reward."""
92
 
@@ -97,17 +122,95 @@ class DeepAgentSWEEnv:
97
  self._task: dict[str, Any] = {}
98
  self._last_reward: float = 0.0
99
  self._last_report: dict[str, Any] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  def reset(self, **kwargs: Any) -> None:
102
  """Materialize repo sandbox from a SWE Lite row. Prompt stays on the dataset."""
103
  self._cleanup()
 
104
 
105
  instance_id = str(kwargs.get("instance_id") or kwargs.get("task_id") or "")
106
  repo = str(kwargs.get("repo") or "")
107
  base_commit = str(kwargs.get("base_commit") or "")
108
- docker_image = str(
109
- kwargs.get("docker_image") or kwargs.get("image") or ""
110
- )
111
  eval_script = str(kwargs.get("eval_script") or "")
112
  fail_to_pass = _as_str_list(
113
  kwargs.get("fail_to_pass")
@@ -131,6 +234,8 @@ class DeepAgentSWEEnv:
131
 
132
  from deepagents_rl.harness.repo import materialize_repo_at_commit
133
 
 
 
134
  self._task = {
135
  "instance_id": instance_id,
136
  "task_id": instance_id,
@@ -144,17 +249,66 @@ class DeepAgentSWEEnv:
144
  }
145
  self._last_reward = 0.0
146
  self._last_report = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- self._sandbox_parent = Path(
149
- tempfile.mkdtemp(prefix=f"darl_grpo_{instance_id[:40]}_")
150
- )
151
- self._sandbox = self._sandbox_parent / "repo"
152
- materialize_repo_at_commit(repo=repo, base_commit=base_commit, dest=self._sandbox)
153
- self._backend = LocalShellBackend(
154
- root_dir=str(self._sandbox),
155
- virtual_mode=True,
156
- timeout=60,
157
- )
158
  return None
159
 
160
  def _cleanup(self) -> None:
@@ -169,6 +323,7 @@ class DeepAgentSWEEnv:
169
  raise RuntimeError("DeepAgentSWEEnv tool called before reset()")
170
  return self._backend
171
 
 
172
  def ls(self, path: str = ".") -> str:
173
  """List files in a directory inside the sandbox.
174
 
@@ -180,6 +335,7 @@ class DeepAgentSWEEnv:
180
  """
181
  return _result_to_str(self._require_backend().ls(path))
182
 
 
183
  def glob(self, pattern: str, path: str = ".") -> str:
184
  """Glob for files matching pattern under path.
185
 
@@ -192,6 +348,7 @@ class DeepAgentSWEEnv:
192
  """
193
  return _result_to_str(self._require_backend().glob(pattern, path))
194
 
 
195
  def grep(self, pattern: str, path: str = ".") -> str:
196
  """Search file contents for a regex pattern.
197
 
@@ -204,6 +361,7 @@ class DeepAgentSWEEnv:
204
  """
205
  return _result_to_str(self._require_backend().grep(pattern, path))
206
 
 
207
  def read_file(self, file_path: str, offset: int = 0, limit: int = 200) -> str:
208
  """Read a text file from the sandbox.
209
 
@@ -219,6 +377,7 @@ class DeepAgentSWEEnv:
219
  self._require_backend().read(file_path, offset=offset, limit=limit)
220
  )
221
 
 
222
  def write_file(self, file_path: str, content: str) -> str:
223
  """Write or create a text file in the sandbox.
224
 
@@ -231,6 +390,7 @@ class DeepAgentSWEEnv:
231
  """
232
  return _result_to_str(self._require_backend().write(file_path, content))
233
 
 
234
  def edit_file(self, file_path: str, old_string: str, new_string: str) -> str:
235
  """Replace old_string with new_string in an existing file.
236
 
@@ -246,6 +406,7 @@ class DeepAgentSWEEnv:
246
  self._require_backend().edit(file_path, old_string, new_string)
247
  )
248
 
 
249
  def execute(self, command: str) -> str:
250
  """Run a shell command in the sandbox.
251
 
@@ -259,41 +420,101 @@ class DeepAgentSWEEnv:
259
 
260
  def get_reward(self) -> float:
261
  """Export git diff and grade via remote Docker SWE evaluator."""
262
- if self._sandbox is None:
263
- self._last_reward = 0.0
264
- return 0.0
265
-
266
- from deepagents_rl.harness.repo import git_diff_patch
267
-
268
- try:
269
- model_patch = git_diff_patch(self._sandbox)
270
- except Exception as e: # noqa: BLE001
271
- self._last_report = {"error": f"git_diff_failed: {e}"}
272
- self._last_reward = 0.0
273
- return 0.0
274
-
275
- if not model_patch.strip():
276
- self._last_report = {"error": "empty_model_patch", "reward_total": 0.0}
277
- self._last_reward = 0.0
278
- return 0.0
279
-
280
- try:
281
- report = grade_patch_remote(
282
- instance_id=self._task["instance_id"],
283
- docker_image=self._task["docker_image"],
284
- eval_script=self._task["eval_script"],
285
- fail_to_pass=list(self._task["fail_to_pass"]),
286
- pass_to_pass=list(self._task["pass_to_pass"]),
287
- model_patch=model_patch,
288
- )
289
- except Exception as e: # noqa: BLE001
290
- self._last_report = {"error": f"grader_failed: {e}"}
291
- self._last_reward = 0.0
292
- return 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
  self._last_report = report
295
- self._last_reward = float(report.get("reward_total") or 0.0)
296
- return self._last_reward
 
297
 
298
 
299
  def prepare_swe_grpo_rows(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -306,7 +527,6 @@ def prepare_swe_grpo_rows(records: list[dict[str, Any]]) -> list[dict[str, Any]]
306
 
307
  rows: list[dict[str, Any]] = []
308
  for rec in records:
309
- # Accept both HF records and already-normalized tasks
310
  if "fail_to_pass_json" in rec or (
311
  "prompt" in rec and isinstance(rec.get("prompt"), str) and "image" in rec
312
  ):
 
2
 
3
  Kaggle has no Docker for instance images — `get_reward` POSTs the model patch
4
  to `SWE_GRADER_URL` (local `scripts/swe_grader_server.py` + tunnel).
5
+
6
+ Each rollout opens a Langfuse agent trace; every tool call is a nested `tool`
7
+ span; grading is an `evaluator` span with reward scores.
8
  """
9
  from __future__ import annotations
10
 
 
12
  import os
13
  import shutil
14
  import tempfile
15
+ import traceback
16
+ import uuid
17
+ from functools import wraps
18
  from pathlib import Path
19
+ from typing import Any, Callable
20
 
21
 
22
  def _result_to_str(obj: Any) -> str:
 
53
  return [str(val)]
54
 
55
 
56
+ def _truncate(obj: Any, n: int = 4000) -> Any:
57
+ if isinstance(obj, str):
58
+ return obj[:n]
59
+ if isinstance(obj, dict):
60
+ return {k: _truncate(v, n) for k, v in list(obj.items())[:40]}
61
+ if isinstance(obj, list):
62
+ return [_truncate(v, n) for v in obj[:40]]
63
+ return obj
64
+
65
+
66
  def grade_patch_remote(
67
  *,
68
  instance_id: str,
 
82
 
83
  headers = {
84
  "Content-Type": "application/json",
 
85
  "ngrok-skip-browser-warning": "1",
86
  }
87
  token = os.environ.get("SWE_GRADER_TOKEN") or ""
 
102
  return resp.json()
103
 
104
 
105
+ def _traced_tool(method: Callable[..., str]) -> Callable[..., str]:
106
+ """Wrap a public tool method so each call becomes a Langfuse tool span."""
107
+
108
+ @wraps(method)
109
+ def wrapper(self: "DeepAgentSWEEnv", *args: Any, **kwargs: Any) -> str:
110
+ return self._call_tool(method.__name__, method, args, kwargs)
111
+
112
+ return wrapper
113
+
114
+
115
  class DeepAgentSWEEnv:
116
  """Per-rollout TRL environment: LocalShellBackend tools + remote Docker reward."""
117
 
 
122
  self._task: dict[str, Any] = {}
123
  self._last_reward: float = 0.0
124
  self._last_report: dict[str, Any] = {}
125
+ self._rollout_id: str = ""
126
+ self._tool_calls: list[dict[str, Any]] = []
127
+ self._lf: Any = None
128
+ self._root_cm: Any = None
129
+ self._root: Any = None
130
+ self._trace_id: str = ""
131
+
132
+ def _langfuse_enabled(self) -> bool:
133
+ if os.getenv("DARL_LANGFUSE", "1").strip() in ("0", "false", "no"):
134
+ return False
135
+ return bool(os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY"))
136
+
137
+ def _ensure_langfuse(self) -> Any | None:
138
+ if not self._langfuse_enabled():
139
+ return None
140
+ if self._lf is not None:
141
+ return self._lf
142
+ try:
143
+ from langfuse import Langfuse, get_client
144
+
145
+ Langfuse(
146
+ public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
147
+ secret_key=os.environ["LANGFUSE_SECRET_KEY"],
148
+ host=os.getenv("LANGFUSE_HOST") or os.getenv("LANGFUSE_BASE_URL"),
149
+ )
150
+ self._lf = get_client()
151
+ return self._lf
152
+ except Exception as e: # noqa: BLE001
153
+ print(f"[langfuse] init failed: {e}")
154
+ return None
155
+
156
+ def _end_trace(self) -> None:
157
+ if self._root_cm is not None:
158
+ try:
159
+ self._root_cm.__exit__(None, None, None)
160
+ except Exception:
161
+ pass
162
+ self._root_cm = None
163
+ self._root = None
164
+ if self._lf is not None:
165
+ try:
166
+ self._lf.flush()
167
+ except Exception:
168
+ pass
169
+
170
+ def _call_tool(
171
+ self,
172
+ name: str,
173
+ method: Callable[..., str],
174
+ args: tuple[Any, ...],
175
+ kwargs: dict[str, Any],
176
+ ) -> str:
177
+ payload = _truncate({"args": list(args), "kwargs": kwargs})
178
+ lf = self._lf
179
+ if lf is None or self._root is None:
180
+ out = method(self, *args, **kwargs)
181
+ self._tool_calls.append({"name": name, "input": payload, "output": _truncate(out)})
182
+ return out
183
+
184
+ with lf.start_as_current_observation(
185
+ name=name,
186
+ as_type="tool",
187
+ input=payload,
188
+ metadata={
189
+ "rollout_id": self._rollout_id,
190
+ "instance_id": self._task.get("instance_id"),
191
+ "harness": "deepagents-swe-grpo",
192
+ },
193
+ ) as span:
194
+ try:
195
+ out = method(self, *args, **kwargs)
196
+ span.update(output=_truncate(out))
197
+ self._tool_calls.append({"name": name, "input": payload, "output": _truncate(out)})
198
+ return out
199
+ except Exception as e: # noqa: BLE001
200
+ err = f"TOOL_ERROR: {type(e).__name__}: {e}"
201
+ span.update(output=err, level="ERROR")
202
+ self._tool_calls.append({"name": name, "input": payload, "output": err})
203
+ return err
204
 
205
  def reset(self, **kwargs: Any) -> None:
206
  """Materialize repo sandbox from a SWE Lite row. Prompt stays on the dataset."""
207
  self._cleanup()
208
+ self._end_trace()
209
 
210
  instance_id = str(kwargs.get("instance_id") or kwargs.get("task_id") or "")
211
  repo = str(kwargs.get("repo") or "")
212
  base_commit = str(kwargs.get("base_commit") or "")
213
+ docker_image = str(kwargs.get("docker_image") or kwargs.get("image") or "")
 
 
214
  eval_script = str(kwargs.get("eval_script") or "")
215
  fail_to_pass = _as_str_list(
216
  kwargs.get("fail_to_pass")
 
234
 
235
  from deepagents_rl.harness.repo import materialize_repo_at_commit
236
 
237
+ self._rollout_id = str(uuid.uuid4())
238
+ self._tool_calls = []
239
  self._task = {
240
  "instance_id": instance_id,
241
  "task_id": instance_id,
 
249
  }
250
  self._last_reward = 0.0
251
  self._last_report = {}
252
+ self._trace_id = ""
253
+
254
+ lf = self._ensure_langfuse()
255
+ if lf is not None:
256
+ try:
257
+ self._root_cm = lf.start_as_current_observation(
258
+ name="grpo-swe-rollout",
259
+ as_type="agent",
260
+ input={
261
+ "instance_id": instance_id,
262
+ "repo": repo,
263
+ "base_commit": base_commit,
264
+ "prompt_preview": _truncate(
265
+ kwargs.get("prompt")
266
+ if isinstance(kwargs.get("prompt"), str)
267
+ else (
268
+ kwargs.get("problem_statement")
269
+ or (kwargs.get("prompt") or "")
270
+ ),
271
+ 2000,
272
+ ),
273
+ },
274
+ metadata={
275
+ "rollout_id": self._rollout_id,
276
+ "instance_id": instance_id,
277
+ "docker_image": docker_image,
278
+ "phase": "grpo_train_or_eval",
279
+ "harness": "deepagents-swe-grpo",
280
+ "tags": ["deepagents-rl", "swe-bench", "grpo", "tools"],
281
+ },
282
+ )
283
+ self._root = self._root_cm.__enter__()
284
+ self._trace_id = lf.get_current_trace_id() or ""
285
+ except Exception as e: # noqa: BLE001
286
+ print(f"[langfuse] start rollout failed: {e}")
287
+ self._root_cm = None
288
+ self._root = None
289
+
290
+ def _materialize() -> None:
291
+ self._sandbox_parent = Path(
292
+ tempfile.mkdtemp(prefix=f"darl_grpo_{instance_id[:40]}_")
293
+ )
294
+ self._sandbox = self._sandbox_parent / "repo"
295
+ materialize_repo_at_commit(repo=repo, base_commit=base_commit, dest=self._sandbox)
296
+ self._backend = LocalShellBackend(
297
+ root_dir=str(self._sandbox),
298
+ virtual_mode=True,
299
+ timeout=60,
300
+ )
301
 
302
+ if lf is not None and self._root is not None:
303
+ with lf.start_as_current_observation(
304
+ name="repo_materialize",
305
+ as_type="span",
306
+ input={"repo": repo, "base_commit": base_commit},
307
+ ) as span:
308
+ _materialize()
309
+ span.update(output={"sandbox": str(self._sandbox)})
310
+ else:
311
+ _materialize()
312
  return None
313
 
314
  def _cleanup(self) -> None:
 
323
  raise RuntimeError("DeepAgentSWEEnv tool called before reset()")
324
  return self._backend
325
 
326
+ @_traced_tool
327
  def ls(self, path: str = ".") -> str:
328
  """List files in a directory inside the sandbox.
329
 
 
335
  """
336
  return _result_to_str(self._require_backend().ls(path))
337
 
338
+ @_traced_tool
339
  def glob(self, pattern: str, path: str = ".") -> str:
340
  """Glob for files matching pattern under path.
341
 
 
348
  """
349
  return _result_to_str(self._require_backend().glob(pattern, path))
350
 
351
+ @_traced_tool
352
  def grep(self, pattern: str, path: str = ".") -> str:
353
  """Search file contents for a regex pattern.
354
 
 
361
  """
362
  return _result_to_str(self._require_backend().grep(pattern, path))
363
 
364
+ @_traced_tool
365
  def read_file(self, file_path: str, offset: int = 0, limit: int = 200) -> str:
366
  """Read a text file from the sandbox.
367
 
 
377
  self._require_backend().read(file_path, offset=offset, limit=limit)
378
  )
379
 
380
+ @_traced_tool
381
  def write_file(self, file_path: str, content: str) -> str:
382
  """Write or create a text file in the sandbox.
383
 
 
390
  """
391
  return _result_to_str(self._require_backend().write(file_path, content))
392
 
393
+ @_traced_tool
394
  def edit_file(self, file_path: str, old_string: str, new_string: str) -> str:
395
  """Replace old_string with new_string in an existing file.
396
 
 
406
  self._require_backend().edit(file_path, old_string, new_string)
407
  )
408
 
409
+ @_traced_tool
410
  def execute(self, command: str) -> str:
411
  """Run a shell command in the sandbox.
412
 
 
420
 
421
  def get_reward(self) -> float:
422
  """Export git diff and grade via remote Docker SWE evaluator."""
423
+ lf = self._lf
424
+ reward = 0.0
425
+ report: dict[str, Any] = {}
426
+
427
+ def _grade() -> float:
428
+ nonlocal report
429
+ if self._sandbox is None:
430
+ report = {"error": "no_sandbox", "reward_total": 0.0}
431
+ return 0.0
432
+
433
+ from deepagents_rl.harness.repo import git_diff_patch
434
+
435
+ try:
436
+ model_patch = git_diff_patch(self._sandbox)
437
+ except Exception as e: # noqa: BLE001
438
+ report = {"error": f"git_diff_failed: {e}"}
439
+ return 0.0
440
+
441
+ if not model_patch.strip():
442
+ report = {"error": "empty_model_patch", "reward_total": 0.0}
443
+ return 0.0
444
+
445
+ try:
446
+ report = grade_patch_remote(
447
+ instance_id=self._task["instance_id"],
448
+ docker_image=self._task["docker_image"],
449
+ eval_script=self._task["eval_script"],
450
+ fail_to_pass=list(self._task["fail_to_pass"]),
451
+ pass_to_pass=list(self._task["pass_to_pass"]),
452
+ model_patch=model_patch,
453
+ )
454
+ except Exception as e: # noqa: BLE001
455
+ report = {
456
+ "error": f"grader_failed: {e}",
457
+ "traceback": traceback.format_exc()[-1500:],
458
+ }
459
+ return 0.0
460
+
461
+ return float(report.get("reward_total") or 0.0)
462
+
463
+ if lf is not None and self._root is not None:
464
+ with lf.start_as_current_observation(
465
+ name="reward_grader",
466
+ as_type="evaluator",
467
+ input={
468
+ "instance_id": self._task.get("instance_id"),
469
+ "n_tool_calls": len(self._tool_calls),
470
+ "tools_used": [c["name"] for c in self._tool_calls],
471
+ },
472
+ ) as span:
473
+ reward = _grade()
474
+ span.update(
475
+ output=_truncate(
476
+ {
477
+ "reward_total": reward,
478
+ "pass_frac": report.get("pass_frac"),
479
+ "resolved": report.get("resolved"),
480
+ "resolution": report.get("resolution"),
481
+ "error": report.get("error"),
482
+ "n_tool_calls": len(self._tool_calls),
483
+ }
484
+ )
485
+ )
486
+ try:
487
+ lf.score_current_trace(name="reward_total", value=float(reward))
488
+ lf.score_current_trace(
489
+ name="pass_frac", value=float(report.get("pass_frac") or 0.0)
490
+ )
491
+ lf.score_current_trace(
492
+ name="resolved", value=1.0 if report.get("resolved") else 0.0
493
+ )
494
+ lf.score_current_trace(
495
+ name="n_tool_calls", value=float(len(self._tool_calls))
496
+ )
497
+ except Exception:
498
+ pass
499
+ if self._root is not None:
500
+ try:
501
+ self._root.update(
502
+ output={
503
+ "reward_total": reward,
504
+ "n_tool_calls": len(self._tool_calls),
505
+ "tools_used": [c["name"] for c in self._tool_calls],
506
+ "resolution": report.get("resolution"),
507
+ }
508
+ )
509
+ except Exception:
510
+ pass
511
+ else:
512
+ reward = _grade()
513
 
514
  self._last_report = report
515
+ self._last_reward = reward
516
+ self._end_trace()
517
+ return reward
518
 
519
 
520
  def prepare_swe_grpo_rows(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
 
527
 
528
  rows: list[dict[str, Any]] = []
529
  for rec in records:
 
530
  if "fail_to_pass_json" in rec or (
531
  "prompt" in rec and isinstance(rec.get("prompt"), str) and "image" in rec
532
  ):