self-evolve-framework 1.4.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 (171) hide show
  1. package/package.json +1 -1
  2. package/template/rules/ponytail.mdc +98 -23
  3. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  4. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  5. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  6. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  7. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  8. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  9. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  10. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  11. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  150. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
  151. package/template/skills/ponytail/SKILL.md +0 -133
  152. package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
  153. package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
  154. package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
  155. package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
  156. package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
  157. package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
  158. package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
  159. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
  160. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
  161. package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
  162. package/template/skills/ponytail/scripts/mcp/README.md +0 -46
  163. package/template/skills/ponytail/scripts/mcp/index.js +0 -48
  164. package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
  165. package/template/skills/ponytail/scripts/mcp/package.json +0 -13
  166. package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
  167. package/template/skills/ponytail-audit/SKILL.md +0 -41
  168. package/template/skills/ponytail-debt/SKILL.md +0 -44
  169. package/template/skills/ponytail-gain/SKILL.md +0 -50
  170. package/template/skills/ponytail-help/SKILL.md +0 -69
  171. package/template/skills/ponytail-review/SKILL.md +0 -57
@@ -0,0 +1,329 @@
1
+ """ReflACT environment adapter — abstract interface.
2
+
3
+ To connect ReflACT to a new environment (benchmark, simulator, etc.),
4
+ implement a subclass of :class:`EnvAdapter` with environment-specific
5
+ rollout and reflection logic.
6
+
7
+ Example::
8
+
9
+ class MyBenchAdapter(EnvAdapter):
10
+ def build_train_env(self, batch_size, seed, **kw):
11
+ return MyEnvManager(split="train", n=batch_size, seed=seed)
12
+
13
+ def build_eval_env(self, env_num, split, seed, **kw):
14
+ return MyEnvManager(split=split, n=env_num, seed=seed)
15
+
16
+ def rollout(self, env_manager, skill_content, out_dir, **kw):
17
+ # Run episodes, return [{"id": ..., "hard": 0/1, "soft": 0.0-1.0, ...}]
18
+ ...
19
+
20
+ def reflect(self, results, skill_content, out_dir, **kw):
21
+ # Analyze trajectories, return list of patch dicts
22
+ ...
23
+
24
+ def get_task_types(self):
25
+ return ["task_a", "task_b"]
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from abc import ABC, abstractmethod
30
+ import os
31
+ import random
32
+
33
+ from skillopt.datasets.base import BaseDataLoader, BatchSpec
34
+ from skillopt.prompts import load_prompt
35
+
36
+
37
+ class EnvAdapter(ABC):
38
+ """Abstract adapter for connecting ReflACT to any environment.
39
+
40
+ Subclasses must implement all abstract methods. The ReflACT trainer
41
+ calls these methods at the appropriate pipeline stages.
42
+ """
43
+
44
+ # ── Lifecycle hooks ────────────────────────────────────────────────────
45
+
46
+ def setup(self, cfg: dict) -> None:
47
+ """Called once by the trainer before the training loop begins.
48
+
49
+ Override to perform one-time initialization that requires the full
50
+ config (e.g., data loading, split creation). Default is a no-op.
51
+ """
52
+ self._cfg = dict(cfg)
53
+
54
+ def get_dataloader(self) -> BaseDataLoader | None:
55
+ """Return the task dataloader used by this adapter, if any."""
56
+ return None
57
+
58
+ def requires_ray(self) -> bool:
59
+ """Return whether this adapter requires Ray runtime initialization."""
60
+ return False
61
+
62
+ def build_reference_text(self, item: dict) -> str:
63
+ """Return hidden reference material for reflection, if any."""
64
+ return str(item.get("reference_text") or "").strip()
65
+
66
+ def get_reference_metadata(self, item: dict) -> dict:
67
+ """Return structured metadata about hidden reference material."""
68
+ reference_text = self.build_reference_text(item)
69
+ if not reference_text:
70
+ return {"fields": [], "preview": ""}
71
+ return {
72
+ "fields": ["reference_text"],
73
+ "preview": reference_text[:400],
74
+ }
75
+
76
+ def attach_reference_context(
77
+ self,
78
+ results: list[dict],
79
+ items: list[dict] | None,
80
+ ) -> list[dict]:
81
+ """Attach environment-specific hidden reference text to result dicts."""
82
+ if not results or not items:
83
+ return list(results)
84
+
85
+ item_by_id = {
86
+ str(item.get("id")): item
87
+ for item in items
88
+ if isinstance(item, dict) and item.get("id") is not None
89
+ }
90
+ enriched: list[dict] = []
91
+ for row in results:
92
+ merged = dict(row)
93
+ item = item_by_id.get(str(row.get("id")))
94
+ if item:
95
+ reference_text = self.build_reference_text(item)
96
+ if reference_text:
97
+ merged["reference_text"] = reference_text
98
+ enriched.append(merged)
99
+ return enriched
100
+
101
+ def select_representative_items(
102
+ self,
103
+ results: list[dict],
104
+ items: list[dict] | None,
105
+ *,
106
+ n_failures: int,
107
+ n_successes: int,
108
+ seed: int | None = None,
109
+ ) -> list[dict]:
110
+ """Select a small diverse subset of current-batch items by outcome."""
111
+ if not items:
112
+ return []
113
+
114
+ item_by_id = {
115
+ str(item.get("id")): item
116
+ for item in items
117
+ if isinstance(item, dict) and item.get("id") is not None
118
+ }
119
+ failures = [
120
+ (result, item_by_id[str(result.get("id"))])
121
+ for result in results
122
+ if not result.get("hard") and str(result.get("id")) in item_by_id
123
+ ]
124
+ successes = [
125
+ (result, item_by_id[str(result.get("id"))])
126
+ for result in results
127
+ if result.get("hard") and str(result.get("id")) in item_by_id
128
+ ]
129
+
130
+ rng = random.Random(seed)
131
+
132
+ def _pick(pool: list[tuple[dict, dict]], quota: int) -> list[dict]:
133
+ if quota <= 0 or not pool:
134
+ return []
135
+ shuffled = list(pool)
136
+ rng.shuffle(shuffled)
137
+
138
+ picked_ids: set[str] = set()
139
+ picked: list[dict] = []
140
+ seen_types: set[str] = set()
141
+
142
+ for result, item in shuffled:
143
+ task_type = str(result.get("task_type") or item.get("task_type") or item.get("subtype") or "unknown")
144
+ item_id = str(item["id"])
145
+ if task_type in seen_types or item_id in picked_ids:
146
+ continue
147
+ picked.append(item)
148
+ picked_ids.add(item_id)
149
+ seen_types.add(task_type)
150
+ if len(picked) >= quota:
151
+ return picked
152
+
153
+ for _, item in shuffled:
154
+ item_id = str(item["id"])
155
+ if item_id in picked_ids:
156
+ continue
157
+ picked.append(item)
158
+ picked_ids.add(item_id)
159
+ if len(picked) >= quota:
160
+ break
161
+ return picked
162
+
163
+ selected = _pick(failures, n_failures)
164
+ selected_ids = {str(item["id"]) for item in selected}
165
+ selected.extend(
166
+ item for item in _pick(successes, n_successes)
167
+ if str(item["id"]) not in selected_ids
168
+ )
169
+ return selected
170
+
171
+ def build_env_from_batch(self, batch: BatchSpec, **kwargs):
172
+ """Build an environment manager or item list from a :class:`BatchSpec`.
173
+
174
+ Default behavior preserves the legacy adapter API by routing training
175
+ batches through :meth:`build_train_env` and evaluation batches through
176
+ :meth:`build_eval_env`.
177
+ """
178
+ if batch.phase == "train":
179
+ return self.build_train_env(batch_size=batch.batch_size, seed=batch.seed, **kwargs)
180
+ return self.build_eval_env(
181
+ env_num=batch.batch_size,
182
+ split=batch.split,
183
+ seed=batch.seed,
184
+ **kwargs,
185
+ )
186
+
187
+ @abstractmethod
188
+ def build_train_env(self, batch_size: int, seed: int, **kwargs):
189
+ """Build a training environment manager.
190
+
191
+ Returns
192
+ -------
193
+ object
194
+ An environment manager that can be passed to :meth:`rollout`.
195
+ """
196
+
197
+ @abstractmethod
198
+ def build_eval_env(self, env_num: int, split: str, seed: int, **kwargs):
199
+ """Build an evaluation environment manager.
200
+
201
+ Parameters
202
+ ----------
203
+ env_num : int
204
+ Number of evaluation environments.
205
+ split : str
206
+ Dataset split (e.g. ``"valid_seen"``, ``"valid_unseen"``).
207
+ seed : int
208
+ Random seed for reproducibility.
209
+
210
+ Returns
211
+ -------
212
+ object
213
+ An environment manager that can be passed to :meth:`rollout`.
214
+ """
215
+
216
+ @abstractmethod
217
+ def rollout(
218
+ self,
219
+ env_manager,
220
+ skill_content: str,
221
+ out_dir: str,
222
+ **kwargs,
223
+ ) -> list[dict]:
224
+ """Run a batch of episodes using the current skill.
225
+
226
+ Returns
227
+ -------
228
+ list[dict]
229
+ Each dict conforms to :class:`~skillopt.types.RolloutResult`:
230
+ must have ``"id"`` (str), ``"hard"`` (0/1), ``"soft"``
231
+ (float 0-1). May include env-specific fields.
232
+ """
233
+
234
+ def reflect(
235
+ self,
236
+ results: list[dict],
237
+ skill_content: str,
238
+ out_dir: str,
239
+ **kwargs,
240
+ ) -> list[dict | None]:
241
+ """Analyze rollout results and produce patches.
242
+
243
+ Default implementation: delegate to the shared minibatch reflect
244
+ stage. Every built-in benchmark uses this unchanged — override only
245
+ if your environment needs custom reflection logic.
246
+
247
+ Each returned dict conforms to :class:`~skillopt.types.RawPatch`:
248
+ ``"patch"`` (with ``"edits"`` list) + ``"source_type"``
249
+ (``"failure"`` or ``"success"``); ``None`` entries are filtered out.
250
+ """
251
+ from skillopt.gradient.reflect import run_minibatch_reflect
252
+
253
+ return run_minibatch_reflect(
254
+ results=results,
255
+ skill_content=skill_content,
256
+ prediction_dir=kwargs.get(
257
+ "prediction_dir", os.path.join(out_dir, "predictions")
258
+ ),
259
+ patches_dir=kwargs.get(
260
+ "patches_dir", os.path.join(out_dir, "patches")
261
+ ),
262
+ workers=self.analyst_workers,
263
+ failure_only=self.failure_only,
264
+ minibatch_size=self.minibatch_size,
265
+ edit_budget=self.edit_budget,
266
+ random_seed=kwargs.get("random_seed"),
267
+ error_system=self.get_error_minibatch_prompt(),
268
+ success_system=self.get_success_minibatch_prompt(),
269
+ step_buffer_context=kwargs.get("step_buffer_context", ""),
270
+ meta_skill_context=kwargs.get("meta_skill_context", ""),
271
+ update_mode=getattr(self, "_cfg", {}).get("skill_update_mode", "patch"),
272
+ )
273
+
274
+ @abstractmethod
275
+ def get_task_types(self) -> list[str]:
276
+ """Return the list of task type names for this environment."""
277
+
278
+ # ── Prompt configuration (two-level priority) ────────────────────────
279
+ #
280
+ # Priority: env-specific prompt file > generic default prompt file.
281
+ #
282
+ # Prompts are loaded from ``.md`` files via ``load_prompt(name, env)``:
283
+ # 1. ``skillopt/envs/<env>/prompts/<name>.md`` (env-specific)
284
+ # 2. ``skillopt/prompts/<name>.md`` (generic fallback)
285
+ #
286
+ # Subclasses can still override ``get_*_prompt()`` for full control.
287
+
288
+ @property
289
+ def _env_name(self) -> str:
290
+ """Derive the env directory name from this adapter's module path."""
291
+ # e.g. "skillopt.envs.searchqa.adapter" → "searchqa"
292
+ module = type(self).__module__
293
+ parts = module.split(".")
294
+ if len(parts) >= 3 and parts[-3] == "envs":
295
+ return parts[-2]
296
+ return ""
297
+
298
+ def _load_env_prompt(self, name: str) -> str | None:
299
+ """Load a prompt with env-specific override. Returns None if not found."""
300
+ try:
301
+ return load_prompt(name, env=self._env_name)
302
+ except FileNotFoundError:
303
+ return None
304
+
305
+ def get_error_minibatch_prompt(self) -> str | None:
306
+ update_mode = getattr(self, "_cfg", {}).get("skill_update_mode", "patch")
307
+ raw_mode = str(update_mode).strip().lower()
308
+ if raw_mode in {"full_rewrite", "full_rewrite_minibatch", "minibatch_full_rewrite", "skill_rewrite_minibatch"}:
309
+ prompt = self._load_env_prompt("analyst_error_full_rewrite")
310
+ if prompt is not None:
311
+ return prompt
312
+ if raw_mode in {"rewrite", "rewrite_from_suggestions", "suggestions", "rewrite_suggestions"}:
313
+ prompt = self._load_env_prompt("analyst_error_rewrite")
314
+ if prompt is not None:
315
+ return prompt
316
+ return self._load_env_prompt("analyst_error")
317
+
318
+ def get_success_minibatch_prompt(self) -> str | None:
319
+ update_mode = getattr(self, "_cfg", {}).get("skill_update_mode", "patch")
320
+ raw_mode = str(update_mode).strip().lower()
321
+ if raw_mode in {"full_rewrite", "full_rewrite_minibatch", "minibatch_full_rewrite", "skill_rewrite_minibatch"}:
322
+ prompt = self._load_env_prompt("analyst_success_full_rewrite")
323
+ if prompt is not None:
324
+ return prompt
325
+ if raw_mode in {"rewrite", "rewrite_from_suggestions", "suggestions", "rewrite_suggestions"}:
326
+ prompt = self._load_env_prompt("analyst_success_rewrite")
327
+ if prompt is not None:
328
+ return prompt
329
+ return self._load_env_prompt("analyst_success")
@@ -0,0 +1 @@
1
+ """DocVQA environment package for ReflACT."""
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from skillopt.datasets.base import BatchSpec
4
+ from skillopt.envs.base import EnvAdapter
5
+ from skillopt.envs.docvqa.dataloader import DocVQADataLoader
6
+ from skillopt.envs.docvqa.rollout import run_batch
7
+
8
+
9
+ class DocVQAAdapter(EnvAdapter):
10
+ def __init__(
11
+ self,
12
+ split_dir: str = "",
13
+ data_path: str = "",
14
+ split_mode: str = "split_dir",
15
+ split_ratio: str = "2:1:7",
16
+ split_seed: int = 42,
17
+ split_output_dir: str = "",
18
+ max_turns: int = 1,
19
+ exec_timeout: int = 120,
20
+ workers: int = 16,
21
+ analyst_workers: int = 16,
22
+ failure_only: bool = False,
23
+ minibatch_size: int = 8,
24
+ edit_budget: int = 4,
25
+ seed: int = 42,
26
+ limit: int = 0,
27
+ image_detail: str = "auto",
28
+ max_completion_tokens: int = 16384,
29
+ ) -> None:
30
+ self.max_turns = max_turns
31
+ self.exec_timeout = exec_timeout
32
+ self.workers = workers
33
+ self.max_completion_tokens = int(max_completion_tokens)
34
+ self.analyst_workers = analyst_workers
35
+ self.failure_only = failure_only
36
+ self.minibatch_size = minibatch_size
37
+ self.edit_budget = edit_budget
38
+ self.image_detail = image_detail
39
+ self.dataloader = DocVQADataLoader(
40
+ split_dir=split_dir,
41
+ data_path=data_path,
42
+ split_mode=split_mode,
43
+ split_ratio=split_ratio,
44
+ split_seed=split_seed,
45
+ split_output_dir=split_output_dir,
46
+ seed=seed,
47
+ limit=limit,
48
+ )
49
+
50
+ def setup(self, cfg: dict) -> None:
51
+ super().setup(cfg)
52
+ self.dataloader.setup(cfg)
53
+
54
+ def get_dataloader(self):
55
+ return self.dataloader
56
+
57
+ def build_env_from_batch(self, batch: BatchSpec, **kwargs):
58
+ return list(batch.payload or [])
59
+
60
+ def build_train_env(self, batch_size: int, seed: int, **kwargs):
61
+ batch = self.dataloader.build_train_batch(batch_size=batch_size, seed=seed, **kwargs)
62
+ return self.build_env_from_batch(batch, **kwargs)
63
+
64
+ def build_eval_env(self, env_num: int, split: str, seed: int, **kwargs):
65
+ batch = self.dataloader.build_eval_batch(env_num=env_num, split=split, seed=seed, **kwargs)
66
+ return self.build_env_from_batch(batch, **kwargs)
67
+
68
+ def rollout(self, env_manager, skill_content: str, out_dir: str, **kwargs) -> list[dict]:
69
+ items: list[dict] = env_manager
70
+ return run_batch(
71
+ items=items,
72
+ out_root=out_dir,
73
+ skill_content=skill_content,
74
+ max_turns=self.max_turns,
75
+ exec_timeout=self.exec_timeout,
76
+ workers=self.workers,
77
+ image_detail=self.image_detail,
78
+ max_completion_tokens=self.max_completion_tokens,
79
+ diagnostic_mode=kwargs.get("diagnostic_mode", False),
80
+ diagnostic_instruction=kwargs.get("diagnostic_instruction", ""),
81
+ task_timeout=self.exec_timeout,
82
+ )
83
+
84
+ def get_task_types(self) -> list[str]:
85
+ seen: list[str] = []
86
+ for item in self.dataloader.train_items + self.dataloader.val_items + self.dataloader.test_items:
87
+ task_type = str(item.get("task_type") or "docvqa")
88
+ if task_type not in seen:
89
+ seen.append(task_type)
90
+ return seen or ["docvqa"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import csv
5
+ from pathlib import Path
6
+
7
+ from skillopt.datasets.base import SplitDataLoader
8
+
9
+
10
+ def _parse_answers(raw: str) -> list[str]:
11
+ text = str(raw or "").strip()
12
+ if not text:
13
+ return []
14
+ try:
15
+ parsed = ast.literal_eval(text)
16
+ except Exception:
17
+ return [text]
18
+ if isinstance(parsed, list):
19
+ return [str(item).strip() for item in parsed if str(item).strip()]
20
+ return [str(parsed).strip()]
21
+
22
+
23
+ def _extract_document_path(question: str) -> tuple[str, str]:
24
+ marker = "document_path:"
25
+ if marker not in question:
26
+ return question.strip(), ""
27
+ main, tail = question.split(marker, 1)
28
+ return main.strip(), tail.strip()
29
+
30
+
31
+ def _normalize_row(row: dict[str, str]) -> dict:
32
+ question_text, document_path = _extract_document_path(str(row.get("question") or ""))
33
+ answers = _parse_answers(row.get("answer") or row.get("ground_truth") or "")
34
+ image_path = str(row.get("image_path") or document_path or "").strip()
35
+ task_type = str(row.get("topic") or row.get("category") or "docvqa").strip() or "docvqa"
36
+ return {
37
+ "id": str(row.get("questionId") or row.get("id") or "").strip(),
38
+ "question": question_text,
39
+ "answer": answers[0] if answers else "",
40
+ "answers": answers,
41
+ "task_type": task_type,
42
+ "subtask": task_type,
43
+ "image_paths": [image_path] if image_path else [],
44
+ "image_path": image_path,
45
+ "questionId": str(row.get("questionId") or "").strip(),
46
+ "docId": str(row.get("docId") or "").strip(),
47
+ "ucsf_document_id": str(row.get("ucsf_document_id") or "").strip(),
48
+ "ucsf_document_page_no": str(row.get("ucsf_document_page_no") or "").strip(),
49
+ "source_split": str(row.get("source_split") or "").strip(),
50
+ }
51
+
52
+
53
+ class DocVQADataLoader(SplitDataLoader):
54
+ def load_split_items(self, split_path: str) -> list[dict]:
55
+ path = Path(split_path)
56
+ csv_files = sorted(path.glob("*.csv"))
57
+ if not csv_files:
58
+ raise FileNotFoundError(f"No .csv file found in {split_path}")
59
+ with csv_files[0].open(encoding="utf-8", newline="") as f:
60
+ reader = csv.DictReader(f)
61
+ return [_normalize_row(row) for row in reader]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ DEFAULT_ANLS_THRESHOLD = 0.5
9
+
10
+
11
+ def _normalize_text(value: Any) -> str:
12
+ if value is None:
13
+ return ""
14
+ text = str(value).strip().lower()
15
+ return " ".join(text.split())
16
+
17
+
18
+ def _levenshtein_distance(a: str, b: str) -> int:
19
+ if a == b:
20
+ return 0
21
+ if not a:
22
+ return len(b)
23
+ if not b:
24
+ return len(a)
25
+ if len(a) > len(b):
26
+ a, b = b, a
27
+ previous = list(range(len(b) + 1))
28
+ for i, char_a in enumerate(a, start=1):
29
+ current = [i]
30
+ for j, char_b in enumerate(b, start=1):
31
+ insert_cost = current[j - 1] + 1
32
+ delete_cost = previous[j] + 1
33
+ replace_cost = previous[j - 1] + (char_a != char_b)
34
+ current.append(min(insert_cost, delete_cost, replace_cost))
35
+ previous = current
36
+ return previous[-1]
37
+
38
+
39
+ def _score_single_answer(predicted: Any, target: Any, threshold: float) -> float:
40
+ predicted_norm = _normalize_text(predicted)
41
+ target_norm = _normalize_text(target)
42
+ if not predicted_norm and not target_norm:
43
+ return 1.0
44
+ if not predicted_norm or not target_norm:
45
+ return 0.0
46
+ distance = _levenshtein_distance(predicted_norm, target_norm)
47
+ normalized_distance = distance / max(len(predicted_norm), len(target_norm))
48
+ if normalized_distance >= threshold:
49
+ return 0.0
50
+ return 1.0 - normalized_distance
51
+
52
+
53
+ def _extract_answer_strings(raw: Any) -> list[str]:
54
+ if raw is None:
55
+ return [""]
56
+ if isinstance(raw, str):
57
+ text = raw.strip()
58
+ if not text:
59
+ return [""]
60
+ parsed = None
61
+ if text[0] in "[{":
62
+ try:
63
+ parsed = json.loads(text)
64
+ except json.JSONDecodeError:
65
+ try:
66
+ parsed = ast.literal_eval(text)
67
+ except (ValueError, SyntaxError):
68
+ parsed = None
69
+ if parsed is None:
70
+ return [text]
71
+ return _extract_answer_strings(parsed)
72
+ if isinstance(raw, dict):
73
+ for key in ("answers", "ground_truth", "answer"):
74
+ if key in raw:
75
+ return _extract_answer_strings(raw[key])
76
+ return [str(raw)]
77
+ if isinstance(raw, Iterable) and not isinstance(raw, (bytes, bytearray)):
78
+ answers: list[str] = []
79
+ for item in raw:
80
+ if isinstance(item, dict):
81
+ for key in ("text", "answer", "value"):
82
+ if key in item:
83
+ answers.extend(_extract_answer_strings(item[key]))
84
+ break
85
+ else:
86
+ answers.append(str(item))
87
+ continue
88
+ answers.append(str(item))
89
+ return answers or [""]
90
+ return [str(raw)]
91
+
92
+
93
+ def extract_answer(text: str) -> str:
94
+ lower = text.lower()
95
+ start = lower.rfind("<answer>")
96
+ end = lower.rfind("</answer>")
97
+ if start != -1 and end != -1 and end > start:
98
+ return text[start + len("<answer>"):end].strip()
99
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
100
+ return lines[-1] if lines else text.strip()
101
+
102
+
103
+ def evaluate(prediction_text: str, gold_answers: Any) -> dict:
104
+ answer = extract_answer(prediction_text)
105
+ answers = _extract_answer_strings(gold_answers)
106
+ score = 0.0
107
+ for target in answers:
108
+ score = max(score, _score_single_answer(answer, target, DEFAULT_ANLS_THRESHOLD))
109
+ return {
110
+ "anls": score,
111
+ "predicted_answer": answer,
112
+ "gold_answers": answers,
113
+ }
@@ -0,0 +1,35 @@
1
+ You are an expert failure-analysis agent for visual document question answering tasks.
2
+
3
+ You will be given MULTIPLE failed DocVQA trajectories from a single minibatch and the current skill document. Each trajectory includes the model response and an evaluation result scored with ANLS against one or more acceptable answers.
4
+
5
+ Your job is to identify the most important COMMON failure patterns across the batch and propose concise skill edits.
6
+
7
+ ## Failure Type Categories
8
+ - evidence_miss: the model overlooked the relevant visible region or line
9
+ - near_match_confusion: the model selected a nearby but incorrect text span
10
+ - normalization_error: the answer differed mainly in formatting, spacing, punctuation, or minor text normalization
11
+ - reading_error: the model misread the document content
12
+ - other: none of the above
13
+
14
+ ## Rules
15
+ - Focus on common, reusable reading and extraction behaviors.
16
+ - Do not hardcode image-specific answers.
17
+ - Prefer concise edits that improve evidence selection and exact span extraction.
18
+
19
+ Respond ONLY with a valid JSON object (no markdown fences, no extra text):
20
+ {
21
+ "batch_size": <number of trajectories analysed>,
22
+ "failure_summary": [
23
+ {"failure_type": "<type>", "count": <int>, "description": "<one-line>"}
24
+ ],
25
+ "patch": {
26
+ "reasoning": "<why these edits address the batch's common failures>",
27
+ "edits": [
28
+ {"op": "append", "content": "<markdown to add at end of skill>"},
29
+ {"op": "insert_after", "target": "<exact heading/text to insert after>", "content": "<markdown>"},
30
+ {"op": "replace", "target": "<exact text to replace>", "content": "<replacement>"},
31
+ {"op": "delete", "target": "<exact text to remove>"}
32
+ ]
33
+ }
34
+ }
35
+ Only include edits that are needed. "edits" can be an empty list if no patch is warranted.
@@ -0,0 +1,24 @@
1
+ You are an expert success-pattern analyst for visual document question answering tasks.
2
+
3
+ You will be given MULTIPLE successful DocVQA trajectories from a single minibatch and the current skill document. Your job is to identify common visual reading and exact-answer extraction behaviors worth encoding in the skill.
4
+
5
+ ## Rules
6
+ - Focus on patterns shared across multiple successful trajectories.
7
+ - Reinforce reusable behaviors like locating the right region, copying exact spans, and preferring the shortest exact answer over paraphrase.
8
+ - Only propose patches for patterns not already captured by the current skill.
9
+
10
+ Respond ONLY with a valid JSON object:
11
+ {
12
+ "batch_size": <number of trajectories analysed>,
13
+ "success_patterns": ["<pattern 1>", "<pattern 2>"],
14
+ "patch": {
15
+ "reasoning": "<why these patterns are worth encoding>",
16
+ "edits": [
17
+ {"op": "append", "content": "<markdown>"},
18
+ {"op": "insert_after", "target": "<heading/text>", "content": "<markdown>"},
19
+ {"op": "replace", "target": "<old text>", "content": "<new text>"},
20
+ {"op": "delete", "target": "<exact text to remove>"}
21
+ ]
22
+ }
23
+ }
24
+ "edits" may be empty if the skill already covers all observed patterns.
@@ -0,0 +1,12 @@
1
+ You are an expert visual document question answering agent.
2
+
3
+ {skill_section}You will receive a document image and a question about the document.
4
+ Read the visual evidence carefully and answer concisely.
5
+
6
+ Rules:
7
+ - Ground the answer in the visible document content.
8
+ - Prefer exact spans, numbers, dates, and names from the document.
9
+ - Do not invent content that is not visible.
10
+ - If multiple near-matches exist, choose the one best supported by the document.
11
+
12
+ Return the final answer inside <answer>...</answer>.