vanishingradient commited on
Commit
a31a731
·
verified ·
1 Parent(s): af71b36

Add deepagents_rl package sources

Browse files
src/deepagents_rl/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """deepagents-rl: coding-agent RL post-training with full observability."""
2
+
3
+ __version__ = "0.1.0"
src/deepagents_rl/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (278 Bytes). View file
 
src/deepagents_rl/data/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .mbpp import build_train_eval, load_mbpp_sanitized, save_jsonl, to_hf_dataset
2
+
3
+ __all__ = ["build_train_eval", "load_mbpp_sanitized", "save_jsonl", "to_hf_dataset"]
src/deepagents_rl/data/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (318 Bytes). View file
 
src/deepagents_rl/data/__pycache__/mbpp.cpython-312.pyc ADDED
Binary file (6.16 kB). View file
 
src/deepagents_rl/data/mbpp.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MBPP sanitized load/filter for small-model coding RL."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from deepagents_rl.rewards.unit_test import run_hidden_tests
9
+
10
+
11
+ def _row_to_task(ex: dict[str, Any], split: str) -> dict[str, Any] | None:
12
+ """Normalize an MBPP sanitized example."""
13
+ task_id = ex.get("task_id") or ex.get("id")
14
+ text = ex.get("text") or ex.get("prompt") or ex.get("description") or ""
15
+ code = ex.get("code") or ex.get("reference_code") or ""
16
+ test_list = list(ex.get("test_list") or ex.get("test_cases") or [])
17
+ if not text or not test_list:
18
+ return None
19
+ # Leakage-aware: first assert may appear in prompt as visible example
20
+ visible = [test_list[0]] if test_list else []
21
+ hidden = list(test_list)
22
+ prompt = (
23
+ f"Write a Python solution for the following problem.\n\n"
24
+ f"{text.strip()}\n\n"
25
+ f"Visible example test (not the full grader):\n{visible[0]}\n\n"
26
+ f"Return ONLY a ```python``` code block with the full solution."
27
+ )
28
+ return {
29
+ "task_id": str(task_id),
30
+ "split": split,
31
+ "text": text.strip(),
32
+ "reference_code": code,
33
+ "visible_tests": visible,
34
+ "hidden_tests": hidden,
35
+ "prompt": prompt,
36
+ "test_list": test_list,
37
+ }
38
+
39
+
40
+ def filter_solvable(tasks: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
41
+ """Keep tasks whose reference solution passes hidden tests in our sandbox."""
42
+ keep: list[dict[str, Any]] = []
43
+ drop: list[dict[str, Any]] = []
44
+ for t in tasks:
45
+ ref = t.get("reference_code") or ""
46
+ if not ref.strip():
47
+ drop.append({**t, "drop_reason": "no_reference"})
48
+ continue
49
+ grade = run_hidden_tests(ref, t["hidden_tests"], timeout_s=5.0)
50
+ if grade["pass_frac"] >= 1.0:
51
+ keep.append(t)
52
+ else:
53
+ drop.append({**t, "drop_reason": "ref_fail", "grade": grade})
54
+ return keep, drop
55
+
56
+
57
+ def load_mbpp_sanitized() -> dict[str, list[dict[str, Any]]]:
58
+ """Load google-research-datasets/mbpp sanitized config from HF."""
59
+ from datasets import load_dataset
60
+
61
+ # Prefer sanitized; fall back to full mbpp
62
+ try:
63
+ ds = load_dataset("google-research-datasets/mbpp", "sanitized")
64
+ except Exception:
65
+ ds = load_dataset("mbpp", "sanitized")
66
+
67
+ out: dict[str, list[dict[str, Any]]] = {}
68
+ for split in ds:
69
+ tasks = []
70
+ for ex in ds[split]:
71
+ t = _row_to_task(dict(ex), split=split)
72
+ if t:
73
+ tasks.append(t)
74
+ out[split] = tasks
75
+ return out
76
+
77
+
78
+ def build_train_eval(
79
+ *,
80
+ filter_refs: bool = True,
81
+ ) -> dict[str, list[dict[str, Any]]]:
82
+ """
83
+ Train = sanitized train + validation + prompt (none of test).
84
+ Eval = sanitized test (held-out).
85
+ """
86
+ raw = load_mbpp_sanitized()
87
+ train_parts = []
88
+ for key in ("train", "validation", "prompt"):
89
+ train_parts.extend(raw.get(key, []))
90
+ eval_parts = list(raw.get("test", []))
91
+
92
+ drop_log: list[dict[str, Any]] = []
93
+ if filter_refs:
94
+ train_parts, d1 = filter_solvable(train_parts)
95
+ eval_parts, d2 = filter_solvable(eval_parts)
96
+ drop_log = d1 + d2
97
+
98
+ # Disjoint task ids
99
+ train_ids = {t["task_id"] for t in train_parts}
100
+ eval_parts = [t for t in eval_parts if t["task_id"] not in train_ids]
101
+
102
+ return {
103
+ "train": train_parts,
104
+ "eval": eval_parts,
105
+ "dropped": drop_log,
106
+ }
107
+
108
+
109
+ def save_jsonl(tasks: list[dict[str, Any]], path: Path) -> None:
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ with path.open("w", encoding="utf-8") as f:
112
+ for t in tasks:
113
+ # Don't need full drop grades in published rows
114
+ row = {k: v for k, v in t.items() if k not in ("drop_reason", "grade")}
115
+ f.write(json.dumps(row) + "\n")
116
+
117
+
118
+ def to_hf_dataset(splits: dict[str, list[dict[str, Any]]]):
119
+ from datasets import Dataset, DatasetDict
120
+
121
+ def _clean(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
122
+ cleaned = []
123
+ for t in rows:
124
+ cleaned.append(
125
+ {
126
+ "task_id": t["task_id"],
127
+ "split": t["split"],
128
+ "text": t["text"],
129
+ "prompt": t["prompt"],
130
+ "reference_code": t.get("reference_code", ""),
131
+ "visible_tests": t["visible_tests"],
132
+ "hidden_tests": t["hidden_tests"],
133
+ }
134
+ )
135
+ return cleaned
136
+
137
+ return DatasetDict(
138
+ {
139
+ "train": Dataset.from_list(_clean(splits["train"])),
140
+ "eval": Dataset.from_list(_clean(splits["eval"])),
141
+ }
142
+ )
src/deepagents_rl/observability/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .langfuse_eval import init_langfuse, log_checkpoint_eval
2
+
3
+ __all__ = ["init_langfuse", "log_checkpoint_eval"]
src/deepagents_rl/observability/langfuse_eval.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Langfuse logging for checkpoint eval batches (Kaggle + local)."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Any
6
+
7
+
8
+ def init_langfuse():
9
+ from langfuse import Langfuse, get_client
10
+
11
+ Langfuse(
12
+ public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
13
+ secret_key=os.environ["LANGFUSE_SECRET_KEY"],
14
+ host=os.getenv("LANGFUSE_HOST")
15
+ or os.getenv("LANGFUSE_BASE_URL")
16
+ or "https://cloud.langfuse.com",
17
+ )
18
+ lf = get_client()
19
+ if not lf.auth_check():
20
+ raise RuntimeError("Langfuse auth_check failed")
21
+ return lf
22
+
23
+
24
+ def log_checkpoint_eval(
25
+ *,
26
+ step: int,
27
+ hf_revision: str,
28
+ train_model_id: str,
29
+ results: list[dict[str, Any]],
30
+ wandb_run_id: str | None = None,
31
+ trace_name: str = "kaggle-ckpt-eval",
32
+ ) -> dict[str, Any]:
33
+ """
34
+ Write one Langfuse agent trace for a checkpoint eval suite.
35
+ Each problem becomes a nested span; aggregate scores on the trace.
36
+ MUST be called BEFORE pushing/saving the checkpoint artifact.
37
+ """
38
+ lf = init_langfuse()
39
+ n = len(results)
40
+ mean_pass = sum(float(r.get("pass_frac", 0.0)) for r in results) / n if n else 0.0
41
+ mean_reward = sum(float(r.get("reward_total", 0.0)) for r in results) / n if n else 0.0
42
+
43
+ with lf.start_as_current_observation(
44
+ name=trace_name,
45
+ as_type="evaluator",
46
+ metadata={
47
+ "step": step,
48
+ "hf_revision": hf_revision,
49
+ "train_model_id": train_model_id,
50
+ "wandb_run_id": wandb_run_id,
51
+ "phase": "ckpt_eval",
52
+ "n_problems": n,
53
+ },
54
+ input={"step": step, "hf_revision": hf_revision, "n_problems": n},
55
+ ) as root:
56
+ for r in results:
57
+ with lf.start_as_current_observation(
58
+ name=f"task:{r.get('task_id')}",
59
+ as_type="span",
60
+ input={"prompt": r.get("prompt", "")[:2000]},
61
+ metadata={"task_id": r.get("task_id")},
62
+ ) as span:
63
+ span.update(
64
+ output={
65
+ "completion": (r.get("completion") or "")[:2000],
66
+ "pass_frac": r.get("pass_frac"),
67
+ "reward_total": r.get("reward_total"),
68
+ "ok": r.get("pass_frac", 0) >= 1.0,
69
+ }
70
+ )
71
+ summary = {
72
+ "mean_pass_frac": mean_pass,
73
+ "mean_reward_total": mean_reward,
74
+ "n_problems": n,
75
+ "n_perfect": sum(1 for r in results if r.get("pass_frac", 0) >= 1.0),
76
+ "step": step,
77
+ "hf_revision": hf_revision,
78
+ }
79
+ root.update(output=summary)
80
+ lf.score_current_trace(name="mean_pass_frac", value=mean_pass)
81
+ lf.score_current_trace(name="mean_reward_total", value=mean_reward)
82
+ lf.score_current_trace(name="n_perfect", value=float(summary["n_perfect"]))
83
+ lf.score_current_trace(name="checkpoint_step", value=float(step))
84
+ trace_id = lf.get_current_trace_id() or ""
85
+
86
+ lf.flush()
87
+ return {"langfuse_trace_id": trace_id, **summary}
src/deepagents_rl/rewards/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from .unit_test import compute_reward, extract_python_code, grade_completion, grade_solution_file
2
+
3
+ __all__ = [
4
+ "compute_reward",
5
+ "extract_python_code",
6
+ "grade_completion",
7
+ "grade_solution_file",
8
+ ]
src/deepagents_rl/rewards/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (338 Bytes). View file
 
src/deepagents_rl/rewards/__pycache__/unit_test.cpython-312.pyc ADDED
Binary file (4.33 kB). View file
 
src/deepagents_rl/rewards/unit_test.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verifiable unit-test rewards for coding RL."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def run_hidden_tests(
11
+ solution_code: str,
12
+ hidden_tests: list[str],
13
+ *,
14
+ timeout_s: float = 5.0,
15
+ python_exe: str | None = None,
16
+ ) -> dict[str, Any]:
17
+ """Execute each assert against solution_code; return pass fraction + details."""
18
+ py = python_exe or sys.executable
19
+ per_test: list[dict[str, Any]] = []
20
+ passed = 0
21
+ for i, assertion in enumerate(hidden_tests):
22
+ script = f"{solution_code}\n{assertion}\n"
23
+ try:
24
+ proc = subprocess.run(
25
+ [py, "-c", script],
26
+ capture_output=True,
27
+ text=True,
28
+ timeout=timeout_s,
29
+ )
30
+ ok = proc.returncode == 0
31
+ err = (proc.stderr or "")[:500]
32
+ except Exception as e: # noqa: BLE001
33
+ ok = False
34
+ err = f"{type(e).__name__}: {e}"
35
+ per_test.append({"i": i, "ok": ok, "stderr": err})
36
+ if ok:
37
+ passed += 1
38
+ total = len(hidden_tests)
39
+ return {
40
+ "pass_frac": (passed / total) if total else 0.0,
41
+ "passed": passed,
42
+ "total": total,
43
+ "per_test": per_test,
44
+ }
45
+
46
+
47
+ def extract_python_code(completion: str) -> str:
48
+ """Pull first ```python fenced block, else raw text."""
49
+ if "```python" in completion:
50
+ start = completion.find("```python") + len("```python")
51
+ end = completion.find("```", start)
52
+ if end == -1:
53
+ return completion[start:].strip()
54
+ return completion[start:end].strip()
55
+ if "```" in completion:
56
+ start = completion.find("```") + 3
57
+ end = completion.find("```", start)
58
+ if end != -1:
59
+ return completion[start:end].strip()
60
+ return completion.strip()
61
+
62
+
63
+ def grade_completion(
64
+ completion: str,
65
+ hidden_tests: list[str],
66
+ *,
67
+ timeout_s: float = 5.0,
68
+ ) -> dict[str, Any]:
69
+ code = extract_python_code(completion)
70
+ grade = run_hidden_tests(code, hidden_tests, timeout_s=timeout_s)
71
+ grade["extracted_code"] = code
72
+ return grade
73
+
74
+
75
+ def compute_reward(
76
+ grade: dict[str, Any],
77
+ *,
78
+ format_ok: bool = True,
79
+ ran_execute: bool = False,
80
+ ) -> dict[str, float]:
81
+ components = {
82
+ "pass_frac": float(grade.get("pass_frac", 0.0)),
83
+ "format_bonus": 0.05 if format_ok and grade.get("extracted_code") else 0.0,
84
+ "tool_bonus": 0.05 if ran_execute else 0.0,
85
+ "penalties": 0.0,
86
+ }
87
+ components["reward_total"] = (
88
+ components["pass_frac"]
89
+ + components["format_bonus"]
90
+ + components["tool_bonus"]
91
+ - components["penalties"]
92
+ )
93
+ return components
94
+
95
+
96
+ def grade_solution_file(
97
+ solution_path: Path,
98
+ hidden_tests: list[str],
99
+ *,
100
+ timeout_s: float = 5.0,
101
+ ) -> dict[str, Any]:
102
+ if not solution_path.exists():
103
+ return {
104
+ "pass_frac": 0.0,
105
+ "passed": 0,
106
+ "total": len(hidden_tests),
107
+ "per_test": [],
108
+ "error": "solution.py missing",
109
+ "extracted_code": "",
110
+ }
111
+ code = solution_path.read_text(encoding="utf-8")
112
+ grade = run_hidden_tests(code, hidden_tests, timeout_s=timeout_s)
113
+ grade["extracted_code"] = code
114
+ grade["error"] = None
115
+ return grade
src/deepagents_rl/train/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .resolve_model import resolve_train_model
2
+
3
+ __all__ = ["resolve_train_model"]
src/deepagents_rl/train/eval_checkpoint.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Held-out eval used BEFORE every checkpoint save."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable
5
+
6
+ from deepagents_rl.rewards.unit_test import compute_reward, grade_completion
7
+
8
+
9
+ def eval_on_problems(
10
+ generate_fn: Callable[[str], str],
11
+ problems: list[dict[str, Any]],
12
+ ) -> list[dict[str, Any]]:
13
+ """
14
+ generate_fn(prompt) -> completion text.
15
+ Returns per-problem grade/reward rows for Langfuse + W&B.
16
+ """
17
+ rows: list[dict[str, Any]] = []
18
+ for p in problems:
19
+ prompt = p["prompt"]
20
+ completion = generate_fn(prompt)
21
+ grade = grade_completion(completion, list(p["hidden_tests"]))
22
+ reward = compute_reward(grade, format_ok=True, ran_execute=False)
23
+ rows.append(
24
+ {
25
+ "task_id": p["task_id"],
26
+ "prompt": prompt,
27
+ "completion": completion,
28
+ "pass_frac": reward["pass_frac"],
29
+ "reward_total": reward["reward_total"],
30
+ "passed": grade["passed"],
31
+ "total": grade["total"],
32
+ }
33
+ )
34
+ return rows
src/deepagents_rl/train/resolve_model.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resolve a Kaggle-fitting HF base model (never GGUF)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ DEFAULT_CANDIDATES = [
10
+ "Qwen/Qwen2.5-Coder-1.5B-Instruct",
11
+ "Qwen/Qwen2.5-Coder-0.5B-Instruct",
12
+ "Qwen/Qwen2.5-1.5B-Instruct",
13
+ "HuggingFaceTB/SmolLM2-1.7B-Instruct",
14
+ "google/gemma-2-2b-it",
15
+ ]
16
+
17
+
18
+ def _probe(repo_id: str, token: str | None) -> bool:
19
+ from huggingface_hub import HfApi
20
+
21
+ api = HfApi(token=token)
22
+ try:
23
+ info = api.model_info(repo_id)
24
+ except Exception:
25
+ return False
26
+ siblings = [s.rfilename for s in (info.siblings or [])]
27
+ # Reject pure-GGUF repos
28
+ has_weight = any(
29
+ f.endswith((".safetensors", ".bin", ".pt"))
30
+ or f == "pytorch_model.bin.index.json"
31
+ or f.endswith(".safetensors.index.json")
32
+ for f in siblings
33
+ )
34
+ only_gguf = siblings and all(
35
+ f.endswith(".gguf") or f.endswith(".md") or "license" in f.lower() or f.startswith(".")
36
+ for f in siblings
37
+ )
38
+ if only_gguf and not has_weight:
39
+ return False
40
+ return has_weight or True # allow repos that list via remote code
41
+
42
+
43
+ def resolve_train_model(
44
+ preferred: list[str] | None = None,
45
+ *,
46
+ override: str | None = None,
47
+ token: str | None = None,
48
+ out_path: Path | None = None,
49
+ ) -> dict[str, Any]:
50
+ token = token or os.getenv("HF_TOKEN")
51
+ override = override or os.getenv("TRAIN_MODEL_ID") or None
52
+ candidates = []
53
+ if override:
54
+ candidates.append(override)
55
+ candidates.extend(preferred or DEFAULT_CANDIDATES)
56
+
57
+ seen: set[str] = set()
58
+ errors: list[dict[str, str]] = []
59
+ for repo_id in candidates:
60
+ if repo_id in seen:
61
+ continue
62
+ seen.add(repo_id)
63
+ try:
64
+ if _probe(repo_id, token):
65
+ result = {
66
+ "train_model_id": repo_id,
67
+ "candidates_tried": list(seen),
68
+ "errors": errors,
69
+ }
70
+ if out_path:
71
+ out_path.parent.mkdir(parents=True, exist_ok=True)
72
+ out_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
73
+ return result
74
+ except Exception as e: # noqa: BLE001
75
+ errors.append({"repo_id": repo_id, "error": f"{type(e).__name__}: {e}"})
76
+ continue
77
+ errors.append({"repo_id": repo_id, "error": "probe_failed"})
78
+
79
+ raise RuntimeError(
80
+ "No trainable HF model resolved. Tried: "
81
+ + ", ".join(seen)
82
+ + f" errors={errors}"
83
+ )