self-evolve-framework 1.5.0 → 1.6.0

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.
Files changed (149) hide show
  1. package/package.json +1 -1
  2. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  3. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  4. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  5. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  6. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  7. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  8. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  9. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  10. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  11. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
@@ -0,0 +1,512 @@
1
+ """Generic task dataloader abstractions for ReflACT.
2
+
3
+ ReflACT does not train model parameters directly. Instead, it iterates over
4
+ task batches, rolls out the current skill, reflects on failures/successes,
5
+ and updates the skill document. Because of that, the "dataloader" abstraction
6
+ here is closer to a batch sampler / episode planner than a tensor loader.
7
+
8
+ Class hierarchy::
9
+
10
+ BaseDataLoader # abstract — simulator-backed envs (e.g. ALFWorld)
11
+ └── SplitDataLoader # abstract — dataset-backed envs with split_dir
12
+
13
+ SplitDataLoader supports two dataset entry modes:
14
+
15
+ 1. ``split_mode="split_dir"``: consume an existing split directory.
16
+ 2. ``split_mode="ratio"``: build a deterministic split directory from a raw
17
+ dataset path using an explicit train:val:test ratio.
18
+
19
+ In either case, the standardised split layout is:
20
+
21
+ split_dir/
22
+ ├── train/ # training items
23
+ ├── val/ # validation / selection items (gate)
24
+ └── test/ # held-out test items
25
+
26
+ Each subdirectory's contents are benchmark-specific. Subclasses only need
27
+ to implement ``load_split_items(split_path)`` to teach the loader how to
28
+ read items from one of those directories.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import glob
33
+ import json
34
+ import os
35
+ import random
36
+ from abc import ABC, abstractmethod
37
+ from dataclasses import dataclass, field
38
+ from typing import Any
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class BatchSpec:
43
+ """A concrete batch request consumed by the training loop.
44
+
45
+ Parameters
46
+ ----------
47
+ phase : str
48
+ ``"train"`` or ``"eval"``.
49
+ split : str
50
+ Dataset split name, typically ``"train"`` or an eval split.
51
+ seed : int
52
+ Random seed used to construct the batch deterministically.
53
+ batch_size : int
54
+ Requested number of items / episodes in this batch.
55
+ payload : object | None
56
+ Environment-specific batch payload. For dataset-backed environments
57
+ this is often a list of sampled items; for simulator-backed
58
+ environments this may be ``None`` and the seed alone can define the
59
+ batch.
60
+ metadata : dict[str, Any]
61
+ Optional structured metadata for logging, resume, or curriculum logic.
62
+ """
63
+
64
+ phase: str
65
+ split: str
66
+ seed: int
67
+ batch_size: int
68
+ payload: object | None = None
69
+ metadata: dict[str, Any] = field(default_factory=dict)
70
+
71
+
72
+ class BaseDataLoader(ABC):
73
+ """Abstract base class for task batch planning in ReflACT.
74
+
75
+ Subclasses are responsible for defining how a train or eval batch is
76
+ sampled. The default implementation here provides deterministic epoch seed
77
+ planning so all loaders share the same reproducibility behavior.
78
+ """
79
+
80
+ def setup(self, cfg: dict) -> None:
81
+ """Optional one-time initialization with the full trainer config."""
82
+
83
+ def set_out_root(self, out_root: str) -> None:
84
+ """Optional hook for loaders that persist split files or state."""
85
+
86
+ def state_dict(self) -> dict[str, Any]:
87
+ """Return serializable loader state for resume support."""
88
+ return {}
89
+
90
+ def load_state_dict(self, state: dict[str, Any]) -> None:
91
+ """Restore loader state from :meth:`state_dict` output."""
92
+
93
+ def get_train_size(self) -> int | None:
94
+ """Return the size of the training pool when known."""
95
+ return None
96
+
97
+ @staticmethod
98
+ def make_base_seeds(steps_per_epoch: int, accumulation: int, seed: int) -> list[int]:
99
+ """Return the deterministic seed pool used to define train batches."""
100
+ batches_per_epoch = steps_per_epoch * accumulation
101
+ return [seed + i + 1 for i in range(batches_per_epoch)]
102
+
103
+ @staticmethod
104
+ def shuffle_epoch_seeds(base_seeds: list[int], epoch: int, seed: int) -> list[int]:
105
+ """Return the per-epoch deterministic shuffle of *base_seeds*."""
106
+ epoch_rng = random.Random(seed + epoch * 1000)
107
+ shuffled = list(base_seeds)
108
+ epoch_rng.shuffle(shuffled)
109
+ return shuffled
110
+
111
+ def plan_train_epoch(
112
+ self,
113
+ *,
114
+ epoch: int,
115
+ steps_per_epoch: int,
116
+ accumulation: int,
117
+ batch_size: int,
118
+ seed: int,
119
+ **kwargs,
120
+ ) -> list[BatchSpec]:
121
+ """Build the full list of training batches for one epoch."""
122
+ base_seeds = self.make_base_seeds(
123
+ steps_per_epoch=steps_per_epoch,
124
+ accumulation=accumulation,
125
+ seed=seed,
126
+ )
127
+ shuffled_seeds = self.shuffle_epoch_seeds(base_seeds, epoch=epoch, seed=seed)
128
+ return [
129
+ self.build_train_batch(batch_size=batch_size, seed=batch_seed, **kwargs)
130
+ for batch_seed in shuffled_seeds
131
+ ]
132
+
133
+ @abstractmethod
134
+ def build_train_batch(self, batch_size: int, seed: int, **kwargs) -> BatchSpec:
135
+ """Construct one training batch specification."""
136
+
137
+ @abstractmethod
138
+ def build_eval_batch(
139
+ self,
140
+ env_num: int,
141
+ split: str,
142
+ seed: int,
143
+ **kwargs,
144
+ ) -> BatchSpec:
145
+ """Construct one evaluation batch specification."""
146
+
147
+
148
+ # ── Split-based dataloader for dataset-backed environments ──────────────
149
+
150
+ # Canonical split names expected under split_dir/
151
+ SPLIT_NAMES = ("train", "val", "test")
152
+
153
+ # Maps legacy / trainer split names → canonical directory names
154
+ _SPLIT_ALIAS: dict[str, str] = {
155
+ "train": "train",
156
+ "valid_seen": "val",
157
+ "selection": "val",
158
+ "val": "val",
159
+ "valid_unseen": "test",
160
+ "test": "test",
161
+ }
162
+
163
+
164
+ def _load_json_or_jsonl(path: str) -> list[dict]:
165
+ """Load a list of items from a JSON or JSONL file."""
166
+ with open(path, encoding="utf-8") as f:
167
+ content = f.read().strip()
168
+ if not content:
169
+ return []
170
+
171
+ try:
172
+ data = json.loads(content)
173
+ except json.JSONDecodeError:
174
+ data = None
175
+
176
+ if isinstance(data, list):
177
+ return data
178
+ if isinstance(data, dict):
179
+ nested = data.get("data")
180
+ if isinstance(nested, list):
181
+ return nested
182
+ return list(data.values())
183
+
184
+ items: list[dict] = []
185
+ for line in content.splitlines():
186
+ line = line.strip()
187
+ if line:
188
+ items.append(json.loads(line))
189
+ return items
190
+
191
+
192
+ def _parse_split_ratio(text: str) -> tuple[int, int, int]:
193
+ parts = [part.strip() for part in str(text or "").split(":") if part.strip()]
194
+ if len(parts) != 3:
195
+ raise ValueError(
196
+ f"split_ratio must be in train:val:test form, got {text!r}"
197
+ )
198
+ try:
199
+ train, val, test = (int(part) for part in parts)
200
+ except ValueError as exc:
201
+ raise ValueError(
202
+ f"split_ratio must contain integers, got {text!r}"
203
+ ) from exc
204
+ if min(train, val, test) <= 0:
205
+ raise ValueError(f"split_ratio parts must be positive, got {text!r}")
206
+ return train, val, test
207
+
208
+
209
+ def _compute_split_counts(total: int, ratio: tuple[int, int, int]) -> tuple[int, int, int]:
210
+ weights = list(ratio)
211
+ denom = sum(weights)
212
+ raw = [total * weight / denom for weight in weights]
213
+ counts = [int(value) for value in raw]
214
+ remaining = total - sum(counts)
215
+ order = sorted(
216
+ range(len(raw)),
217
+ key=lambda idx: (raw[idx] - counts[idx], weights[idx]),
218
+ reverse=True,
219
+ )
220
+ for idx in order[:remaining]:
221
+ counts[idx] += 1
222
+ return counts[0], counts[1], counts[2]
223
+
224
+
225
+ class SplitDataLoader(BaseDataLoader):
226
+ """Base class for dataset-backed environments.
227
+
228
+ Supported modes:
229
+
230
+ - ``split_mode="split_dir"``: load an existing ``train/``, ``val/``,
231
+ ``test/`` directory tree.
232
+ - ``split_mode="ratio"``: load raw items from ``data_path`` and materialize
233
+ a deterministic split directory with the requested ratio.
234
+ """
235
+
236
+ def __init__(
237
+ self,
238
+ split_dir: str = "",
239
+ data_path: str = "",
240
+ split_mode: str = "ratio",
241
+ split_ratio: str = "2:1:7",
242
+ split_seed: int = 42,
243
+ split_output_dir: str = "",
244
+ seed: int = 42,
245
+ limit: int = 0,
246
+ **kwargs,
247
+ ) -> None:
248
+ self.split_dir = split_dir
249
+ self.data_path = data_path
250
+ self.split_mode = split_mode
251
+ self.split_ratio = split_ratio
252
+ self.split_seed = int(split_seed)
253
+ self.split_output_dir = split_output_dir
254
+ self.seed = seed
255
+ self.limit = limit
256
+ self._splits: dict[str, list[dict]] = {}
257
+
258
+ # ── Setup ────────────────────────────────────────────────────────────
259
+
260
+ def setup(self, cfg: dict) -> None:
261
+ if not self.split_mode:
262
+ self.split_mode = str(cfg.get("split_mode", "ratio") or "ratio")
263
+ if not self.split_dir:
264
+ self.split_dir = cfg.get("split_dir", "")
265
+ if not self.data_path:
266
+ self.data_path = cfg.get("data_path", "")
267
+ if not self.split_output_dir:
268
+ self.split_output_dir = cfg.get("split_output_dir", "")
269
+ if "split_seed" in cfg and not self.split_seed:
270
+ self.split_seed = int(cfg.get("split_seed", 0) or 0)
271
+ if not self.split_seed:
272
+ self.split_seed = self.seed
273
+ if not self.split_ratio:
274
+ self.split_ratio = str(cfg.get("split_ratio", "2:1:7") or "2:1:7")
275
+
276
+ mode = str(self.split_mode or "ratio").strip().lower()
277
+ if mode not in {"ratio", "split_dir"}:
278
+ raise ValueError(
279
+ f"{type(self).__name__} split_mode must be 'ratio' or 'split_dir', "
280
+ f"got {self.split_mode!r}"
281
+ )
282
+ self.split_mode = mode
283
+
284
+ if self.split_mode == "ratio":
285
+ self.split_dir = self._materialize_ratio_split(cfg)
286
+ if not self.split_dir:
287
+ raise ValueError(
288
+ f"{type(self).__name__} requires either "
289
+ "`split_mode=ratio` with `data_path`, or `split_mode=split_dir` "
290
+ f"with `split_dir` pointing to {'/'.join(SPLIT_NAMES)}/."
291
+ )
292
+ self._load_all_splits()
293
+
294
+ def _resolve_split_output_dir(self, cfg: dict) -> str:
295
+ if self.split_output_dir:
296
+ return os.path.abspath(self.split_output_dir)
297
+ out_root = os.path.abspath(str(cfg.get("out_root") or os.getcwd()))
298
+ env_name = str(cfg.get("env") or type(self).__name__.replace("DataLoader", "").lower())
299
+ ratio_tag = str(self.split_ratio or "2:1:7").replace(":", "-")
300
+ return os.path.join(out_root, "_generated_splits", f"{env_name}_{ratio_tag}_seed{self.split_seed}")
301
+
302
+ def load_raw_items(self, data_path: str) -> list[dict]:
303
+ """Load raw items from a dataset path before ratio splitting.
304
+
305
+ Subclasses can override when the raw dataset is not a single JSON/JSONL
306
+ file or when directory layouts require custom normalization.
307
+ """
308
+ if os.path.isdir(data_path):
309
+ if any(os.path.isdir(os.path.join(data_path, name)) for name in SPLIT_NAMES):
310
+ raise ValueError(
311
+ f"{type(self).__name__} got a split directory as data_path. "
312
+ "Use split_mode=split_dir and pass it as split_dir instead."
313
+ )
314
+ candidates = sorted(glob.glob(os.path.join(data_path, "*.json")))
315
+ candidates += sorted(glob.glob(os.path.join(data_path, "*.jsonl")))
316
+ if len(candidates) != 1:
317
+ raise ValueError(
318
+ f"{type(self).__name__} expected data_path to be one JSON/JSONL file "
319
+ f"or a directory containing exactly one such file, got: {data_path}"
320
+ )
321
+ return _load_json_or_jsonl(candidates[0])
322
+ return _load_json_or_jsonl(data_path)
323
+
324
+ def write_split_items(self, split_path: str, items: list[dict]) -> None:
325
+ os.makedirs(split_path, exist_ok=True)
326
+ out_path = os.path.join(split_path, "items.json")
327
+ with open(out_path, "w", encoding="utf-8") as f:
328
+ json.dump(items, f, ensure_ascii=False, indent=2)
329
+
330
+ def _materialize_ratio_split(self, cfg: dict) -> str:
331
+ data_path = os.path.abspath(str(self.data_path or "").strip())
332
+ if not data_path:
333
+ raise ValueError(
334
+ f"{type(self).__name__} requires data_path when split_mode=ratio."
335
+ )
336
+
337
+ ratio = _parse_split_ratio(self.split_ratio)
338
+ items = self.load_raw_items(data_path)
339
+ if not isinstance(items, list) or not items:
340
+ raise ValueError(f"No raw items available for ratio split from {data_path}")
341
+
342
+ shuffled = list(items)
343
+ rng = random.Random(self.split_seed)
344
+ rng.shuffle(shuffled)
345
+
346
+ train_n, val_n, test_n = _compute_split_counts(len(shuffled), ratio)
347
+ train_items = shuffled[:train_n]
348
+ val_items = shuffled[train_n: train_n + val_n]
349
+ test_items = shuffled[train_n + val_n: train_n + val_n + test_n]
350
+
351
+ split_dir = self._resolve_split_output_dir(cfg)
352
+ manifest = {
353
+ "source_data_path": data_path,
354
+ "split_mode": "ratio",
355
+ "split_ratio": self.split_ratio,
356
+ "split_seed": self.split_seed,
357
+ "counts": {
358
+ "train": len(train_items),
359
+ "val": len(val_items),
360
+ "test": len(test_items),
361
+ },
362
+ }
363
+ os.makedirs(split_dir, exist_ok=True)
364
+ self.write_split_items(os.path.join(split_dir, "train"), train_items)
365
+ self.write_split_items(os.path.join(split_dir, "val"), val_items)
366
+ self.write_split_items(os.path.join(split_dir, "test"), test_items)
367
+ with open(os.path.join(split_dir, "split_manifest.json"), "w", encoding="utf-8") as f:
368
+ json.dump(manifest, f, ensure_ascii=False, indent=2)
369
+ print(
370
+ f" [{type(self).__name__}] generated ratio split {self.split_ratio} "
371
+ f"at {split_dir} from {data_path}"
372
+ )
373
+ return split_dir
374
+
375
+ def _load_all_splits(self) -> None:
376
+ for name in SPLIT_NAMES:
377
+ split_path = os.path.join(self.split_dir, name)
378
+ if not os.path.isdir(split_path):
379
+ raise ValueError(
380
+ f"Missing '{name}/' subdirectory in split_dir: {self.split_dir}"
381
+ )
382
+ items = self.load_split_items(split_path)
383
+ if self.limit:
384
+ items = items[: self.limit]
385
+ self._splits[name] = items
386
+
387
+ counts = " ".join(f"{k}={len(v)}" for k, v in self._splits.items())
388
+ print(f" [{type(self).__name__}] {counts} (from {self.split_dir})")
389
+
390
+ def load_split_items(self, split_path: str) -> list[dict]:
391
+ """Load items from one split directory (e.g. ``split_dir/train/``).
392
+
393
+ Default: finds the first ``.json`` file in the directory and loads it
394
+ as a JSON array. Subclasses can override for custom formats.
395
+ """
396
+ json_files = sorted(glob.glob(os.path.join(split_path, "*.json")))
397
+ if not json_files:
398
+ raise FileNotFoundError(
399
+ f"No .json file found in {split_path}"
400
+ )
401
+ with open(json_files[0], encoding="utf-8") as f:
402
+ items = json.load(f)
403
+ if not isinstance(items, list):
404
+ raise ValueError(
405
+ f"Expected JSON array in {json_files[0]}, got {type(items).__name__}"
406
+ )
407
+ return items
408
+
409
+ # ── Accessors ────────────────────────────────────────────────────────
410
+
411
+ @property
412
+ def train_items(self) -> list[dict]:
413
+ return self._splits.get("train", [])
414
+
415
+ @property
416
+ def val_items(self) -> list[dict]:
417
+ return self._splits.get("val", [])
418
+
419
+ @property
420
+ def test_items(self) -> list[dict]:
421
+ return self._splits.get("test", [])
422
+
423
+ def get_split_items(self, split: str) -> list[dict]:
424
+ """Resolve a split name (including legacy aliases) to its item list."""
425
+ canonical = _SPLIT_ALIAS.get(split, split)
426
+ return list(self._splits.get(canonical, self.val_items))
427
+
428
+ def get_train_size(self) -> int:
429
+ return len(self.train_items)
430
+
431
+ def plan_train_epoch(
432
+ self,
433
+ *,
434
+ epoch: int,
435
+ steps_per_epoch: int,
436
+ accumulation: int,
437
+ batch_size: int,
438
+ seed: int,
439
+ **kwargs,
440
+ ) -> list[BatchSpec]:
441
+ """Build one full epoch that covers the train split in shuffled order.
442
+
443
+ For split-backed datasets, an epoch should correspond to one pass over
444
+ the available training items rather than repeated independent sampling.
445
+ """
446
+ epoch_rng = random.Random(seed + epoch * 1000)
447
+ items = list(self.train_items)
448
+ epoch_rng.shuffle(items)
449
+
450
+ total_batches = steps_per_epoch * accumulation
451
+ if total_batches <= 0:
452
+ return []
453
+
454
+ batches: list[BatchSpec] = []
455
+ cursor = 0
456
+ for batch_idx in range(total_batches):
457
+ batch_items = items[cursor: cursor + batch_size]
458
+ cursor += len(batch_items)
459
+
460
+ # Extremely small datasets can leave trailing empty microbatches
461
+ # when accumulation > 1. Reuse the shuffled prefix in that case so
462
+ # the trainer still receives the expected batch count.
463
+ if not batch_items and items:
464
+ refill_rng = random.Random(seed + epoch * 1000 + batch_idx + 1)
465
+ batch_items = list(items)
466
+ refill_rng.shuffle(batch_items)
467
+ batch_items = batch_items[:batch_size]
468
+
469
+ batches.append(
470
+ BatchSpec(
471
+ phase="train",
472
+ split="train",
473
+ seed=seed + epoch * 1000 + batch_idx + 1,
474
+ batch_size=len(batch_items),
475
+ payload=batch_items,
476
+ )
477
+ )
478
+
479
+ return batches
480
+
481
+ # ── Batch construction ───────────────────────────────────────────────
482
+
483
+ def build_train_batch(self, batch_size: int, seed: int, **kwargs) -> BatchSpec:
484
+ rng = random.Random(seed)
485
+ items = list(self.train_items)
486
+ rng.shuffle(items)
487
+ items = items[:batch_size]
488
+ return BatchSpec(
489
+ phase="train",
490
+ split="train",
491
+ seed=seed,
492
+ batch_size=len(items),
493
+ payload=items,
494
+ )
495
+
496
+ def build_eval_batch(
497
+ self,
498
+ env_num: int,
499
+ split: str,
500
+ seed: int,
501
+ **kwargs,
502
+ ) -> BatchSpec:
503
+ items = self.get_split_items(split)
504
+ if env_num and env_num < len(items):
505
+ items = items[:env_num]
506
+ return BatchSpec(
507
+ phase="eval",
508
+ split=split,
509
+ seed=seed,
510
+ batch_size=len(items),
511
+ payload=items,
512
+ )
@@ -0,0 +1,9 @@
1
+ """ReflACT Engine -- the training runner.
2
+
3
+ Analogous to the Runner in mmengine: orchestrates the full training pipeline
4
+ including rollout, gradient computation, aggregation, optimization, and
5
+ evaluation.
6
+ """
7
+ from skillopt.engine.trainer import ReflACTTrainer # noqa: F401
8
+
9
+ __all__ = ["ReflACTTrainer"]